Backbone calling Controller method from Views - backbone.js

[new to Backbone]
Working on a schedule App .use case is.
1) User Clicks reschedule button . [in View]
2) Fetch available schedule . [in Controller]
3) User Selects one of schedule. [in View]
4) Post Updated schedule list to server. [in Controller]
5) Re render View
Issue is (1),(3) are on View and (2),(4) are on Controller.
I need to route View -> Route -> Controller. for each step.
Is this the right way to pass events to controller . Is there a better way available ??
Is this the case for using Backbone.Events.
Folder Structure. [using Require , Backbone ]
├───app
│ ├───managers
│ ├───models
│ ├───utils
│ └───views
├───lib
└───tpl
Schedule Model.
Schedule = Backbone.Model.extend({
initialize: function () {
console.log("Schedule model initialized");
}
}),
ScheduleView :
events : {
"click #cancel_login_trip" : "cancelLoginTrip",
"click #cancel_logout_trip" : "cancelLogoutTrip",
},
cancelLoginTrip : function(){
var tripindex = this.model.get('previousIndex')+1;
tripindex = "login_"+tripindex;
Backbone.history.navigate('schedule/cancel/'+tripindex, {trigger:true,replace:true});
},
Route
cancelSchedule : function (tripindex) {
var t = tripindex.split("_");
var dic={};
dic.tripindex = parseInt(t[1]);
dic.triptype = t[0];
scheduleManager.cancelSchedule(dic);
},
Schedule Manager [ Controller ] :
this.cancelSchedule = function(dic){
console.log("inside cancel schedule");
console.log(dic);
index = dic.tripindex;
scheduleDaysize = configManager.getFeatureConfig(Constants.SCHEDULE,Constants.SCHEDULE_DAYS_AFTER);
myAnalyticsLogger.debug(LogMessages.REQUEST_TO_DISPLAY_SCHEDULE + index);
if(index<0){
var errorText= 'past schedule can not be viewed ';
errorModel.set({errorText:errorText,response:""});
return;
}
if(index >= scheduleCollection.length && scheduleDaysize != null){
var errorText= 'this schedule can not be cancelled ';
errorModel.set({errorText:errorText,response:""});
return;
}
updateScheduleOnServer(scheduleCollection.length,
scheduleCollection.length+scheduleDaysize);
};

My personal approach would be create many small views, and have on large model with all logic and drop the middle controller. If this is not an option, best way of decoupling imho would be through global event model
var vent = {}
_.extend(vent,Backbone.Events);
vent.on()
vent.trigger()
you can have it global for all application, or specific for a part of the domain

Related

ng-table reset page to 1 on sort

I am using ng-table in my application, I was looking to reset current page to 1 when user changes sort order. I gone through ng-table documentation, but no use.
You should be able to do this via the page() function of NgTableParams:
$scope.tableParams.page(1);
For that, you need to subscribe to the ngTableEventsChannel.afterReloadData(); that is fired after sorting changes. There's an example that logs the events and shows how to subscribe.
This works for me:
var vm = this;
vm.tableParams = new NgTableParams();
vm.recentPage = 1;
ngTableEventsChannel.onAfterDataSorted(function() {
vm.tableParams.page(1);
vm.tableParams.reload();
}, $scope, function(tableParams) {
var reset = tableParams._params.page !== 1 && vm.recentPage === tableParams._params.page;
vm.recentPage = tableParams._params.page;
return reset;
});

Routing in SAPUI5: How to implement passing of URL? Model data not initialy loaded

