backbone .bind('change', _.bind(this.render, this)) not registering - backbone.js

I have a measure model, that is made up of two collections, a beats collections, and a measureRep[resentation] collection. Each collection is made of beat models and representation models respectively.
Whenever a measureRep[resentation] collection changes (by addition or subtraction of a representation model), I want the measureView (which has the measure model, and therefore the measureRep[resentation] collection) to re-render itself using the render function().
I am adding a new model in another View by the following function:
var representationModel = new RepresentationModel({representationType: newRepType});
StageCollection.get(cid).get('measures').models[0].get('measureRepresentations').add(representationModel);
I can see that before and after the addition that the measure and its measureRep collection are getting added correctly, however, the bind call on the measureView is not registering the change and calling the render function. I even put the bind on the model, to show that the backend is getting updated, however, it doesn't respond. This leads me to believe that the View and the models are decoupled, but that doesn't make sense, since it originally renders from the model. Here are the relevant files:
measureView.js [View]:
define([...], function(...){
return Backbone.View.extend({
initialize: function(options){
if (options) {
for (var key in options) {
this[key] = options[key];
}
this.el = '#measure-container-'+options.parent.cid;
}
window.css = this.collectionOfRepresentations; //I use these to attach it to the window to verify that the are getting updated correctly
window.csd = this.model; // Same
_.bindAll(this, 'render'); // I have optionally included this and it hasn't helped
this.collectionOfRepresentations.bind('change', _.bind(this.render, this));
this.render();
},
render: function(){
// Make a template for the measure and append the MeasureTemplate
var measureTemplateParameters = { ... };
var compiledMeasureTemplate = _.template( MeasureTemplate, measureTemplateParameters );
// If we are adding a rep, clear the current reps, then add the template
$(this.el).html('');
$(this.el).append( compiledMeasureTemplate )
// for each rep in the measuresCollection
_.each(this.collectionOfRepresentations.models, function(rep, repIndex) {
var measureRepViewParamaters = { ... };
new MeasureRepView(measureRepViewParamaters);
}, this);
return this;
},
...
});
});
measure.js [Model]:
define([ ... ], function(_, Backbone, BeatsCollection, RepresentationsCollection) {
var MeasureModel = Backbone.Model.extend({
defaults: {
beats: BeatsCollection,
measureRepresentations: RepresentationsCollection
},
initialize: function(){
var logg = function() { console.log('changed'); };
this.measureRepresentations.bind('change', logg);
this.bind('change', logg);
}
});
return MeasureModel;
});
representations.js [Collection]:
define([ ... ], function($, _, Backbone, RepresentationModel){
var RepresentationsCollection = Backbone.Collection.extend({
model: RepresentationModel,
initialize: function(){
}
});
return RepresentationsCollection;
});
I have also tried registering the bind on the measure model, and not its child collection, but neither work.
_.bindAll(this, 'render');
this.model.bind('change', _.bind(this.render, this));

