In a backbone model, is it possible to trigger an event in the initialize function, for a nested view? I based my current code off this example: https://stackoverflow.com/a/8523075/2345124 and have updated it for backbone 1.0.0. Here is my initialize function, for a Model:
var Edit = Backbone.Model.extend({
initialize: function() {
this.trigger('marquee:add');
this.on('change', function(){
this.trigger('marquee:add');
});
}
...
}
I'm trying to call a method renderMarquee when the model is initialized:
var EditRow = Backbone.View.extend({
initialize: function() {
this.listenTo(this.model, "change", this.render); // works
this.listenTo(this.model, "marquee:add", this.renderMarquee); // only called when changed, but not when initially created
...
}
renderMarquee IS called when the model is changed, but not when it is initialized. 'change' events work as expected (this.render is called). Any thoughts?
Thanks!
I am currently facing a similar problem. I needed to trigger the change event in the initialize method of my model.
I looked into the backbone code which revealed why this is not happening:
var Model = Backbone.Model = function(attributes, options) {
...
this.set(attrs, options);
this.changed = {};
this.initialize.apply(this, arguments);
};
the set is executed before the initialize and this.change is emptied setting the model state to "nothing has changed".
In order to overwrite behavior this I added the following code to my initialize method.
initialize: function(attributes, options) {
...
this.changed = attributes;
this.trigger('change');
for (attr_name in attributes) {
this.trigger('change:' + attr_name);
}
},
I trigger all change events manually, this is important for me since inheriting models may bind to change or change:attrxy. But this is not enough, because if I just trigger the events the changedAttributes() method would return false therefore I also set this.changed to the current attributes.
This doesn't make a lot of sense because you are initializing the model somewhere prior to doing the view.listenTo call. Unfortunately, you don't really have a choice in that matter.
You are probably going to want to move the event handling to a Backbone.Collection which already has built in events you can listen on for adding/removing.
Related
I want to perform an action, clearing parent element, after a collection has fetched his models but prior to the models rendering.
I've stumbled upon before and after render methods yet they are model specific, which will cause my parent element to clear before every model rendering.
I'm able of course to perform the action pre-fetching yet I want it to occur when fetch is done and before models are rendered.
I tried using reset and change events listening on the collection yet both resulted unwanted end result.
Reset event seamed to go in that direction yet the passed argument was the entire collection and not a single model from the collection, therefore using the add event callback wasn't possible due to difference in argument type (collection and not a model as required)
Any ideas how to invoke a callback when fetch a collection fetch is successful yet models are yet to be rendered?
The model contains the returned attributes while collection contains url for fetching and parse method to return argument wrapped object.
Below is the code I use to render the collection view, which is basically rendering each model's view within the collection.
Collection View
---------------
var FoosView = Backbone.View.extend({
el: '#plans',
events: {
//'click tr': 'rowClick'
},
initialize: function() {
this.listenTo(this.collection, 'add', this.renderNew);
_.bindAll(this, "render");
this.render();
},
renderNew: function(FooModel) {
var item = new FooView({model: FooModel});
this.$el.prepend(item.render().$el);
}
...
});
The model view
--------
var FooView = Backbone.View.extend({
tagName: 'li',
initialize: function(options) {
this.options = options || {};
this.tpl = _.template(fooTpl);
},
render: function() {
var data = this.model.toJSON();
this.$el.html(this.tpl(data));
return this;
}
});
Thanks in advance.
OK, I think I understand your question and here is a proposed solution. You are now listening to the reset event on your collection and calling this.renderAll. this.renderAll will take the list of models from the collection and render them to the page, but only AFTER the list element has been emptied. Hope this helps :).
var FoosView = Backbone.View.extend({
el: '#plans',
collection: yourCollection, // A reference to the collection.
initialize: function() {
this.listenTo(this.collection, 'add', this.renderNew);
this.listenTo(this.collection, 'reset', this.renderAll);
},
renderAll: function() {
// Empty your list.
this.$el.empty();
var _views = []; // Create a list for you subviews
// Create your subviews with the models in this.collection.
this.collection.each(function(model) {
_views.push(new FooView({model: model});
});
// Now create a document fragment so as to not reflow the page for every subview.
var container = document.createDocumentFragment();
// Append your subviews to the container.
_.each(_views, function(subview) {
container.appendChild(subview.render().el);
});
// Append the container to your list.
this.$el.append(container);
},
// renderNew will only run on the collections 'add' event.
renderNew: function(FooModel) {
var item = new FooView({model: FooModel});
this.$el.prepend(item.render().$el);
}
});
I am forced to assume a few things about you html, but I think the above code should be enough to get you up and running. Let me know if it works.
I'm not totally sure about what you are asking but have you tried:
MyCollection.fetch({
success: function(models,response) {
//do stuff here
}
});
Also you may be interested taking a look at http://backbonejs.org/#Model-parse
Hope it helps!
Edit: there is no direct link between fetching and rendering my bet is that you binded rendering to model change.
USE===============>>>> http://backbonejs.org/#Model-parse
I'm clearly missing the obvious here, but it's been a long day already.
The following code creates an infinite loop in the browser:
M = Backbone.Model.extend({
localStorage: new Backbone.LocalStorage("ModelName"),
initialize: function() {
this.on("change", this.save, this);
}
});
While the following code works fine:
M = Backbone.Model.extend({
localStorage: new Backbone.LocalStorage("ModelName"),
initialize: function() {
this.on("change", this.modelChanged, this);
},
modelChanged: function() {
this.save();
}
});
What's the difference?
(Yes, I'm using local storage for a model rather than a collection, but the model is a singleton that doesn't exist in a collection.)
The change event passes arguments to its handler, and if save is called with arguments, it applies them as new attributes to the model, and causes a change event (which passes attributes to save... which causes a change... etc)
Again some simple problem with backbone. Instead of long description here is some sample code:
var Group = Backbone.Model.extend({
defaults: {
Product: new Products()
},
initialize: function(data){
var _this = this;
var products = new Products(data.Product);
this.set('Product', products);
this.get('Product').each(function(product){
product.on('destroy', function(){
_this.trigger('change');
}, _this);
});
this.bind('change', this.update, this);
},
update: function(){
console.info("something changed");
console.log(this.get('Product').toJSON());
},
});
So group model contains Product-collection, which obviously contains products. In initialize i am trying to make sure that the update method of the group is called example when product is changed and destroyed. All seems to be working nicely, events get called, attributes look great but it fails when I call destroy method in product model. In update I try to print the contents of product collection and what I get is products BEFORE remove is done. If I call this debug line after 500ms timeout, the contents are ok. Product is removed etc.
So according to my understanding the destroy event of product is called and then passed to group before actual removal from collection is done. What am I doing wrong?
Backbone handles the removal of a destroyed model in a collection by listening to the destroy event on the models: see the source code for Backbone.Model - destroy and Backbone.Collection - _onModelEvent.
The order in which the handlers will be executed is not guaranteed, you will have to use something else. For example, listen to the destroyevent on the collection which will fire after the model is actually removed:
initialize: function(data){
var _this = this;
var products = new Products(data.Product);
this.set('Product', products);
this.get('Product').on("destroy", this.update, this);
this.bind('change', this.update, this);
},
Check this Fiddle http://jsfiddle.net/NUtmt/ for a complete example.
According to Backbone.js documentation:
Whenever a UI action causes an attribute of a model to change, the
model triggers a "change" event; all the Views that display the
model's data are notified of the event, causing them to re-render.
So I suppose that render() method should be bound to "change" event by default. However the following code does not work:
TestModel = Backbone.Model.extend({});
TestView = Backbone.View.extend({
render: function() {
alert('render called');
}
});
var mod = new TestModel;
var view = new TestView({model:mod});
mod.change();
It works only if I add explicit bind call:
initialize: function() {
this.model.bind('change', this.render, this);
}
Does this mean that my understanding of default render() callback is not correct and we should always bind render() callback by hand?
Unless something has changed in the last few months, yes, that is the case. This is a good thing, as it gives flexibility as to when views are rendered/re-rendered (for example, some applications might want to render a view only after a model has been persisted on the server, not necessarily when it changes in the browser). If you want your views to always re-render when a model attribute changes, you can extend the default backbone view with your own base view that binds its render method to the model change event, then extend all your concrete views from that. Ex:
MyView = Backbone.View.extend({
initialize: function() {
Backbone.View.prototype.initialize.apply(this, arguments);
this.model.bind('change', this.render);
}
});
MyConcreteView = MyView.extend({...});
var model = new Backbone.Model({...});
var view = new MyConcreteView({model: model});
model.set({prop: 'value'});
You can redefine the Backbone.View constructor to set the render callback by default after creating a new view using the code beneath:
Backbone.View = (function(View) {
// Define the new constructor
Backbone.View = function(options) {
// Call the original constructor
View.apply(this, arguments);
// Add the render callback
if (this.model != null) {
this.model.bind("change", this.render, this);
} else {
// Add some warning or throw exception about
// the render callback not being triggered
}
};
// Clone static properties
_.extend(Backbone.View, View);
// Clone prototype
Backbone.View.prototype = (function(Prototype) {
Prototype.prototype = View.prototype;
return new Prototype;
})(function() {});
// Update constructor in prototype
Backbone.View.prototype.constructor = Backbone.View;
return Backbone.View;
})(Backbone.View);
Now you can create a new view like so:
view = new Backbone.View({model: new Backbone.Model})
I'm having a look at Backbone.js, but I'm stuck. The code until now is as simple as is possible, but I seem not to get it. I use Firebug and this.moments in the render of MomentsView is an object, but all the methods from a collection don't work (ie this.moments.get(1) doesn't work).
The code:
var Moment = Backbone.Model.extend({
});
var Moments = Backbone.Collection.extend({
model: Moment,
url: 'moments',
initialize: function() {
this.fetch();
}
});
var MomentsView = Backbone.View.extend({
el: $('body'),
initialize: function() {
_.bindAll(this, 'render');
this.moments = new Moments();
},
render: function() {
_.each(this.moments, function(moment) {
console.log(moment.get('id'));
});
return this;
}
})
var momentsview = new MomentsView();
momentsview.render();
The (dummy) response from te server:
[{"id":"1","title":"this is the moment","description":"another descr","day":"12"},{"id":"2","title":"this is the mament","description":"onother dascr","day":"14"}]
The object has two models according to the DOM in Firebug, but the methods do not work. Does anybode have an idea how to get the collection to work in the view?
The problem here is that you're fetching the data asynchronously when you initialize the MomentsView view, but you're calling momentsview.render() synchronously, right away. The data you're expecting hasn't come back from the server yet, so you'll run into problems. I believe this will work if you call render in a callback to be executed once fetch() is complete.
Also, I don't think you can call _.each(this.moments) - to iterate over a collection, use this.moments.each().
Try removing the '()' when instantiate the collection.
this.moments = new Moments;
Also, as it's an asynchronous call, bind the collection's 'change' event with the rendering.
I hope it helps you.