My goal is to write a SAPUI5 Fiori app with routing support. One mail goal is to have passable URLs. For example in an E-Mail like "please approve this: link". The link is an URL matched by my rounting config, e.g.index.html#/applicants/8.
I use a typical sap.m.SplitApp kind of application. Clicking a list item in masterview changes the URL to index.html#/applicants/[id of entry in JSON]. I can click on the list, my defined routes are getting matched and the apps loads the (applicant) data as expected.
However, and here comes my question, this doeas not work when using an URL directly, say pasting [my url]/index.html#/applicants/8 into my browser. The app is launched but no detail data is loaded. I have to click on another list item again to get the data.
Actually, the controller is called when passing the URL, but it seems the model is not initiated and undefined. My JSON model is bound in the createContent function of my Component.js
// Update 2015-05-14
The problems seems to be around the getData() function. I have the model, it has the entries, but getData() returns undefined for the first time my app is loaded. I recently read getData() is deprecated. How should I improve my coding below?
// Component.js
ui5testing.Component.prototype.createContent = function(){
// create root view
var oView = sap.ui.view({
id : "app",
viewName : "ui5testing.view.Main",
type : "JS",
viewData : {
component : this
}
var oModel = new sap.ui.model.json.JSONModel("model/mock_applicants.json");
oView.setModel(oModel);
[...]
return oView;
});
// Master controller
handleApplicantSelect : function (evt) {
var oHashChanger = sap.ui.core.routing.HashChanger.getInstance();
var context = evt.getParameter("listItem").getBindingContext();
var path = context.getPath();
var model = this.getView().getModel();
var item = model.getProperty(path);
oHashChanger.setHash("applicants/" + item.id);
},
// Detail controller
onInit: function() {
this.router = sap.ui.core.UIComponent.getRouterFor(this);
this.router.attachRoutePatternMatched(this._handleRouteMatched, this);
},
_handleRouteMatched : function(evt){
var objectId = evt.getParameter("arguments").id;
var model = this.getView().getModel();
var data = model.getData()["applicants"];
var pathId;
if (data) {
for (var i = 0; data.length; i++) {
if ( objectId == data[i].id ) {
pathId = i;
break;
}
}
var sPath = "/applicants/" + pathId;
var context = new sap.ui.model.Context(model, sPath)
this.getView().setBindingContext(context);
}
},
As you've figured out that getData() returns undefined for the first time, which means the model data is still not yet loaded. So you can make use of attachRequestCompleted method of the model & fire an event from the component & listen to that event in the detail controller to ensure the routerPatternMatched() gets executed only after the data is loaded.
//Component.js
var oModel = new sap.ui.model.json.JSONModel("model/mock_applicants.json");
oModel.attachRequestCompleted(jQuery.proxy(function(){
this.fireEvent("MockDataLoaded"); // fireEvent through component
},this));
oView.setModel(oModel);
//Detail controller
onInit : function(){
this.router = sap.ui.core.UIComponent.getRouterFor(this);
var oComponent = this.getOwnerComponent();
oComponent.attachEvent("MockDataLoaded",jQuery.proxy(function(){
this.router.attachRoutePatternMatched(this._handleRouteMatched, this);
},this));
}
Or the simplest & but the dirty way would be to make an synchronous request instead of an async request to load data.
var oModel = new sap.ui.model.json.JSONModel();
oModel.loadData(""model/mock_applicants.json",{bAsync:false});
oView.setModel(oModel);

Is it good practice to manage view instantiation in a router?

So this is my first Backbone project and I'm wondering if I'm doing things in the best way. My app basically has two states, one of them displays a search box and the other displays a search box with a table under it. My router has routes for searching and for the initial landing page with just the search view. When the user types in the query the router navigates to the search route and the table view is added to the page. This is my router:
app.Router = Backbone.Router.extend({
routes: {
'': 'index',
'search/coords=:address&age=:age&rad=:rad': 'search'
},
search: function(address, age, rad){
app.statusView || (app.statusView = new app.StatusView());
app.searchView || (app.searchView = new app.SearchView());
app.trigger('status:loading');
app.Practices.fetch({
reset: false,
success: function() {
app.searchView.setElement($('#search-box')).render();
var searchQuery = new app.SearchQueryModel({age: age, coords: address.split(","), radius: rad});
if (!app.tableView){
app.tableView = new app.TableView({model: searchQuery});
} else {
app.tableView.model = searchQuery;
app.tableView.refresh();
};
}
});
app.trigger('status:clear');
},
index: function() {
app.statusView = new app.StatusView();
app.searchView = new app.SearchView();
app.footerView = new app.FooterView();
app.searchView.setElement($('#search-box')).render();
}
});
As you can see my views are instantiated in the index route and then the same views are used when you search, unless the user is going directly to the search page in which case the views are instantiated there. I'd be surprised if this wasn't very sub-optimal because it seems clumsy to be checking if the view already exists in the search route. Is there a better way of doing things?
Lets say its not bad, but there is one better approach.
As for now you router is in charge of hook-up URL with app astatus and also for view and model control. The second may be detached from Router, so you will need Controller abstraction, but Backbone does not provide Controller "from the box".
But this is not the problem, you can use plugin or take a look at Controller realization in Marionette.js
The main idea here is to split responsibilities between app part correctly:
1) Router - keeps routes and hook up URL with controller action
2) Controller - manage views and models (create, delete, fetch and so on)
3) View - listen to model and DOM events and render data
4) Model - provide actual data and work with data.
First of all welcome to Backbone. It is a lovely framework which can allow you to make things as beautiful or ugly as you'ld like. Your question is about where view instantiation should be, in terms of good practices. Of course it seems sort of wrong to do it there as it violates the Law of Demeter by handling both url routing and view instantiation.
But the views have to be run from somewhere right? If not the router then where?
So I have two responses:
If your app is simple and you just want to play with backbone then you're probably going to be fine. A lot of people let single page app frameworks complicate otherwise simple apps. I'm not trying to be lazy, but where you have it now is the natural beginner's choice in Backbone. If this is your case then stop here.
If you want to use the full power of backbone to custom make a framework then read on.
So my setup is designed to be able to start a new project using some boilerplate functions and create only a few classes which are specific to the new app. Route handling and all of that kind of thing seems low-level enough to me that it should be just part of some configuration that I don't want to look at often. The upshot is that my router looks like this:
define([
'autorouter'
], function(AutoRouter){
var AppRouter = AutoRouter.extend({
autoRoutes: {
":page" : "routeDirect",
":page/:object" : "routeDirect",
":page/:object/:action" : "routeDirect",
"": "routeDirect"
}
});
return AppRouter;
});
Then for each new project I have a file where I keep the non-default routes, for instance:
define(function(require){
return {
"schedule" : require('screens/schedule')
, "logout" : require('screens/logout')
, "login" : require('screens/login')
, "create" : require('screens/create')
, "upload" : require('screens/upload')
, "select" : require('screens/selection')
, "inventory" : require('screens/inventory')
, "describe" : require('screens/description')
}
});
I put each screen into it's own file (using requirejs for the multi-file dependency management). The extra variables get passed through to the screen.
Each screen is the brain for a particular user experience and is responsible for loading views and maybe handling some events while that screen is active.
If that seems like an interesting setup then here is how I did it:
For the router itself I use a boilerplate class which I borrowed from Derick Bailey with some slight modifications:
define([
'jquery', 'underscore', 'backbone'],
function($, _, Backbone) {
var AutoRouter = Backbone.Router.extend({
constructor: function(options){
Backbone.Router.prototype.constructor.call(this, options);
var that = this;
that.app = options.app;
if (this.autoRoutes){
that.processAutoRoutes(options.app, that.autoRoutes);
}
},
processAutoRoutes: function(app, autoRoutes){
var method, methodName;
var route, routesLength;
var routes = [];
var router = this;
for(route in autoRoutes){
routes.unshift([route, autoRoutes[route]]);
}
routesLength = routes.length;
for (var i = 0; i < routesLength; i++){
route = routes[i][0];
methodName = routes[i][1];
method = app[methodName];
router.route(route, methodName, method);
}
}
});
return AutoRouter;
});
I never have to look at it, but I do need to pass it an app instance. For example:
this.appRouter = new AppRouter({app : this});
Finally my route direction function:
define(function(require){
var pathParser = function(path){
return Array.prototype.slice.call(path);
}
var pathApply = function(path, routes, context){
var pathArray = pathParser(path);
var primary = pathArray[0];
if (routes.hasOwnProperty(primary)){
routes[primary].apply(context, pathArray.slice(1));
} else {
routes["default"].apply(context, pathArray.slice(1));
}
}
return function(path){
//NOTE PLEASE that this references AutoRouter
//Which has an app property
var oApp = this.app;
var pathRoutes = _.extend(require('urls'), {
"default" : require('screens/default')
});
pathApply(arguments, pathRoutes, oApp);
};
});
So, did I make things better? Well if you're doing something very simple with just a screen or two, then you certainly don't want to build this sort of setup from scratch. But if you're like me, and you want to be able to quickly produce new projects then having some boilerplate like the two classes above allows for one JSON object to tell the app which routes I should send to which screens. Then I can have all of the logic in the appropriate places, allowing separation of concerns. Which is why I think Backbone is so pleasant.
My understanding of your problem is that you are triggering a route each time you are hitting search.
If this is how you are doing it, then use view events hash (used to capture and handle events that happen in a view) for search.Don't use routes. Define an events hash in the view and have a callback to handle the search.
var myAppEventBus = _.extend({},Backbone.Events);
var myAppController = {
function : search(options) {
// create an instance of the collection and do a fetch call passing the
// search parameters to it.
var searchResultsCollection = new SearchResultsCollection();
// pass search criteria, the success and error callbacks to the fetch
// method.
var that = this;
searchResultsCollection.fetch(
{
data:that.options,
success : function() {
// Pass the fetched collection object in the trigger call so that
// it can be
// received at the event handler call back
var options = {
"searchResultsCollection" : that.searchResultsCollection;
};
myAppEventBus.trigger("search_event_triggered",options);
},
error : function() {
// do the error handling here.
}
}
);
}
};
// Application Router.
var MyAppRouter = Backbone.Router.extend({
routes : {
'search/coords=:address&age=:age&rad=:rad': 'search'
},
search : function(searchParams) {
// Fetch the query parameters and pass it to the view.
var routeSearchExists = false;
var searchOptions = {};
var options = {};
if(searchParams) {
routeSearchExists = true;
// If search params exist split and set them accordingly in
// the searchOptions object.
options.searchOptions = searchOptions;
}
// Create and render the search view. Pass the searchOptions
var searchView = new SearchView(options);
searchView.render();
// Create and render an instance of the search results view.
var searchResultsView = new SearchResultsView();
searchResultsView.render();
// If there are search parameters from the route, then do a search.
if(routeSearchExists) {
searchView.search();
}
}
});
// The main view that contains the search component and a container(eg: div)
// for the search results.
var SearchView = Backbone.View.extend({
el : "#root_container",
searchOptions : null,
initialize : function(options) {
// Intialize data required for rendering the view here.
// When the user searches for data thru routes, it comes down in the
// options hash which can then be passed on to the controller.
if(options.searchOptions) {
this.searchOptions = options.searchOptions;
}
},
events : {
"search #search_lnk":"initSearch"
},
initSearch : function(event) {
event.preventDefault();
var searchOptions = {};
// Fetch the search fields from the form and build the search options.
myAppController.search(searchOptions);
},
search : function() {
if(this.searchOptions) {
myAppController.search(searchOptions);
}
}
});
// The view to display the search results.
var SearchResultsView = Backbone.View.extend({
searchResultsCollection : null;
initialize : function(options) {
// Handling the triggered search event.
myAppEventBus.on("search_event_triggered",this.render,this);
},
render : function(options) {
//search results collection is passed as a property in options object.
if(options.searchResultsCollection)
//Render your view.
else
// Do it the default way of rendering.
}
});
SearchView is the root view that contains the search component and a container like div to hold the search results.
SearchResultsView displays the result of a search.
When search option is clicked, the event callback (initSearch) gets the entered search data.
The search method on myAppController object is invoked and the search query is passed.
An instance of the search collection is created and fetch is invoked passing it the search query and also the success and error callback.
On success, a custom backbone event is triggered along with the fetched collection.
The callback(render method in SearchResultsView) for this event is invoked.
The callback renders the results of the search.
When loading in the router an instance for both the views can be created(the results view will be empty) and attached to the dom.
If you wish to search by multiple query strings at the url then I would suggest you to use the following route.
search?*queryString.
In the route callback make a call to a utility function the splits the querystring and returns you a search object and pass on the search string to the view.

