When you create a Backbone model and attach a change event, it will listen for it. And event if you change them at the same time (in the same set call) it will fire the change event the number of changes you have in your model.
So imagine I have a view which is like a grid with filters and it has to refresh every time I change a filter.
My default model looks like this:
defaults: {
filters: {
from: '2016-01-01',
to: today.format(format),
limit: 25,
page: 1,
cycle: 'all'
}
},
I have a listener for the model in the view:
this.listenTo(this.model, 'change:filters', this.onShow);
And every time I change a filter I do:
this.model.set('filters', {
from: '2016-07-01',
to: '2016-07-31',
limit: 25,
page: 1,
cycle: 'currentMonth'
});
As you see I've changed 3 properties of this model. What happen is that it will fire the change event 3 times.
What I want to achieve is that it only change once, even if I have 3 changes on the model.
Is that doable?
Thanks in advance
From Backbone documentation:
"change:[attribute]" (model, value, options) — when a specific attribute has been updated.
In Your case model has one attribute: filters. If You set new value to this attribute (no matter it is object, array, int, float, string) event will be fired. Once and only once.
If You need to observe a modification of attribute values, You must write Your own handler.
this.listenTo(this.model, 'change:filters', function(model, value) {
prev = model.previous('filters');
if(prev.from != value.from) model.trigger('change:filters:from', model, value.from);
if(prev.to != value.to) model.trigger('change:filters:to', model, value.to);
if(prev.limit != value.limit) model.trigger('change:filters:limit', model, value.limit);
if(prev.page != value.page) model.trigger('change:filters:page', model, value.page);
if(prev.cycle != value.cycle) model.trigger('change:filters:cycle', model, value.cycle);
});
and in code:
this.listenTo(this.model, 'change:filters:from', this.onShow);
this.listenTo(this.model, 'change:filters:to', this.onShow);
this.listenTo(this.model, 'change:filters:limit', this.onShow);
this.listenTo(this.model, 'change:filters:page', this.onShow);
this.listenTo(this.model, 'change:filters:cycle', this.onShow);
I've run into this situation mostly when using Marionette's Collection or CompositeViews and listening to changes on the underlying collection. Often times, more than one child model would be updated and the CompositeView would need to be re-rendered or re-ordered, but I didn't want to re-render/re-order for every child model change -- just once when one or more changes occur at the same time. I handled this with a setTimeout like below:
var MyView = Backbone.Marionette.CompositeView.extend({
childView: ChildView,
initialize: function() {
this.listenTo(this.collection, 'change', this.onChildModelChanged);
this.childModelChanged = false;
},
onChildModelChanged: function() {
// use a setTimeout 0 so that we batch 'changes' and re-render once
if(this.childModelChanged === false) {
this.childModelChanged = true;
setTimeout(function() {
this.render();
this.childModelChanged = false;
}.bind(this), 0);
}
}
});
This allows all child model change events to fire before re-rendering or re-ordering. Hope this helps.
Think this would be the kind of a hack for the scenario in question. You can define a custom event on the model.
Listen to a custom event on the model change-filters
Pass option silent : true when you are setting the model.
Trigger the custom event on the model if there are any changed attributes.
Related
I have a case where I am creating multiple views for a single model. When I close a view, I am removing the model from the collection.
But in case of multiple views, there are other views that depend on the model.
So, how can I know when there are no views depending on the model? When should I destroy the model in case of multiple views?
Generally speaking the lifecycle of a view shouldn't cause a model/collection to be modified, but assuming you have a good reason for it.
Here is a slight improvement on Kenny's answer, I'd suggest to have dependentViews as an array on the object, not a value in the attributes (if Views aren't persisted, best not persist view dependencies).
var myModel = Backbone.Model.extend({
initialize: function() {
this.dependentViews = [];
},
addDependentView: function(view) {
this.dependentView.push(view);
},
closeDependentView: function(view) {
this.dependentViews = _.without(this.dependentViews, view);
if (_.isEmpty(this.dependentViews)) {
//remove model from collection
}
}
})
var view1 = Backbone.View.extend({
initialize: function() {
this.model.addDependentView(this);
}
})
var view2 = Backbone.View.extend({
initialize: function() {
this.model.addDependentView(this);
}
})
...
onCloseView: function() {
this.model.closeDependentView(this);
}
Also an array of objects might come in handy for the dependency list, that way you could if necessary in future, make calls from the model to the dependent views.
Another possible solution might be to use the internal event listener register as a means of tracking any objects listening to the model. But that would be more involved and would depend on internal functionality within Backbone.
Though this is not a proper way but still you can achieve this following
Declare dependentViews in your model's defaults
var myModel = Backbone.Model.extend({
defaults: function() {
return {
dependentViews: 0
};
}
});
In initialization of each view, increment the dependentView
var view1 = Backbone.View.extend({
initialize: function() {
this.model.set("dependentViews",
this.model.get("dependentViews") + 1);
}
});
var view2 = Backbone.View.extend({
initialize: function() {
this.model.set("dependentViews",
this.model.get("dependentViews") + 1);
}
});
On close of view decrement dependentViews and on each view destroy, just check the value of dependentViews. If it is 0, remove the model from the collection.
onCloseView: function() {
this.model.set("dependentViews",
this.model.get("dependentViews") - 1);
if (this.model.get("dependentViews") === 0) {
//remove model from collection
}
}
Having the number of views inside the data model as suggested in other answers is not a good idea.
I have a case where I am creating multiple views for a single model
So you have a "place" where you create views, and destroy these views. This could be a parent view, controller, router instance etc.
If you don't have one then you need one.
You must be having references to these view instances for future clean up.
If you don't have it then you need it.
When you're destroying a view just check the remaining number of instance, if there are none then remove the model (If the view removes itself in response to some user action then it needs to notify the parent of removal).
This is something that should take place outside the view instances and model.
I create my view with a new collection.
I fire add and sync events :
this.mapDetailsCOllection.on('add', self.onAddElement, self);
this.mapDetailsCOllection.on('sync', self.onSync, self);
Before fetch I do :
this.mapDetailsCOllection.off("add");
this.mapDetailsCOllection.fetch();
And when fetch is ok, in my sync callback :
this.mapDetailsCOllection.on('add', self.onAddElement, self);
But even if I put off the add event I everytime go in add event callback function when I fetch.
I'm going to suggest the following because I do not have any context on how you've architected your application (Backbone.js is great because it gives you a lot of rope, but how you architect your application can greatly change how you need to implement sync solutions). I'm more than happy to adjust, expand, or clarify this post if you can share a little more about how you've architected your App, View, Collection, and Model code for me to understand what you are trying to achieve.
In your code, I would listen to the update event instead of the add event (since add triggers on each new item added to the collection, compared to update which triggers after any number of items have been added/removed from a collection). Then I would remove the on/off change for add event and instead have your view listen to Collection.reset event rather than turning off and on your listeners for your fetch/sync cycle.
I've used this type of View design-pattern successfully in the past:
var _ = require('lodash');
var DocumentRow = Backbone.View.extend({
events: {
"click #someEl": "open"
},
clean: function() {
// cleans up event bindings to prevent memory leaks
},
initialize: function(options) {
_.bindAll(this, [
'initialize',
'open',
'render',
'clean'
]);
options = options || {};
this.collection = options.collection ? options.collection : SomeCollection;
this.listenTo(this.collection, "reset", this.render);
},
open: function(item) {
...
},
render: function() {
...
}
});
Based on this:
The behavior of fetch can be customized by using the available set
options. For example, to fetch a collection, getting an "add" event
for every new model, and a "change" event for every changed existing
model, without removing anything:
collection.fetch({remove: false})
collection.set(models, [options]) and this:
All of the appropriate "add", "remove", and "change" events are fired
as this happens. Returns the touched models in the collection. If
you'd like to customize the behavior, you can disable it with options:
{add: false}, {remove: false}, or {merge: false}.
you can pass
collection.fetch({add: false})
to avoid firing add event.
Let's say I have an application with two views that are connected - some sort of a form element in the main content area, and a "sidebar" which stores a summary of the form in question.
I'd like to create a simple "flash" animation on both the text input itself and the view that represents it in the summary. These two views are in very distant parts of the DOM tree, so it is not practical to register them as sub-views of their parents and bubble the event up that way. So, the form field (a simple input type="text") has this model
//The model that manages these two views
var InputModel = Backbone.Model.extend({
defaults: {
flash: false
}
})
//The view of the input field in the actual form - its this.model is an instance of InputModel
var InputFormView = Backbone.Model.extend({
events: {
'mousedown': 'clicked'
}
initialize: function() {
this.render();
this.model.on('change:flash', this.flash, this);
},
render: function() {
return this; //The 'el' DOM element is passed to this view on creation
},
clicked: function() {
this.model.set('flash', true);
},
flash: function(model) {
if (model.changed.flash && model.changed.flash === true){
//Do the 'flashing' animation using jQuery animate
} else {
this.model.set('flash', false);
}
}
});
//The summary column view - its this.model is an instance of InputModel
var InputSummaryView = Backbone.Model.extend({
initialize: function() {
this.render();
this.model.on('change:flash', this.flash, this);
},
render: function() {
return this; //The 'el' DOM element is passed to this view on creation
},
flash: function(model) {
if (model.changed.flash && model.changed.flash === true){
//Do the 'flashing' animation using jQuery animate
} else {
this.model.set('flash', false);
}
}
});
This defintely works in practice. The problem I see is two-fold:
I now have attributes in my model that are not useful data about the input. As in, whether 'flash' is true or not is not semantically useful information - it is strictly related to the "presentation" of the data, not the content of the data itself, and thus should be relegated to the views.
The flash field will either clog up my DB with useful information, or I'll need to filter it out every time I save().
Is there any way to avoid this problem? To have the two very distant views engage in event- based communication without using (or at least mucking up) their shared model?
You're definitely correct that you don't want to pollute the model with that data. There's actually a much more straightforward option: use an event manager. Create an object whose sole job is to trigger and respond to events globally in your app. Happily, Backbone.Events can do this for you, as the docs suggest:
var dispatcher = _.clone(Backbone.Events);
Then, you can just do dispatcher.trigger('flash') and dispatcher.on('flash') in your views. Although, I would rename the event to something more meaningful about what has actually happened.
in backbone we have an app that uses an event Aggregator, located on the window.App.Events
now, in many views, we bind to that aggregator, and i manually wrote a destroy function on a view, which handles unbinding from that event aggregator and then removing the view. (instead of directly removing the view).
now, there were certain models where we needed this functionality as well, but i can't figure out how to tackle it.
certain models need to bind to certain events, but maybe i'm mistaken but if we delete a model from a collection it stays in memory due to these bindings to the event aggregator which are still in place.
there isn't really a remove function on a model, like a view has.
so how would i tacke this?
EDIT
on request, some code example.
App = {
Events: _.extend({}, Backbone.Events)
};
var User = Backbone.Model.extend({
initialize: function(){
_.bindAll(this, 'hide');
App.Events.bind('burglar-enters-the-building', this.hide);
},
hide: function(burglarName){
this.set({'isHidden': true});
console.warn("%s is hiding... because %s entered the house", this.get('name'), burglarName);
}
});
var Users = Backbone.Collection.extend({
model: User
});
var House = Backbone.Model.extend({
initialize: function(){
this.set({'inhabitants': new Users()});
},
evacuate: function(){
this.get('inhabitants').reset();
}
});
$(function(){
var myHouse = new House({});
myHouse.get('inhabitants').reset([{id: 1, name: 'John'}, {id: 1, name: 'Jane'}]);
console.log('currently living in the house: ', myHouse.get('inhabitants').toJSON());
App.Events.trigger('burglar-enters-the-building', 'burglar1');
myHouse.evacuate();
console.log('currently living in the house: ', myHouse.get('inhabitants').toJSON());
App.Events.trigger('burglar-enters-the-building', 'burglar2');
});
view this code in action on jsFiddle (output in the console): http://jsfiddle.net/saelfaer/szvFY/1/
as you can see, i don't bind to the events on the model, but to an event aggregator.
unbinding events from the model itself, is not necessary because if it's removed nobody will ever trigger an event on it again. but the eventAggregator is always in place, for the ease of passing events through the entire app.
the code example shows, that even when they are removed from the collection, they don't live in the house anymore, but still execute the hide command when a burglar enters the house.
I see that even when the binding event direction is this way Object1 -> listening -> Object2 it has to be removed in order to Object1 lost any alive reference.
And seeing that listening to the Model remove event is not a solution due it is not called in a Collection.reset() call then we have two solutions:
1. Overwrite normal Collection cleanUp
As #dira sais here you can overwrite Collection._removeReference to make a more proper cleaning of the method.
I don't like this solutions for two reasons:
I don't like to overwrite a method that has to call super after it.
I don't like to overwrite private methods
2. Over-wrapping your Collection.reset() calls
Wich is the opposite: instead of adding deeper functionality, add upper functionality.
Then instead of calling Collection.reset() directly you can call an implementation that cleanUp the models before been silently removed:
cleanUp: function( data ){
this.each( function( model ) { model.unlink(); } );
this.reset( data );
}
A sorter version of your code can looks like this:
AppEvents = {};
_.extend(AppEvents, Backbone.Events)
var User = Backbone.Model.extend({
initialize: function(){
AppEvents.on('my_event', this.listen, this);
},
listen: function(){
console.log("%s still listening...", this.get('name'));
},
unlink: function(){
AppEvents.off( null, null, this );
}
});
var Users = Backbone.Collection.extend({
model: User,
cleanUp: function( data ){
this.each( function( model ) { model.unlink(); } );
this.reset( data );
}
});
// testing
var users = new Users([{name: 'John'}]);
console.log('users.size: ', users.size()); // 1
AppEvents.trigger('my_event'); // John still listening...
users.cleanUp();
console.log('users.size: ', users.size()); // 0
AppEvents.trigger('my_event'); // (nothing)
Check the jsFiddle.
Update: Verification that the Model is removed after remove the binding-event link
First thing we verify that Object1 listening to an event in Object2 creates a link in the direction Obect2 -> Object1:
In the above image we see as the Model (#314019) is not only retained by the users collection but also for the AppEvents object which is observing. Looks like the event linking for a programmer perspective is Object that listen -> to -> Object that is listened but in fact is completely the opposite: Object that is listened -> to -> Object that is listening.
Now if we use the Collection.reset() to empty the Collection we see as the users link has been removed but the AppEvents link remains:
The users link has disappear and also the link OurModel.collection what I think is part of the Collection._removeReference() job.
When we use our Collection.cleanUp() method the object disappear from the memory, I can't make the Chrome.profile tool to explicitly telling me the object #314019 has been removed but I can see that it is not anymore among the memory objects.
I think the clean references process is a tricky part of Backbone.
When you remove a Model from a Collection the Collection takes care to unbind any event on the Model that the Collection its self is binding. Check this private Collection method.
Maybe you can use such a same technique in your Aggregator:
// ... Aggregator code
the_model.on( "remove", this.unlinkModel, this );
// ... more Aggregator code
unlinkModel: function( model ){
model.off( null, null, this );
}
This is in the case the direction of the binding is Aggregator -> Model. If the direction is the opposite I don't think you have to make any cleaning after Model removed.
Instead of wrapping Collection's reset with cleanUp as fguillen suggested, I prefer extending Collection and overriding reset directly. The reason is that
cleanUp takes effect only in client's code, but not in library(i.e. Backbone)'s.
For example, Collection.fetch may internally call Collection.reset. Unless modifying the Backbone's source code, we cannot unbind models from events(as in cleanUp) after calling Collection.fetch.
Basically, my suggested snippet is as follows:
var MyCollection = Backbone.Collection.extend({
reset: function(models, options) {
this.each(function(model) {
model.unlink(); // same as fguillen's code
});
Backbone.Collection.prototype.reset.apply(this, arguments);
}
});
Later, we can create new collections based on MyCollection.
I have a collection of models. When a model changes it triggers a change event on the collection. I watch for the collection change event and then I update the UI.
How should I go about updating the UI? Don't I need to know what models are new, so I can append, and what already exist, so I can update?
One of the reason I feel I need this granularity is because there's an animation transition, so I need to relate every model to it's previous state. Does backbone help with this or should I just build this on my own?
to know which models are new, listen to the collection's "add" event. then you can render the individual item.
MyView = Backbone.View.extend({
initialize: function(){
_.bindAll(this, "renderItem");
this.collection.bind("add", this.renderItem);
},
renderItem: function(item){
// render the new item here
},
render: function(){
this.collection.each(this.renderItem);
}
});
in this example, rendering the collection works the same as rendering an individual item - it calls the same renderItem method that the "add" event calls.
to handle the scenario where you have a sorted list... you'll have to do some logic in your renderItem method to figure out the location of the item. something like this maybe (untested code... just an idea):
MyView = Backbone.View.extend({
initialize: function(){
_.bindAll(this, "renderItem");
this.collection.bind("add", this.renderItem);
},
renderItem: function(item){
var itemView = new ItemView({model: item});
itemView.render();
return itemView;
},
render: function(){
this.collection.each(function(item){
var itemView = renderItem(item);
var itemIndex = item.get("index");
var previousItem = this.$(".myList")[itemIndex];
$(itemView.el).insertAfter($(previousItem));
});
}
});
this code assumes you have an index attribute on your models to know the order that the models are supposed to be in.
also note that there's a high likelihood that this code won't execute as-is, since i haven't tested it out. but it should give you an idea of the code you'll want to write.
You don't need to go through the collection if the model changes. Simple render a sub view for each model and in which you can handle the change event.
Imagine you have the following structure:
Collection: Tasks
Model: Task
And two views:
Tasks view (main view)
Task view (sub view)
In the #render method of the Tasks view you render e.g. a <ul> element which will hold your tasks (sub views). Then you iterate through all your collection's models and create a Task view for each, passing the model and appending it's el to your main views <ul>.
Within your Task view you bind the #render method to the models change event and each Task view will take care of itself. — Am I making sense?