I'm currently developing my first Backbone single page app project and I'm facing an issue.
Basically I have a menu (html select input element) implemented as a View. Its value is used to control pretty much every other data requests since it specifies which kind of data to show in the other Views.
Right now I handle the DOM event and trigger a global event so that every model can catch it and keep track internally of the new value. That's because that value is then needed when requesting new data. But this doesn't look like a good solution because A) I end up writing the same function (event handler) in every model and B) I get several models with the same variable.
var Metrics = Backbone.Collection.extend({
url: "dummy-metrics.json",
model: MetricsItem,
initialize: function () {
this.metric = undefined;
},
setMetric: function (metric) {
this.metric = metric;
globalEvents.trigger("metric:change", this.get(metric));
}
});
var GlobalComplexity = Backbone.Collection.extend({
url: function () {
var url = "http://asd/global.json?metric=" + this.metric;
return url;
}, //"dummy-global.json",
model: GlobalComplexyItem,
initialize: function () {
this.metric = undefined;
this.listenTo(globalEvents, "metric:change", this.updateMetric);
},
updateMetric: function (metric) {
this.metric = metric.get("id");
this.fetch({ reset: true });
}
});
All my other Collections are structured like GlobalComplexity.
What's the cleanest way to solve this problem?
Thank you very much.
Define a global parametersManager. Export an instance (singleton) then require it when you need it.
On "globalupdate" you update the parametersManager then trigger "update" for all your model/collections so they'll look what are the current parameters in the parametersManager.
Related
I have a template that needs to receive data from two different API endpoints (URLs): cart and user.
I want the two endpoints to act as one model or collection so that I can do something like .changedAttributes() or sync, or fetch.
I know Backbone is very permissive, but I am really lost.
Playground:
I've created a codepen to see what I've done so far: http://codepen.io/anything/pen/AXoBoa
Desired result should be something like:
initialize: function(){
var self = this;
collection.fetch({
success: function(data){
self.collection = data;
}
})
},
render: function(){
var self = this;
var source = $("#template").html();
var template = Handlebars.compile(source);
var htmlToRender = template(self.collection.toJSON());
}
You could create an event concentrator listening to its registered objects and retrigger the events you catch.
Something like
var aggregate = _.extend({}, Backbone.Events);
aggregate.register = function(m) {
var self = this;
this.listenTo(m, 'all', function() {
this.trigger.apply(this, arguments);
});
};
You would then use it like this
aggregate.on('change', function(m) {
// do what you have to do when one of the models change
console.log('Change on ', m.toJSON());
});
aggregate.on('sync', function(m) {
//same thing for syncs
console.log('sync ', m.toJSON());
});
var m1 = new Backbone.Model({id: 1});
var m2 = new Backbone.Model({id: 2});
aggregate.register(m1);
aggregate.register(m2);
m1.fetch();
m2.set({data: 2});
And a demo http://jsfiddle.net/nikoshr/hm0xc79z/
A slightly different approach to nikoshr based on http://backbonejs.org/#Events. The basic idea is that you set up an event object that can be referenced in both views. You can name the events whatever you want and they can triggered and listened to wherever the object is available.
Create event object. In your case, add it to your main appshell object.
appShell.Events = _.extend({}, Backbone.Events);
When user performs action in user view like logout, trigger an event.
appShell.Events.trigger('user:logout');
Listen to the event in another view and perform action based off of it.
this.listenTo(appShell.Events, 'user:logout', this.doSomething);
Put logic in the doSomething to do whatever you need to do on the other view.
I have a config.json that I am going to load into my app as a Backbone Model like:
var Config = Backbone.Model.extend({
defaults: {
base: ''
},
url: 'config.json'
});
Other models should be dependent on some data contained in Config like:
var ModelA = Backbone.Collection.extend({
initialize: function(){
//this.url should be set to Config.base + '/someEndpoint';
}
});
In above example, ModelA's url property is dependent on Config's base property's value.
How do I go about setting this up properly in a Backbone app?
As I see it, your basic questions are:
How will we get an instance of the configuration model?
How will we use the configuration model to set the dependent model's url?
How can we make sure we don't use the url function on the dependent model too early?
There are a lot of ways to handle this, but I'm going to suggest some specifics so that I can just provide guidance and code and "get it done," so to speak.
I think the best way to handle the first problem is to make that configuration model a singleton. I'm going to provide code from backbone-singleton GitHub page below, but I don't want the answer to be vertically long until I'm done with the explanation, so read on...
var MakeBackboneSingleton = function (BackboneClass, options) { ... }
Next, we make a singleton AppConfiguration as well as a deferred property taking advantage of jQuery. The result of fetch will provide always(callback), done(callback), etc.
var AppConfiguration = MakeBackboneSingleton(Backbone.Model.extend({
defaults: {
base: null
},
initialize: function() {
this.deferred = this.fetch();
},
url: function() {
return 'config.json'
}
}));
Now, time to define the dependent model DependentModel which looks like yours. It will call AppConfiguration() to get the instance.
Note that because of MakeBackboneSingleton the follow is all true:
var instance1 = AppConfiguration();
var instance2 = new AppConfiguration();
instance1 === instance2; // true
instance1 === AppConfiguration() // true
The model will automatically fetch when provided an id but only after we have completed the AppConfiguration's fetch. Note that you can use always, then, done, etc.
var DependentModel = Backbone.Model.extend({
initialize: function() {
AppConfiguration().deferred.then(function() {
if (this.id)
this.fetch();
});
},
url: function() {
return AppConfiguration().get('base') + '/someEndpoint';
}
});
Now finally, putting it all together, you can instantiate some models.
var newModel = new DependentModel(); // no id => no fetch
var existingModel = new DependentModel({id: 15}); // id => fetch AFTER we have an AppConfiguration
The second one will auto-fetch as long as the AppConfiguration's fetch was successful.
Here's MakeBackboneSingleton for you (again from the GitHub repository):
var MakeBackboneSingleton = function (BackboneClass, options) {
options || (options = {});
// Helper to check for arguments. Throws an error if passed in.
var checkArguments = function (args) {
if (args.length) {
throw new Error('cannot pass arguments into an already instantiated singleton');
}
};
// Wrapper around the class. Allows us to call new without generating an error.
var WrappedClass = function() {
if (!BackboneClass.instance) {
// Proxy class that allows us to pass through all arguments on singleton instantiation.
var F = function (args) {
return BackboneClass.apply(this, args);
};
// Extend the given Backbone class with a function that sets the instance for future use.
BackboneClass = BackboneClass.extend({
__setInstance: function () {
BackboneClass.instance = this;
}
});
// Connect the proxy class to its counterpart class.
F.prototype = BackboneClass.prototype;
// Instantiate the proxy, passing through any arguments, then store the instance.
(new F(arguments.length ? arguments : options.arguments)).__setInstance();
}
else {
// Make sure we're not trying to instantiate it with arguments again.
checkArguments(arguments);
}
return BackboneClass.instance;
};
// Immediately instantiate the class.
if (options.instantiate) {
var instance = WrappedClass.apply(WrappedClass, options.arguments);
// Return the instantiated class wrapped in a function so we can call it with new without generating an error.
return function () {
checkArguments(arguments);
return instance;
};
}
else {
return WrappedClass;
}
};
I am struggling with when to destroy backbone views. I know I need to destroy the view somewhere, but I am not sure where.
I have the following code in router.js
routes: {
"names/search": "nameSearch",
"companies/search": "companySearch"
},
initialize: function(){
Backbone.history.start();
this.navigate("#/", true);
}
nameSearch: function () {
require(["app/views/RecordSearch"], function (RecordSearchView) {
var obj = {};
obj.Status = [utils.xlate("On Assignment"), utils.xlate("Candidate")];
var view = new RecordSearchView({ model: obj, el: $(".content") }, { "modelName": "Candidate" });
view.delegateEvents();
});
},
companySearch: function () {
require(["app/views/RecordSearch"], function (RecordSearchView) {
var view = new RecordSearchView({ model: {}, el: $(".content") }, { "modelName": "Company" });
view.delegateEvents();
});
}
And then in RecordSearchView.js I have the following function that is called when a user clicks the search button
doSearch: function () {
require(["app/utils/SearchHelper", "app/models/" + modelName, "app/views/SearchResults"], function (SearchHelper, Model, SearchResultsView) {
var obj = $("#searchForm").serializeArray();
var params = SearchHelper.getQuery(obj);
params["page"] = 1;
params["resultsPerPage"] = 25;
var collection = new Model[modelName + "Collection"]({}, { searchParams: params });
params["Fields"] = collection.getSearchFields();
collection.getPage(params["page"], function (data) {
require(["app/views/SearchResults"], function (SearchResultsView) {
App.Router.navigate(modelName + "/search/results");
var view = new SearchResultsView({ collection: data, el: $(".content") });
view.delegateEvents();
});
});
return false;
});
And SearchResults.js
return BaseView.extend({
init: function () {
this.render();
},
render: function () {
var data = this.collection.convertToSearchResults();
this.$el.html(template(data));
return this;
}
});
The problem is the second time I perform any search (calling the doSearch function from RecordSearch.js). As soon as I perform the second search, the data shown is that belonging to the previous search I performed. (For example I do a name search and it works, then do a company search but the screen shows company search results but then is quickly replaced with name search results).
My questions are
I suspect I need to call some cleanup code on the view before it is re-used. Where is the proper place within a backbone application to run this.
Is there anything wrong with the way I load SearchResults view from within RecordSearch view? SearchResults does not have a path on my router, but it is basically a form post, so I assume it shouldn't?
Any help is appreciated.
This problem is quite common and is known as Zombie Views. Derick Bailey explains this issue very well here: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
However unfortunately you can't simply solve it without changing the way you are loading your views.
Because you are loading them inside RequireJS modules that will keep it in the local var scope, you are losing the reference to the views once the route has been fully processed.
In order to solve this problem, you would need to keep the reference of the current view somewhere, and then properly dispose it before calling another view, something like this:
showView: function(view) {
this.currentView && this.currentView.remove();
this.currentView = view;
this.currentView.render();
$('#content').html(this.currentView.el);
}
More about this solution here: http://tiagorg.com/talk-backbone-tricks-or-treats-html5devconf/#/6
I personally suggest you adopting a solution that will take care of this for you, like Marionette.js
It will handle this and quite many other issues, by providing the missing gaps of every Backbone-based architecture.
I have the following controller in ExtJs:
Ext.define('FileBrowser.controller.BrowserController', {
extend: 'Ext.app.Controller',
views: ['browser.tree_dir', 'browser.grid_file'],
stores: ['store_dir', 'store_file'],
init: function () {
this.control({
'window > tree_dir': {
itemclick: {
fn: function (view, record, item, index, event) {
if (record.isLeaf() == false) {
Ext.getStore('store_file').load({
params: {
dir: record.data.id
}
});
var parentOfCurrentFiles = record.data.id
nodeId = record.data.id;
htmlId = item.id;
var grid_view = this.getView('browser.grid_file');
var grid_view_v = grid_view.getView();
grid_view_v.refresh();
}
}
}
}
});
},
onPanelRendered: function () {
console.log('The panel was rendered');
}
});
If you notice under 'itemclick' I am trying to refresh one of my views, my approach is not working. Can anyone explain to me how I can refresh the view? Thank you.
Replace var grid_view= this.getView('browser.grid_file'); with var grid_view= this.getView('browser.grid_file').create(); to get a real instance (as I already told you, getView() only return the view config, not a instance!) or if you have already created that grid and only one instance exist use the xtype along with a component query to receive it var grid_view=Ext.ComponentQuery('grid_file')[0]
Now to the refresh()
Basically you never need to call this method cause your grid is bound to a store and any change made on this store is directly reflected to your grid.
I would also recommend you to store view instances when creating them instead of using queries or directly use the ref property and let ExtJS do the work for you. The last one will the best solution you I guess... Take a look at ref's within the API examples and give it a try.
So what you are trying to do is, load the store and have the data reflect once you refresh the grid_view...?
In that case, you haven't done a setStore() to the grid, or if you have done that elsewhere, you are't doing a setData() to the store. Also you should call the refresh on the grid.
I need to display three different views which are related to three different model or collections.
In order to perform this task I wrote the following code. (*)
Please tell me if it is the right way to make this, anyway it works.
Here my problem.
In one of this view, let's say the firstView, is possible to perform a DELETE request to the server which take care to delete all the data related to this three view.
Now I need to delete my three view…
but from the firstView I cannot access to the others two views.
1) How can I perform this task?
2) Should I redesign/improve my implementation?
(*)
// module for display three different views
define([
"js/views/01View",
"js/views/02View",
"js/views/03View"
], function (FirstView, SecondView, ThirdView) {
var MainView = Backbone.View.extend({
initialize: function ()
{
this.render();
},
render: function ()
{
var movie_id = this.options.movie_id;
this.firstView = new FirstView(movie_id);
this.secondView = new SecondView(movie_id);
this.thirdView = new ThirdView(movie_id);
}
});
return MainView;
});
P.S.:
The _id is used to build the url parameter of collections or models
url1: http://localhost/movie/movie_id (model1)
url2: http://localhost/movie/movie_id/followers (collection2)
ulrs: http://localhost/movie/movie_id/feeds (collection3)
When I delete the model1 the view2 and view3 related to collection2 and collection3 should be removed.
To fit your problem based on our comments conversation, Backbone architecture revolves using events, so why not use an event aggregator to send events around, don't limit yourself to the backbone constructs. fire an event from one view to another in backbone This pattern provides an elegant solution to your problem.
Views should not respond to direct method calls but to events. Said that you either create a common EventAggregator accesible from every View (as #20100 has explained in his answer) or you connect the Views through a common Model and make each View to listen to its own more interesting events on it.
In your case you can instantiate the Movie model out of the Views instantiations and connect the three Views around it:
// code simplified and not tested
var MainView = Backbone.View.extend({
initialize: function ( opts ) {
this.movie = new Movie({ id: this.opts.movie_id} )
this.movie.fetch();
this.render();
},
render: function () {
this.firstView = new FirstView( this.movie );
this.secondView = new SecondView( this.movie );
this.thirdView = new ThirdView( this.movie );
}
});
var ThirdView = Backbone.View.extend({
initialize: function( opts ) {
this.movie = opts.movie;
this.movie.on( "destroy", this.cleanUp, this )
this.followers = // fetch the followers as you do now, use this.model.id
}
cleanUp: function(){
// your clean up code when model is detroyed
}
});