Bind and trigger backbone event to a specific view

I'm creating an ajax upload component which consists of a progress bar for each backbone view, this is how my view template looks like.
<script id="view-template-dropped-file" type="text/html">
<a><%=name %></a><span><%=fileSize%></span>
<div class="ui-progress-bar">
<div class="ui-progress"></div>
</div>
</script>
When I drop files on my drop area I create a view for each file like this
for (i = 0; i < files.length; i++) {
var view = new DroppedFileView({
model: new DroppedFile({
name: files[i].name,
fileSize: files[i].size
})
});
var $li = view.render().$el;
$('#droparea ul').append($li);
});
The drop area with some files added showing a progress bar for each file. http://cl.ly/Lf4v
Now when I press upload I need to show the progress for each file individually.
What I tried to do was to bind to an event in my DroppedFileView like this
initialize: function() {
var app = myapp.app;
app.bind('showProgress', this._progress, this);
}
and the _progress function
_progress: function(percentComplete) {
this.$el.find('.ui-progress').animateProgress((percentComplete * 100), function () { }, 2000);
}
and this is how I trigger the event from the drop area view
xhr: function () {
var xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener("progress", function (e) {
if (e.lengthComputable) {
var percentComplete = e.loaded / e.total;
app.trigger('showProgress', percentComplete);
}
}, false);
return xhr;
}
of course this will not work because I listen to the same showProgress event in all views which will cause all progress bars to show the same progress.
So, is it possible to bind an event to a specified view so the progress can be updated individually or is events not a good approach?
You might want to consider making the DroppedFile model emit the progress events. So simply instead of triggering the event on app, trigger it on the model instance which is being uploaded.
Your sample code doesn't mention which class holds the xhr method, but it would make sense to define it on the model itself. In which case the event triggering is trivial:
xhr: function () {
var model = this;
var xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener("progress", function (e) {
if (e.lengthComputable) {
var percentComplete = e.loaded / e.total;
model.trigger('showProgress', percentComplete);
}
}, false);
return xhr;
}
And in view constructor:
initialize: function() {
this.model.bind('showProgress', this._progress, this);
}
Edit based on comments:
Even if your view structure is a bit more complicated than I assumed above, in my opinion using the DroppedFile model as event emitter is the way to go. If one DroppedFileView represents DroppedFile, it should reflect the state of the model it makes sense.
Just keep track of the models in DropzoneView, just like (or instead of how) you do now with the files in the DropzoneView.files. Whether you want to have the actual AJAX request to be the responsibility of the view or refactor it to the individual models doesn't really matter.

