What is the correct way to persist an inherited variable, on action to the parent in Backbone.js?
I can see some logical ways to do this but they seem inefficient and thought it might be worth asking for another opinion.
The two classes are both views which construct a new model to be saved to a collection, the parent passing a variable through to a popup window where this variable can be set.
I'm not sure there's enough detail in your question to answer, but there are a few ways to to do this:
Share a common model. As you describe it, you're using two views to construct a model, so the easiest way is probably to pass the model itself to the child view and have the child view modify the model, rather than passing any variables between views:
var MyModel = Backbone.Model.extend({});
var ParentView = Backbone.View.extend({
// initialize the new model
initialize: function() {
this.model = new MyModel();
},
// open the pop-up on click
events: {
'click #open_popup': 'openPopUp'
},
openPopUp: function() {
// pass the model
new PopUpView({ model: this.model })
}
});
var PopUpView = Backbone.View.extend({
events: {
'change input#someProperty': 'changeProperty'
},
changeProperty: function() {
var value = $('input#someProperty').val();
this.model.set({ someProperty : value });
}
});
Trigger an event on the parent. If for some reason you can't just pass the value via the model, you can just pass a reference to the parent and trigger an event:
var ParentView = Backbone.View.extend({
initialize: function() {
// bind callback to event
this.on('updateProperty', this.updateProperty, this);
},
updateProperty: function(value) {
// do whatever you need to do with the value here
},
// open the pop-up on click
events: {
'click #open_popup': 'openPopUp'
},
openPopUp: function() {
// pass the model
new PopUpView({ parent: this })
}
});
var PopUpView = Backbone.View.extend({
events: {
'change input#someProperty': 'changeProperty'
},
changeProperty: function() {
var value = $('input#someProperty').val();
this.options.parent.trigger('updateProperty', value);
}
});
Related
I'm writing basic to-do list using Backbone.js. Every input adding as a model to collection. Listening for 'add' on collection and rendering newly added model (appending li with 'task' to ul). Then by double-clicking on item I'm retrieving html() of it and in a loop comparing it to corresponding attribute in a model. When it catch the right model - destroying the model (should be deleted from a collection accordingly). But some issue occuring in console, it says
Uncaught TypeError: Cannot read property 'toJSON' of undefined
and adding some buggy effect (not everytime can delete item by the first dblckick). If anyone can point the problem out it would be greatly appreciated!
Here's code
var Model = Backbone.Model.extend({
default: {
task: '',
completed: false
}
});
var Collection = Backbone.Collection.extend({
model: Model
});
var ItemView = Backbone.View.extend({
tagName: 'li',
render: function () {
this.$el.html(this.model.toJSON().task);
return this;
}
});
var TodoView = Backbone.View.extend({
el: '#todo',
initialize: function () {
this.collection = new Collection();
this.collection.on('add', this.render, this);
},
events: {
'click .add': 'add',
'dblclick li': 'destroy',
'keydown': 'keyEvent'
},
add: function () {
this.collection.add(new Model({ //adding input as an model to collection
task: this.$el.find('#todo').val(),
completed: false
}));
this.$el.find('#todo').val(''); //clearing input field
this.$el.find('#todo').focus(); //focusing input after adding task
},
keyEvent: function (e) {
if (e.which === 13) {
this.add();
}
},
destroy: function (e) {
// console.log(this.collection.toJSON());
this.collection.each(function (model) {
if ($(e.target).html() === model.toJSON().task) {
model.destroy();
}
});
e.target.remove();
// console.log(this.collection.toJSON());
},
render: function (newModel) {
var self = this,
todoView;
todoView = new ItemView({
model: newModel
});
self.$el.find('.list').append(todoView.render().el);
return this;
}
});
var trigger = new TodoView();
And here's http://jsbin.com/ciwunizuyi/edit?html,js,output
The problem is that in your destroy method, you find the model to destroy by comparing the task property of the models. If you have multiple models with the same task property, you'll get the error. The actual error occurs because you're removing items from the collection while iterating over it.
Instead of comparing the task property, you could use the cid (client id) property that Backbone gives all models. One way to do this would be this:
When rendering an ItemView, use jQuery's data method to store the cid with the view element (alternatively, use a custom data attribute)
this.$el.data('cid', this.model.cid);
In the destroy function, get the cid property from the view element, and use it to find the right model in the collection (you can use the collection's get method here):
destroy: function (e) {
var id = $(e.target).data('cid');
var model = this.collection.get(id);
model.destroy();
e.target.remove();
},
Adding a unique attribute to the DOM element is only one way to solve this problem. One, much better, alternative would be to listen for the double-click event from the ItemView class itself. That way, you would always have a reference to this.model.
EDIT: This shows the code above in action: http://jsbin.com/nijikidewe/edit?js,output
I'm using backbone js in my Project, I'm struck in a small confusion with views.
I'm having these 2 views. After writing them i am in a confusion whether i'm in right path or not. The reason for my doubt is that the code was looking almost the same except that the el in which the view is rendered and the template that is used in the view.
Will this type of code effect the performance?? can I optimize it ?
code:
Project.views.list = Backbone.View.extend({
// The DOM Element associated with this view
el: '.lists-section-content',
itemView: function(x){
return new Project.views.list(x);
},
// View constructor
initialize: function(payload) {
this.data = payload.data;
this.colStr = payload.colStr;
this.render();
},
events: {
},
render: function() {
sb.renderXTemplate(this);
return this;
}
});
Firstly you be better to provide el value at first element of tree initialization, otherwise all views will try to use same DOM element(s):
var myTreeRoot= new Project.views.list({
el: '.lists-section-content',
data: payload.data,
colStr: payload.colStr
});
After this you'll need to modify initialize function a little to utilize options argument of view constructor:
// View constructor
initialize: function(options) {
this.data = options.data;
this.colStr = options.colStr;
this.render();
},
And finally answering to your question, no this way it will not affect performance. You just need to track leaf views inside parent view and remove them with parent, it's needed to avoid memory leaks. Here is example of cleanup (all leaf views collected with this.subViews array and removed on parent removal):
Project.views.list = Backbone.View.extend({
// The DOM Element associated with this view
itemView: function(x){
var view = new Project.views.list(x)
this.subViews.push(view)
this.$('.item-container:first').append(view.el)
},
remove: function() {
_.invoke(this.subViews, 'remove')
return Backbone.View.prototype.remove.apply(this, arguments)
},
// View constructor
initialize: function(options) {
this.data = options.data;
this.subViews = [];
this.colStr = options.colStr;
this.render();
},
render: function() {
sb.renderXTemplate(this);
return this;
}
});
I have the following View:
var PageView = Backbone.View.extend({
model: Models.MyModel,
initialize: function () {
this.state = window.state;
this.state.on("change", this.render, this);
},
render: function () {
}
});
The state is an another Model that will contain different global settings like: pageNumber, pageSize and etc.
So my question is: is it possible to change this.state.on("change", this.render, this); to something like:
this.state.on("change:pageNumber=2", this.render, this);
i.e. this.render will be executed after state is changed and only if pageNumber property will be equal to 2.
I know that I can just place if statement into render method but if there is way to do that like above it will be greater.
Thanks.
Backbone does not offer a filtering mechanism on events, but you could alter your state model to trigger custom events with the signature you wish.
For example, let's say state is an instance of this class
var EqualBindModel = Backbone.Model.extend({
arm: function(attribute, watchvalue) {
this.on('change:'+attribute, function(model, val, options) {
if (watchvalue === val)
model.trigger('change:'+attribute+'='+val, model, val, options);
});
}
});
you could then setup your custom event with
var state = new EqualBindModel();
state.arm('pageNumber', 2);
and listen to with
state.on("change:pageNumber=2", function(model, value, options) {
console.log('event change:pageNumber=2');
});
And a demo http://jsfiddle.net/ZCab8/1/
Trying to setup a successful binding on a model.
Binding works when adding/removing/updating values to the model. But once I recreate the model, binding stops working. I need to create new model so it gets new id/cid when saved to Collection.
I tried this.model.clear() but that doesn't assign new id/cid to the model.
Hope it makes sense. Thank you!
app.View = Backbone.View.extend({
initialize: function() {
this.model = new app.Model();
this.listenTo(this.model,'change', this.semaphore);
},
start: function(value) {
// Create new model unless running app first time
if( this.model.attributes.title != null ) this.model = new app.Model();
this.model.set({title: value});
},
semaphore: function() {
// Doesn't get call when new model gets reassigned
// Do stuff...
}
}
Nothing fancy but it works:
initialize: function() {
// Leave it blank
},
// New element gets created within the view
start: function(value) {
// Creates new model
this.model = new app.Word();
// Binds it to a function
this.listenTo(this.model,'change', this.semaphore);
// Set value(s) so binding function reacts to it
this.model.set({title: value});
},
I have a Backbone View for a general element that specific elements will be inheriting from. I have event handling logic that needs to be applied to all elements, and event handling logic that is specific to child-type elements. I'm having trouble because a child View has its own callback for an event that is also handled by the parent View, and so when I try to use the events hash in both, either the child or parent callback is never called. Let me illustrate with some code:
var ElementView = Backbone.View.extend({
events: {
"mouseup": "upHandler",
"mousedown": "downHandler",
"mousemove": "moveHandler"
},
initialize: function() {
// add events from child
if (this.events)
this.events = _.defaults(this.events, ElementView.prototype.events);
this.delegateEvents(this.events);
}
});
var StrokeView = ElementView.extend({
events: {
"mousemove": "strokeMoveHandler"
}
});
How would I solve this in an extensible way, especially if I will later have another level of inheritance?
One way to handle this would be to use "namespaces" for events:
var ElementView = Backbone.View.extend({
events: {
"mouseup.element": "upHandler",
"mousedown.element": "downHandler",
"mousemove.element": "moveHandler"
},
initialize: function() {
// add events from child
if (this.events)
this.events = _.defaults(this.events, ElementView.prototype.events);
this.delegateEvents(this.events);
}
});
var StrokeView = ElementView.extend({
events: {
"mousemove.strokeview": "strokeMoveHandler"
}
});
In fact, this approach is suggested in Backbone.js documentation.
I've done something similiar by taking advantage of JavaScript's faux-super as mentioned in the Backbone.js documentation and the initialization function
var ElementView = Backbone.View.extend({
events: {
"mouseup": "upHandler",
"mousedown": "downHandler",
"mousemove": "moveHandler"
},
initialize: function() {
this.delegateEvents();
}
});
var StrokeView = ElementView.extend({
initialize: function() {
this.events = _.extend({}, this.events, {
"mousemove": "strokeMoveHandler"
});
// Call the parent's initialization function
ElementView.prototype.initialize.call(this);
}
});
var SubStrokeView = StrokeView.extend({
initialize: function() {
this.events = _.extend({}, this.events, {
"click": "subStrokeClickHandler",
"mouseup": "subStrokeMouseupHandler"
});
// Call the parent's initialization function
StrokeView.prototype.initialize.call(this);
}
});
var c = new SubStrokeView();
console.log(c.events);
// Should be something like
// click: "subStrokeClickHandler"
// mousedown: "downHandler"
// mousemove: "strokeMoveHandler"
// mouseup: "subStrokeMouseupHandler"
The magic happens by setting the events within the initialize function. If you have multiple events attributes in your prototypes, JavaScript will only see the one set most recently due to how prototyping works.
Instead, by doing it this way, each view sets its own this.events, then calls its parent's initialize function which in turn extends this.events with its events, and so on.
You do need to set this.events in this specific way:
this.events = _.extend({}, this.events, ...new events...);
instead of
_.extend(this.events, ...new events...);
Doing it the second way will munge the events object within the parent's (ElementView) prototype. The first way ensures each model gets its own copy.