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})
Related
I have created a collection passing a collection view and a collection. The collection references a model I have created. when fetching the collection the items get rendered succesfully, but when the models change, the itemViews are not being re-rendered as expected.
for example, the itemAdded function in the tweetCollectionView is called twice ( two models are added on fetch ) but even though the parse function returns different properties over time for those models ( I assume this would call either a change event on the collection, or especially a change event on the model, which I have tried to catch in the ItemView ) the itemChanged is never called, and the itemViews are never re-rendered, which i would expect to be done on catching the itemViews model change events.
The code is as follows below:
function TweetModule(){
String.prototype.parseHashtag = function() {
return this.replace(/[#]+[A-Za-z0-9-_]+/g, function(t) {
var tag = t;
return "<span class='hashtag-highlight'>"+tag+"</span>";
});
};
String.prototype.removeLinks = function() {
var urlexp = new RegExp( '(http|ftp|https)://[\w-]+(\.[\w-]+)+([\w.,#?^=%&:/~+#-]*[\w#?^=%&/~+#-])?' );
return this.replace( urlexp, function(u) {
var url = u;
return "";
});
};
var TweetModel = Backbone.Model.extend({
idAttribute: 'id',
parse: function( model ){
var tweet = {},
info = model.data;
tweet.id = info.status.id;
tweet.text = info.status.text.parseHashtag().removeLinks();
tweet.name = info.name;
tweet.image = info.image_url;
tweet.update_time_full = info.status.created_at;
tweet.update_time = moment( tweet.update_time_full ).fromNow();
return tweet;
}
});
var TweetCollection = Backbone.Collection.extend({
model: TweetModel,
url: function () {
return '/tweets/game/1'
}
});
var TweetView = Backbone.Marionette.ItemView.extend({
template: _.template( require('./templates/tweet-view.html') ),
modelEvents:{
"change":"tweetChanged"
},
tweetChanged: function(){
this.render();
}
})
var TweetCollectionView = Marionette.CompositeView.extend({
template: _.template(require('./templates/module-twitter-feed-view.html')),
itemView: TweetView,
itemViewContainer: '#tweet-feed',
collection: new TweetCollection([], {}),
collectionEvents: {
"add": "itemAdded",
"change": "itemChanged"
},
itemAdded: function(){
console.log('Item Added');
},
itemChanged: function(){
console.log("Changed Item!");
}
});
this.startInterval = function(){
this.fetchCollection();
this.interval = setInterval( this.fetchCollection, 5000 );
}.bind(this);
this.fetchCollection = function(){
this.view.collection.fetch();
this.view.render();
}.bind(this);
//build module here
this.view = new TweetCollectionView();
this.startInterval();
};
I may be making assumptions as to Marionette handles event bubbling, but according to the docs, I have not seen anything that would point to this.
Inside your CollectionView, do
this.collection.trigger ('reset')
after model have been added.
This will trigger onRender () method in ItemView to re-render.
I know I'm answering an old question but since it has a decent number of views I thought I'd answer it correctly. The other answer doesn't address the problem with the code and its solution (triggering a reset) will force all the children to re-render which is neither required nor desired.
The problem with OP's code is that change is not a collection event which is why the itemChanged method is never called. The correct event to listen for is update, which according to the Backbone.js catalog of events is a
...single event triggered after any number of models have been added
or removed from a collection.
The question doesn't state the version of Marionette being used but going back to at least version 2.0.0 CollectionView will intelligently re-render on collection add, remove, and reset events. From CollectionView: Automatic Rendering
When the collection for the view is "reset", the view will call render
on itself and re-render the entire collection.
When a model is added to the collection, the collection view will
render that one model in to the collection of child views.
When a model is removed from a collection (or destroyed / deleted),
the collection view will destroy and remove that model's child view
The behavior is the same in v3.
One example in codeschool's backbone.js tutorial has the following solution:
Application.js
var appointment = new Appointment({id: 1});
appointment.on('change',function() {
alert('its changed');
});
I realize this is probably a simplified example but in most cases wouldn't you want this defined on the model definition so it applies to all model instances?
Something in the model definition that says whenever an instance of me changes fire this method in the view? That view method could then fire the alert.
I'm obviously just learning so any help is appreciated!
Here the event is attached to that particular model instance. So the same will not trigger an event for any other instance..
var appointment = new Appointment({id: 1}); <--- Event is triggered
var appointment1 = new Appointment({id: 2}); <--- Event is not triggered
appointment.on('change',function() {
console.log('its changed');
});
Since the event is attached directly on the instance of the model. But if you do the same when defining the Model, it would trigger the same on all the instances of the Model.
var Appointment = Backbone.Model.extend({
initialize: function() {
this.on('change', function() {
console.log('its changed')
});
}
});
Now any change on the instance of the model will trigger an event.
var appointment = new Appointment({id: 1}); <--- Event is triggered
var appointment1 = new Appointment({id: 2}); <--- Event is triggered
If you talking about the same on a View, then the model that is passed to the instance will generally keep listening to the event. And if there is any change then a method will be invoked changing the state of the view.
var View = Backbone.View.extend({
initialize: function() {
// Listening to the event on the model which when
// triggered will render the view again
this.listenTo(this.model, 'change', this.render);
},
render: function() {
// do something
}
});
var view = new View();
view.render();
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.
I have a two views:
1 LeftView (maximized when RightView is minimized & vice versa)
2 RightView (containing)
- collection of
- RightItemView (rendering RightItemModel)
When RightView is maximized and the user clicks a RightItemView, I want to maximize LeftView and display something according to the data from the clicked RightItemView.
What's the proper way to wire them?
I would recommend using the Backbone.Events module:
http://backbonejs.org/#Events
Basically, this line is all it takes to create your event dispatcher:
var dispatcher = _.clone(Backbone.Events);
Then all of your views can trigger/listen for events using the global dispatcher.
So, in RightItemView you would do something like this in the click event:
dispatcher.trigger('rightItemClick', data); // data is whatever you need the LeftView to know
Then, in LeftView's initialize function, you can listen for the event and call your relevant function:
dispatcher.on('rightItemClick', this.maximizeAndDisplayData);
Assuming your LeftView would have a function like so:
maximizeAndDisplayData: function(data) {
// do whatever you need to here
// data is what you passed with the event
}
The solution #jordanj77 mentioned is definitely one of the correct ways to achieve your requirement. Just out of curiosity, I thought of another way to achieve the same effect. Instead of using a separate EventDispatcher to communicate between the two views, why shouldn't we use the underlying model as our EventDispatcher? Let's try to think in those lines.
To start with, add a new boolean attribute to the RightItem model called current and default it to false. Whenever, the user selects the RightItemView, set the model's current attribute to true. This will trigger a change:current event on the model.
var RightItem = Backbone.Model.extend({
defaults: {
current: false,
}
});
var RightItemView = Backbone.View.extend({
events: {
'click li': 'changeCurrent'
}
changeCurrent: function() {
this.model.set('current', true);
}
});
On the other side, the LeftView will be handed a Backbone.Collection of RightItem models during creation time. You would anyways have this instance to supply the RightView isn't it? In its initialize method, the LeftView will listen for change:current event. When the event occurs, LeftView will change the current attribute of the model it is currently displaying to false and start displaying the new model that triggered this event.
var LeftView = Backbone.View.extend({
initialize: function() {
this.collection.on('change:current', this.render, this);
},
render: function(model) {
// Avoid events triggered when resetting model to false
if(model.get('current') === true) {
// Reset the currently displayed model
if (this.model) {
this.model.set('current') = false;
}
// Set the currently selected model to the view
this.model = model;
// Display the view for the current model
}
}
});
var leftView = new LeftView({
// Use the collection that you may have given the RightView anyways
collection: rightItemCollection
});
This way, we get to use the underlying model as the means of communication between the Left and Right Views instead of using an EventDispatcher to broker for us.
The solution given by #Ganeshji inspired me to make a live example
I've created 2 views for this.
var RightView = Backbone.View.extend({
el: $('.right_view'),
template: _.template('<p>Right View</p>'),
renderTemplate: function () {
this.$el.html('');
this.$el.append(this.template());
this.$link = this.$el.append('Item to view').children('#left_view_max');
},
events: {
'click #left_view_max' : 'maxLeftView'
},
maxLeftView: function () {
//triggering the event for the leftView
lView.trigger('displayDataInLeftView', this.$link.attr('title'));
},
initialize: function (options) {
this.renderTemplate();
}
});
var LeftView = Backbone.View.extend({
el: $('.left_view'),
template: _.template('<p>Left View</p>'),
renderTemplate: function () {
this.$el.html('');
this.$el.append(this.template());
},
displayDataInLeftView: function (data) {
this.$el.append('<p>' + data + '</p>');
},
initialize: function (options) {
//set the trigger callback
this.on('displayDataInLeftView', this.displayDataInLeftView, this);
this.renderTemplate();
}
});
var lView = new LeftView();
var rView = new RightView();
Hope this helps.
So I have a View that looks like this.
//base class
var SelectListView = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'addOne', 'addAll');
this.collection.bind('reset', this.addAll);
},
addAll: function() {
this.collection.each(this.addOne);
},
events: {
"change": "changedSelected"
},
changedSelected: function() {
this.selected = $(this.el);
this.setSelectedId($(this.el).val());
}
});
//my extended view
var PricingSelectListView = SelectListView.extend({
addOne: function(item) {
$(this.el).append(new PricingView({ model: item }).render().el);
}
});
I have instantiated the view like this...
var products = new ProductPricings();
var pricingsView = new PricingSelectListView({
el: $("#sel-product"),
collection: products
});
Somewhere else (another views custom method)I have updated the pricing view's collection
pricingsView.collection = new ProductPricings(filtered);
This does not seen to do anything.
pricingsView.render();
So now the collection has fewer items but the new view is never rendered or refreshed in the DOM.
How to I do I 1.) refresh the rendering in the DOM? 2.) Make it automatically refresh the DOM? Do I have to somehow tell it to render when ever the collection changes?
You bound addOne() to a reset event. When you just replace the pricingsView.collection instance then that event is not triggered and addOne() is not executed.
Try instead:
pricingsView.collection.reset(filtered);
This might work since you bind to collection's reset event already:
pricingsView.collection.reset(filtered);
http://backbonejs.org/#Collection-reset
You still have tweak your rendering logic to remove old markup from the view when reset happens.