how to do polling in backbone.js?

Hi i am working on a paly2.0 framework application(with java) using backbone.js. In my application i need to get the table data from the database regularly ( for the use case of displaying the upcoming events list and if the crossed the old event should be removed from the list ).I am getting the data to display ,but the issue is to hit the Database regularly.For that i tried use backbone.js polling concept as per these links Polling a Collection with Backbone.js , http://kilon.org/blog/2012/02/backbone-poller/ .But they not polling the latest collection from db. Kindly suggest me how to achieve that or any other alternatives ?
Thanks in adv.
There is not a native way to do it with Backbone. But you could implement long polling requests adding some methods to your collection:
// MyCollection
var MyCollection = Backbone.Collection.extend({
urlRoot: 'backendUrl',
longPolling : false,
intervalMinutes : 2,
initialize : function(){
_.bindAll(this);
},
startLongPolling : function(intervalMinutes){
this.longPolling = true;
if( intervalMinutes ){
this.intervalMinutes = intervalMinutes;
}
this.executeLongPolling();
},
stopLongPolling : function(){
this.longPolling = false;
},
executeLongPolling : function(){
this.fetch({success : this.onFetch});
},
onFetch : function () {
if( this.longPolling ){
setTimeout(this.executeLongPolling, 1000 * 60 * this.intervalMinutes); // in order to update the view each N minutes
}
}
});
var collection = new MyCollection();
collection.startLongPolling();
collection.on('reset', function(){ console.log('Collection fetched'); });

Resources