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.
Related
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.
Marionette is great but some things can get a bit confusing to follow. On my initial page load I show a Marionette.Layout in a region of a Marionette.Application. But for some reason the click events are delegated but are not actually responding. If I navigate to another route (removing the layout view) and then return to the route (re-rendering the view) then the events are active. If I cause the view to be rendered twice in a row, then it works. Maybe I'm initializing this system wrong.
var ListView = Backbone.Marionette.Layout.extend({
el: "#listView", // exists on DOM already
events: {
// this is what is supposed to work
// as well as events in my subviews
"click" : function() { console.log("clicked");}
});
var Router = Backbone.Router.extend({
initialize: function(options) {
this.app = options.app;
},
start: function(page) {
console.log("start...");
// with silent false this causes list() to be called
// and the view is rendered
// but the ListView does not get its events delegated
Backbone.history.start({
pushState: false,
silent: false
});
// only by running loadUrl here
// do I get the events delegated
// or by switching between other routes
// and coming back to 'list'
// but this is causing a second useless calling
// of list() re-rendering
console.log("loadUrl...");
Backbone.history.loadUrl(page);
// actually the events are not yet delegated at this point
// if I pause a debugger here.
// events only active after the Application finishes its initializers
},
routes: {
'list': 'list'
},
list: function() {
var lv = ListView({});
this.app.focus.show(lv)
}
});
var app = new Backbone.Marionette.Application();
app.addInitializer(function(options) {
this.router = new Router({app: this});
this.router.start();
});
app.addRegions({
// #focus does exist on the DOM
focus: "#focus",
});
app.start({});
An alterative start that I expected would work but sadly doesn't
start: function(page) {
console.log("start...");
Backbone.history.start({
pushState: false,
silent: true // don't trigger the router
});
// call the router here via history
console.log("loadUrl...");
Backbone.history.loadUrl(page);
// causes page to be rendered correctly
// but events are not delegated
},
I've stepped through it all with a debugger but still can't see what the magical effect of loadUrl is that causes it to work.
I've also tried just reinserting the layout into the region:
app.focus.show(app.focus.currentView)
Maybe there's a different way to structure this that would simply not run into this issue.
Not 100% sure this is the answer, but...
If you're elements exist on the DOM already, and you are trying to attach a view to them, and attach that view to a region, using the region's attach method:
list: function() {
var lv = ListView({});
this.app.focus.attach(lv)
}
the attach method was created specifically for the situation where you want to use an existing DOM element. Calling show instead, will replace what's there, which may be causing the jQuery events to be released.
in the onRender section add
this.delegateEvents();
When Marionette closes a view it unbinds events as well. Events are only delegated when the view is initialized. When you show a previously stored view you will need to call the delegateEvents method again.
https://github.com/marionettejs/backbone.marionette/issues/714
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'm pretty new to Backbone so excuse me if this question is a little obvious.
I am having problems with a collection inside of a model. When the collection changes it doesn't register as a change in the model (and doesn't save).
I have set up my model like so:
var Article = Backbone.Model.extend({
defaults: {
"emsID" : $('html').attr('id')
},
initialize: function() {
this.tags = new App.Collections.Tags();
},
url: '/editorial_dev.php/api/1/article/persist.json'
});
This works fine if I update the tags collection and manually save the model:
this.model.tags.add({p : "a"});
this.model.save();
But if the model is not saved the view doesn't notice the change. Can anyone see what I am doing wrong?
initialize: function() {
this.tags = new App.Collections.Tags();
var model = this;
this.tags.bind("change", function() {
model.save();
});
},
Bind to the change event on the inner collection and just manually call .save on your outer model.
This is actually an addendum to #Raynos answer, but it's long enough that I need answer-formatting instead of comment-formatting.
Clearly OP wants to bind to change and add here, but other people may want to bind to destroy as well. There may be other events (I'm not 100% familiar with all of them yet), so binding to all would cover all your bases.
remove also works instead of destroy. Note that both remove and destroy fire when a model is deleted--first destroy, then remove. This propagates up to the collection and reverses order: remove first, then destroy. E.g.
model event : destroy
model event : remove
collection event : destroy
collection event : remove
There's a gotcha with custom event handlers described in this blog post.
Summary: Model-level events should propagate up to their collection, but can be prevented if something handles the event first and calls event.stopPropagation. If the event handler returns false, this is a jQuery shortcut for event.stopPropagation();
event.preventDefault();
Calling this.bind in a model or collection refers to Underscore.js's bind, NOT jQuery/Zepto's. What's the difference? The biggest one I noticed is that you cannot specify multiple events in a single string with space-separation. E.g.
this.bind('event1 event2 ...', ...)
This code looks for the event called event1 event2 .... In jQuery, this would bind the callback to event1, event2, ... If you want to bind a function to multiple events, bind it to all or call bind once for each event. There is an issue filed on github for this, so this one will hopefully change. For now (11/17/2011), be wary of this.
define events in Backbone.Model
var Todo = Backbone.Model.extend({
initialize: function(){
this.on('change', function(){
console.log('- Values for this model have changed.');
});
}
})
define events in Backbone.View
var TodoView = Backbone.View.extend({
className: "document-row",
events: {
"click .icon": "open",
"click .button.delete": "destroy"
}
})
My Question
define events syntax are quite different between model/collection and view, why are they designed in that way?
I think it's better to define model event like this. But backbone don't support it.
var Todo = Backbone.Model.extend({
events: {
"change": "foo"
},
foo: function(){
console.log("test")
}
});
There are two separate type of events: Backbone.Events and jQuery DOM events - so making these look the same seems like a bad idea as it would make code confusing not to mention it wouldn't actually work because the View UI events need different info: '<eventName> <optional DOM selector>: <methodName>' whereas normal internal events have a different syntax.
Backbone.Events follows the typical "publish/subscribe" pattern - it's just a way for apps to internally say "Something has happened" via .trigger and "I want to know when something happens" via .on or .once and the equivalent which you would use in a View because it handles cleaning up the listen when the view is removed: .listenTo and .listenToOnce.
So in your example the "change" event in the model is an internal event which Backbone fires when an attribute changes. The "change" in the View is jQuery DOM event (actually a delegate event) and you could optionally listen to something deeper in the view "change .myinput" so they are not equivalent.
The other difference is that .trigger can pass any arguments it likes after the first one (the event name), whereas the View event passes the DOM event object which you don't control e.g. onChange: function(ev) { ev.preventDefault(); }.