see: https://stackoverflow.com/a/8175141/1449799
In order to detect additions of models to a collection, you need to listen for the add event (not the change event, which will fire when a model in the collection is changed http://documentcloud.github.io/backbone/#Events-catalog ).
so try:
this.measureRepresentations.bind('add', logg);

Related

A proper example of backbone views: Change attributes, CRUD, without Zombie Views

Trying to make a reasonable teaching model of Backbone that shows proper ways to take advantage of backbone's features, with a grandparent, parent, and child views, models and collections...
I am trying to change a boolean attribute on a model, that can be instantiated across multiple parent views. How do I adjust the listers to accomplish this?
The current problem is that when you click on any non-last child view, it moves that child to the end AND re-instantiates it.
Plnkr
Click 'Add a representation'
Click 'Add a beat' (you can click this more than once)
Clicking any beat view other than the last one instantiates more views of the same beat
Child :
// our beat, which contains everything Backbone relating to the 'beat'
define("beat", ["jquery", "underscore", "backbone"], function($, _, Backbone) {
var beat = {};
//The model for our beat
beat.Model = Backbone.Model.extend({
defaults: {
selected: true
},
initialize: function(boolean){
if(boolean) {
this.selected = boolean;
}
}
});
//The collection of beats for our measure
beat.Collection = Backbone.Collection.extend({
model: beat.Model,
initialize: function(){
this.add([{selected: true}])
}
});
//A view for our representation
beat.View = Backbone.View.extend({
events: {
'click .beat' : 'toggleBeatModel'
},
initialize: function(options) {
if(options.model){
this.model=options.model;
this.container = options.container;
this.idAttr = options.idAttr;
}
this.model.on('change', this.render, this);
this.render();
},
render: function(){
// set the id on the empty div that currently exists
this.$el.attr('id', this.idAttr);
//This compiles the template
this.template = _.template($('#beat-template').html());
this.$el.html(this.template());
//This appends it to the DOM
$('#'+this.container).append(this.el);
return this;
},
toggleBeatModel: function() {
this.model.set('selected', !this.model.get('selected'));
this.trigger('beat:toggle');
}
});
return beat;
});
Parent :
// our representation, which contains everything Backbone relating to the 'representation'
define("representation", ["jquery", "underscore", "backbone", "beat"], function($, _, Backbone, Beat) {
var representation = {};
//The model for our representation
representation.Model = Backbone.Model.extend({
initialize: function(options) {
this.idAttr = options.idAttr;
this.type = options.type;
this.beatsCollection = options.beatsCollection;
//Not sure why we have to directly access the numOfBeats by .attributes, but w/e
}
});
//The collection for our representations
representation.Collection = Backbone.Collection.extend({
model: representation.Model,
initialize: function(){
}
});
//A view for our representation
representation.View = Backbone.View.extend({
events: {
'click .remove-representation' : 'removeRepresentation',
'click .toggle-representation' : 'toggleRepType',
'click .add-beat' : 'addBeat',
'click .remove-beat' : 'removeBeat'
},
initialize: function(options) {
if(options.model){this.model=options.model;}
// Dont use change per http://stackoverflow.com/questions/24811524/listen-to-a-collection-add-change-as-a-model-attribute-of-a-view#24811700
this.listenTo(this.model.beatsCollection, 'add remove reset', this.render);
this.listenTo(this.model, 'change', this.render);
},
render: function(){
// this.$el is a shortcut provided by Backbone to get the jQuery selector HTML object of this.el
// so this.$el === $(this.el)
// set the id on the empty div that currently exists
this.$el.attr('id', this.idAttr);
//This compiles the template
this.template = _.template($('#representation-template').html());
this.$el.html(this.template());
//This appends it to the DOM
$('#measure-rep-container').append(this.el);
_.each(this.model.beatsCollection.models, function(beat, index){
var beatView = new Beat.View({container:'beat-container-'+this.model.idAttr, model:beat, idAttr:this.model.idAttr+'-'+index });
}, this);
return this;
},
removeRepresentation: function() {
console.log("Removing " + this.idAttr);
this.model.destroy();
this.remove();
},
//remove: function() {
// this.$el.remove();
//},
toggleRepType: function() {
console.log('Toggling ' + this.idAttr + ' type from ' + this.model.get('type'));
this.model.set('type', (this.model.get('type') == 'line' ? 'circle' : 'line'));
console.log('Toggled ' + this.idAttr + ' type to ' + this.model.get('type'));
this.trigger('rep:toggle');
},
addBeat: function() {
this.trigger('rep:addbeat');
},
removeBeat: function() {
this.trigger('rep:removebeat');
}
});
return representation;
});
This answer should be working properly for all views, being able to create, or delete views without effecting non related views, and change attributes and have related views auto update. Again, this is to use as a teaching example to show how to properly set up a backbone app without the zombie views...
Problem
The reason you are seeing duplicate views created lies in the render() function for the Beat's view:
render: function(){
// set the id on the empty div that currently exists
this.$el.attr('id', this.idAttr);
//This compiles the template
this.template = _.template($('#beat-template').html());
this.$el.html(this.template());
//This appends it to the DOM
$('#'+this.container).append(this.el);
return this;
}
This function is called when:
when the model associated with the view changes
the beat view is first initialized
The first call is the one causing the problems. initialize() uses an event listener to watch for changes to the model to re-render it when necessary:
initialize: function(options) {
...
this.model.on('change', this.render, this); // case #1 above
this.render(); // case #2 above
...
},
Normally, this is fine, except that render() includes code to push the view into the DOM. That means that every time the model associated with the view changes state, the view not only re-renders, but is duplicated in the DOM.
This seems to cause a whole slew of problems in terms of event listeners being bound incorrectly. The reason, as far as I know, that this phenomenon isn't caused when there is just one beat present is because the representation itself also re-renders and removes the old zombie view. I don't entirely understand this behavior, but it definitely has something to do with the way the representation watches it's beatCollection.
Solution
The fix is quite simple: change where the view appends itself to the DOM. This line in render():
$('#'+this.container).append(this.el);
should be moved to initialize, like so:
initialize: function(options) {
if(options.model){
this.model=options.model;
this.container = options.container;
this.idAttr = options.idAttr;
}
this.model.on('change', this.render, this);
this.render();
$('#'+this.container).append(this.el); // add to the DOM after rendering/updating template
},
Plnkr demo with solution applied

Backbone call a model method from the view

I have a backbone app with require where I want to add a collection inside a collection with a method inside model.
I have tried to insert the method in the collection but I can't add elements.
I'd want to make a collection of app when I click an element outside the app I want add inside my app other app in a collection.
This is my app:
Model:
define(['backbone', 'collections/element'],function(Backbone, ElementCollection){
var DesignModel = Backbone.Model.extend({
initialize:function(){
console.log('Initialized Design model');
_.defaults(this, {
elements: new ElementCollection()
});
},
addElement: function(elements, options) {
return this.elements.add(elements, options);
}
});
return DesignModel;
});
Collection:
define(['backbone', 'models/design'], function(Backbone, DesignModel){
var designCollection = Backbone.Collection.extend({
model: DesignModel,
});
return designCollection;
});
View
define(['jquery' , 'backbone', 'models/design', 'collections/design', 'views/element'],
function($, Backbone, DesignModel, DesignCollection, ElementView){
var DesignView = Backbone.View.extend({
el:$('#page'),
initialize: function(){
console.log('initialize DesignView');
this.collection = new DesignCollection();
var here = this;
$('#insert-dynamic-element').click(function(){
var element = new ElementView();
here.collection.models.addElement(element);
});
},
render: function(){
}
})
return DesignView;
});
I have tried to call the function addElement in this way:
here.collection.models.addElement(element);
and
here.collection.addElement(element);
But always with error that Object has no method addElement
How can I solve this? I want to call the method addElement from the view to add an app inside another app in a collection.
Thanks
The safest way to call the method is to add the method to the collection instead of the Model. Currently the method is available on the Model instance .
So this.collection.models.addElement will not cut it
Collection
define(['backbone', 'models/design'], function(Backbone, DesignModel){
var designCollection = Backbone.Collection.extend({
model: DesignModel,
addElement: function(elements, options) {
return this.add(elements, options);
}
});
return designCollection;
});
View
define(['jquery' , 'backbone', 'models/design', 'collections/design', 'views/element'],
function($, Backbone, DesignModel, DesignCollection, ElementView){
var DesignView = Backbone.View.extend({
el:$('#page'),
initialize: function(){
console.log('initialize DesignView');
this.collection = new DesignCollection();
var here = this;
$('#insert-dynamic-element').click(function(){
var element = new ElementView();
here.collection.addElement(element);
});
},
render: function(){
}
})
return DesignView;
});
If you do not want to move the method from the current model. Then you might have to call a specific model using the index
here.collection.at(0).addElement(element);
But there might be a case when there are no model in the collection and this might lead to a error condition..
here.collection.at(0) && here.collection.at(0).addElement(element);
Well, you need to get a specific model, not the array of them. This seems like an error since you'll be picking a specific model essentially arbitrarily (unless you application has semantics that support this), but this would work:
here.collection.at(0).addElement(element);

Event handling between views

Ok I have a layout like the one in this pic:
The table in the upper part of the screen is made by:
MessageListView
define(['backbone','collections/messages','views/message'], function(Backbone, MessageCollection, MessageView) {
var MessageListView = Backbone.View.extend({
el: '#messagesContainer',
initialize: function() {
this.collection = new MessageCollection();
this.collection.fetch({reset:true});
this.listenTo( this.collection, 'reset', this.render );
this.table = this.$el.find("table tbody");
this.render();
},
render: function() {
this.collection.each( function(message, index) {
this.renderMessage(message, index);
}, this);
},
renderMessage: function(message, index) {
var view = new MessageView({
model:message,
className: (index % 2 == 0) ? "even" : "odd"
});
this.table.append( view.render().el );
}
});
return MessageListView;
});
MessageView
define(['backbone','models/message'], function(Backbone, MessageCollection, MessageView) {
var MessageView = Backbone.View.extend({
template: _.template( $("#messageTemplate").html() ),
render: function() {
this.setElement( this.template(this.model.toJSON()) );
return this;
},
events:{
'click':'select'
},
select: function() {
// WHAT TO DO HERE?
}
});
return MessageView;
});
AppView
define(['backbone','views/messages'], function(Backbone, MessageList) {
var App = Backbone.View.extend({
initialize: function() {
new MessageList();
}
});
return App;
});
I will soon add a new view (maybe "PreviewView") in the lower part of the screen.
I want to make something happen inside the "PreviewView" when user clicks a row.
For example, it could be interesting to display other model's attributes (details, e.g.) inside the PreviewView.
What is the best practice?
holding a reference to PreviewView inside each MessageView ?
triggering events inside select method, and listening to them using on() inside the preview view.
using a transient "selected" attribute in my model, and make PreviewView listen to collection "change" events?
Thank you, if you need more details tell me please, I'll edit the question.
Not sure about the best practice but I found this solution trivial to implement. I created a global messaging object, bus, whatever:
window.App = {};
window.App.vent = _.extend({}, Backbone.Events);
You have to register the "triggerable" functions of PreviewView on the previously created event bus (according to your example, this should be in the PreviewView):
initialize: function () {
App.vent.on('PreviewView.show', this.show, this);
}
Now you should be able to trigger any of registered events from anywhere within your application by calling: App.vent.trigger. For example when the user click on a row you will have something similar:
App.vent.trigger('PreviewView.show');
in case if you have to send and object along with the triggered event use:
App.vent.trigger('PreviewView.show', data);

What is better way to store and access list of views in backbone.js?

I've been using backbone for quite some time now, and each time I get dynamic lists of views that have their own events and behaviors, I wonder how should they be stored. I've two ways and my thoughts on them are..
Store views internally in another view. This requires overhead in proper filtering, but is sort-of independent from DOM + might have better memory usage
Just generate views, put them in DOM and trigger events of views with jquery, like $('#someviewid').trigger('somecustomfunction'); - easier to write and access but dependencies are harder to see and I'm not certain that view/model gets deleted if I just remove DOM node
What would you recommend?
So here is expanded second example, where new views are just appended to internal html and storyViews themselves are forgotten. But If I want to access specific view from this list, I would have to use DOM attributes, like id or data and then trigger view functions with jquery accessors
Devclub.Views.StoriesList = Backbone.View.extend({
initialize: function () {
this.collection.bind('reset', this.reset, this);
this.collection.fetch();
},
reset: function (modelList) {
$(this.el).html('');
var me = this;
$.each(modelList.models, function (i, model) {
me.add(model);
});
},
add: function (model) {
var contact_model = new Devclub.Models.Story(model);
var view = new Devclub.Views.Story({
model: contact_model
});
var storyView = view.render().el;
$(this.el).append(storyView);
}
});
In contrast, I could instead store same view list in an array and iterate over it if I want to call some view methods directly
I think you should keep a reference of the child views. Here is an example from the book by Addy Osmani.
Backbone.View.prototype.close = function() {
if (this.onClose) {
this.onClose();
}
this.remove(); };
NewView = Backbone.View.extend({
initialize: function() {
this.childViews = [];
},
renderChildren: function(item) {
var itemView = new NewChildView({ model: item });
$(this.el).prepend(itemView.render());
this.childViews.push(itemView);
},
onClose: function() {
_(this.childViews).each(function(view) {
view.close();
});
} });
NewChildView = Backbone.View.extend({
tagName: 'li',
render: function() {
} });

Backbone view event atacched to all views

I'm doing my first application in backbone and i get a strange thing happening trying to attach an event.
I got this code so far:
//View for #girl, EDIT action
GirlEditView = Backbone.View.extend({
initialize: function(el, attr) {
this.variables = attr;
console.log(attr);
this.render();
},
render: function() {
var template = _.template( $("#girl_edit").html(), this.variables );
$(this.el).html( template );
$("#edit_girl").modal('show');
}
});
//View for #girl
GirlView = Backbone.View.extend({
initialize: function(el, attr) {
this.variables = attr;
this.render();
},
render: function() {
var template = _.template( $("#girl_template").html(), this.variables );
$(this.el).html( $(this.el).html() + template );
},
events: {
"click p.modify": "modify"
},
modify: function() {
//calls to modify view
new GirlEditView({el : $("#edit_girl")}, this.variables);
}
});
//One girl from the list
Girl = Backbone.Model.extend({
initialize: function() {
this.view = new GirlView({el : $("#content")}, this.attributes );
}
});
//all the girls
Girls = Backbone.Collection.extend({
model: Girl,
});
//do magic!
$(document).ready(function() {
//Underscore template modification
_.templateSettings = {
escape : /\{\[([\s\S]+?)\]\}/g,
evaluate : /\{\[([\s\S]+?)\]\}/g,
interpolate : /\{\{([\s\S]+?)\}\}/g
}
//get initial data and fill the index
var list = [];
$.getJSON('girls.json', function(data) {
list = [];
$.each(data, function(key, val) {
list.push( new Girl(val) );
});
var myGirls = new Girls(list);
console.log( myGirls.models);
});
});
As you can see.
I'm using a collection to store all the girls and the data comes from a REST api in ruby.
Each girls create a new model instance and inside i attached a view instance.
I don't know if it's a good practice but i can't think a better way to do it.
Each view makes a content with a unique id. girl-1 girl-2 and go on.
Now, the template have a edit button.
My original idea is to attack the onclick event and trigger the edit view to get rendered.
That is working as expected.
The proble so far is:
When the events triggers, all the collection (girls) fire the edit view, not the one that "owns" the rendered view.
My question is what i'm doing wrong?
Thanks a lot
All the edit-views come up because all the GirlViews are using the same el:
this.view = new GirlView({el : $("#content")}, this.attributes );
and then you render be appending more HTML:
render: function() {
var template = _.template( $("#girl_template").html(), this.variables );
$(this.el).html( $(this.el).html() + template );
}
Backbone events are bound using delegate on the view's el. So, if multiple views share the same el, you'll have multiple delegates attached to the same DOM element and your events will be a mess of infighting.
You have things a little backwards: models do not own views, views watch models and collections and respond to their events. You'll see this right in the documentation:
constructor / initialize new View([options])
[...] There are several special options that, if passed, will be attached directly to the view: model, collection, [...]
Generally, you create a collection, c, and then create the view by handing it that collection:
var v = new View({ collection: c })
or you create a model, m, and then create a view wrapped around that model:
var v = new View({ model: m })
Then the view binds to events on the collection or model so that it can update its display as the underlying data changes. The view also acts as a controller in Backbone and forwards user actions to the model or collection.
Your initialization should look more like this:
$.getJSON('girls.json', function(data) {
$.each(data, function(key, val) {
list.push(new Girl(val));
});
var myGirls = new Girls(list);
var v = new GirlsView({ collection: myGirls });
});
and then GirlsView would spin through the collection and create separate GirlViews for each model:
var _this = this;
this.collection.each(function(girl) {
var v = new GirlView({ model: girl });
_this.$el.append(v.render().el);
});
Then, GirlView would render like this:
// This could go in initialize() if you're not certain that the
// DOM will be ready when the view is created.
template: _.template($('#girl_template').html()),
render: function() {
this.$el.html(this.template(this.model.toJSON());
return this;
}
The result is that each per-model view will have its own distinct el to localize the events. This also makes adding and removing a GirlView quite easy as everything is nicely wrapped up in its own el.

Resources