Trouble saving a model within a view with backbone.js - backbone.js

I've got a view setup that shows a couple of input fields. When either of the fields is changed I want to save the changed value for the model.
Here's my view:
EditWordView = Backbone.View.extend({
tagName: 'article',
className: 'word',
initialize: () ->
_.bindAll this, 'render'
this.template = _.template($('#edit-word-template').html())
this.model.bind 'change', this.render
,
events: {
"change input": "changed"
},
render: () ->
this.model.fetch()
$word = $('#word')
$word.empty()
renderContent = this.template(this.model.toJSON())
$(this.el).html(renderContent)
$word.append(this.el)
this
,
changed: (event) ->
changed = event.currentTarget
value = $("#" + changed.id).val()
obj = {}
obj[changed.id] = value
this.model.save(obj)
,
save: (event) ->
this.model.save()
view = new WordView({model: this.model})
view.render()
event.preventDefault()
})
And here's the model:
SpellingWord = Backbone.Model.extend({
url: () ->
'/spelling_words/' + this.id
})
When I get to the changed event and try to save the model
this.model.save(obj)
has no effect
The set(attrs) method is called on the model within the save method in backbone.js but the value is never changed in the model and hence never persisted back to the backend. Is this something to do with the scope of the this.model object?
If I call
this.model.set(obj)
before this.model.save(obj) then value of the model's attributes never changes.
set(attrs) works within the console for objects so I'm a bit stumped as to why it won't in my view.
Ideas?
EDIT>>>>>>>
I've dug into the backbone code and in the save method set is called:
save : function(attrs, options) {
options || (options = {});
if (attrs && !this.set(attrs, options)) return false;
var model = this;
var success = options.success;
After the this.set(attrs, options) method is called the attributes of the model aren't changed.
Now, looking in the set method I don't see how the attribute is supposed to be changed when it's actually changing the now variable?
var now = this.attributes, escaped = this._escapedAttributes;
// Run validation.
if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false;
// Check for changes of `id`.
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
// We're about to start triggering change events.
var alreadyChanging = this._changing;
this._changing = true;
// Update attributes.
for (var attr in attrs) {
var val = attrs[attr];
if (!_.isEqual(now[attr], val)) {
now[attr] = val;
delete escaped[attr];
this._changed = true;
if (!options.silent) this.trigger('change:' + attr, this, val, options);
}
}

Well, looks like
this.model.bind 'change', this.render
was calling the render method from inside save, which first did a fetch and overwrote my changes. The save then didn't take place because the attributes hadn't been changed.
It was always going to be something like that.....

Related

Override backbone 'set' method

I want to override backbone set method so that whenever I set a value to backbone Model the callbacks registered on that attribute get called without checking for same previous value of that attribute .
var model = Backbone.Model.extend({
defaults : {
prop1 : true
}
});
var view = Backbone.View.extend({
initialize : function(){
this.listenTo(this.model,"change:prop1", this.callback);
},
callback : function(){
// set is called on prop1
}
});
var m1 = new model();
var v1 = new view({model:m1});
m1.set("prop1",true); // It doesn't trigger callback because I'm setting the same value to prop1
You can write a new method in backbone model set like this :
var model = Backbone.Model.extend({
defaults: {
prop1: true;
},
// Overriding set
set: function(attributes, options) {
// Will be triggered whenever set is called
if (attributes.hasOwnProperty(prop1)) {
this.trigger('change:prop1');
}
return Backbone.Model.prototype.set.call(this, attributes, options);
}
});
Actually, the signature of function set differs from the one depicted in the accepted answer.
export var MyModel = Backbone.Model.extend({
set: function (key, val, options) {
// custom code
var result = Backbone.Model.prototype.set.call(this, key, val, options);
// custom code
return result;
}
});
See:
http://backbonejs.org/docs/backbone.html
https://github.com/jashkenas/backbone/issues/1391

Change event is not getting trigger when updating model in backbone js

I have a view called layerPanel that is using screenData model. Now on model.set i get update event from model itself, but its not working on view.
MODEL
var screenData = Backbone.Model.extend({
initialize : function() {
_.bindAll(this,"update");
this.bind('change:vdata', this.update);
},
update: function() {
var obj = this.vdata;
alert("update");
},
vdata:[{id : 0, title : "Welcome to Persieve 0"}]
});
VIEW
var layerPanel = Backbone.View.extend({
el: "#allLayers",
model: new screenData(),
initialize: function() {
this.render();
this.model.bind('change:vdata', this.render);
},
render: function() {
this.template = _.template(LayersTpl, {splitData: this.model.vdata});
this.$el.html(this.template);
return this;
}
});
Here is how I set values in Model.
screendata = new screenData;
var obj = screendata.vdata;
obj[obj.length] = {id : $(".bullet").length, title : "Welcome to Persieve"};
var tempData = [];
for ( var index=0; index<obj.length; index++ ) {
if ( obj[index]) {
tempData.push( obj );
}
}
obj = tempData;
screendata.set({vdata:[obj]});
The event should fire. But your render wont work as the 'this' context needs setting.
try:
this.model.bind('change:vdata', this.render, this);
or even better, use listenTo and the context is implicit (+ you can clean up easily this.remove())
Edit. From the edit you made above, I can see that you are creating a new screendata instance. The binding you created is for a different instance model: new screenData() .
You must reference the binded object and set it if you want the event to trigger.
If all the model setting happens in the actual model. Call this.set({vdata:[obj]});

Configurable Backbone.js events selectors

I'm working on simple application with excelent framework Backbone.js and it will be nice if I could make configurable backbone view events selector for example this way it doesn't work, cause it's wrong js syntax:
return Backbone.View.extend({
events: {
'click ' + someConfigSelector: 'doSomethink'
}
})
Is there any other way to setup events in Backbone views?
Check out this fiddle: http://jsfiddle.net/phoenecke/hyLMz/1/
You can use a function for events instead of an object.
return Backbone.View.extend({
events: function() {
// add some normal events to the hash
var e = {
'click #foo': 'doSomething1',
'click #bar': 'doSomething2'
};
// add one where key is calculated
e['click ' + someConfigSelector] = 'doSomething';
// return events hash
return e;
}
});
That way, you can add your events where the key is calculated based on your config.
I prefer the method below for dynamic events, as this function will be called any time delegateEvent is called.
Per the docs on delegateEvent, which is called after the view's initialize function is executed, you can use this technique to create the events dynamically:
var TestView = Backbone.View.extend({
events: function () {
var evt = {};
evt['click ' + this._extraSelector] = '_onButtonClick';
return evt;
},
initialize: function(config) {
// passed in the selector, or defaulted
this._extraSelector = config.extraSelector || "BUTTON.default";
},
_onButtonClick: function() { }
});
var v = new TestView({ extraSelector: 'BUTTON.sample' });
You could do it like this:
return Backbone.View.extend({
events: { },
initialize:function() {
this.events['click ' + someConfigSelector] = 'doSomethink';
}
})
But it somewhat breaks the convention of how events are declared in backbone and makes the code harder to read. Perhaps someone has a better approach.

I'm new to TDD backbone with jasmine-sinon. change event in backbone unit test

I'm making a test for event change to a selectbox view in backbone which is 'should provide the correct path to router.navigate when changed'. this is the scenario, If I select a value in the dropdown, it should redirect to the correct url.
this is the test (campaign.test.js):
it('should provide the correct path to router.navigate when changed', function() {
var routeName = 'test_campaign';
var routerSpy = sinon.spy(Insights.router, 'navigate');
this.view.$el.bind('change', function() {
expect(routerSpy).toHaveBeenCalledWith('campaign/' + routeName, true);
});
//create the option element holding the test value
this.view.$el.append($('<option value="' + routeName +'" selected="selected" />'));
this.view.$el.trigger('change');
routerSpy.restore();
});
this is the module (campaign.js):
DropdownView: Backbone.View.extend({
tagName: 'select',
className: 'campaign-list',
events: {
'change' : 'navCampaign'
},
initialize: function() {
_.bindAll(this);
this.collection.on('reset', this.render);
this.collection.on('add', this.addCampaign);
//if no campaigns exist then add a dummy placeholder
if(this.collection.models.length === 0) {
this.$el.append('<option value="">No Campaigns</option>');
}
},
navCampaign: function() {
Insights.router.navigate();
},
addCampaign: function(campaign) {
//remove the No Campaigns placeholder when campaigns are added
var firstChild = this.$el.children(':first');
if(firstChild.attr('value') === '') {
firstChild.remove();
}
var view = new Insights.Campaign.DropdownItem({ model: campaign });
var item = view.render().el;
this.$el.append(item);
},
render: function() {
this.collection.each(this.addCampaign);
return this;
}
})
In my test I created a new OPTION element then set a value with attribute selected.
How can I pass this as the currentTarget of the change event and send it to trigger()?
Or is there a easier way to do this?
Im getting this test fail. Error: Expected Function to have been called with 'campaign/test_campaign', true.
Your test have to look like this:
it('should provide the correct path to router.navigate when changed', function() {
var routeName = 'test_campaign';
var routerSpy = sinon.spy(Insights.router, 'navigate');
var view = new DropdownView();
//create the option element holding the test value
this.view.$el.append($('<option value="' + routeName +'" selected="selected" />'));
this.view.$el.trigger('change');
expect(routerSpy).toHaveBeenCalledWith('campaign/' + routeName, true);
routerSpy.restore();
});
First of all you have to create an instance of your view in the test and you have to create the spy before you create the view. Both can also be done in the beforeEach block.
When you adding the expect code in an event handler it can happen that it will called before the navigate method on your router is called. Cause two handlers are added to the listener, one to call navigate on the router and one that test that it was called on the router. As you can't insure which one is called first the test will fail when the listener from the test is called first.
Maybe its better to test it with passing the collection with data to the view, not setting the DOM of the view element directly in the test. So you will also test that your initialize and addData method will work.
Here are the changes, that made this test passed. :)
I changed the value of variable routeName, in campaign.test.js
from
var routeName = 'test_campaign';
to
var routeName = this.view.$el.val();
Then implemented this test to campaign.js
navCampaign: function() {
var newRoute = 'campaign/' + this.$el.val();
Insights.router.navigate(newRoute, true);
}
there I got this green. :)
sorry, I would like to make this clear with the help of my team mate.
var routeName = this.view.$el.val(); is pointing to null
by making the value dynamic it will loose the test's effectiveness,
so I put back to var routeName = 'test_campaign'.
the point of this test is to specify what value it is expecting based on a predefined set of inputs.

How can I bind the model to the view?

When the view is initialized, how can I bind the model to the specific View that is created? The view is current initialized at the start of the application. Also, how can I bind the model to the collection?
(function ($) { //loads at the dom everything
//Creation, Edit, Deletion, Date
var Note = Backbone.Model.extend({
defaults: {
text: "write here...",
done: false
},
initialize: function (){
if(!this.get("text")){
this.set({"text": this.default.text});
}
},
edit: function (){
this.save({done: !this.get("done")});
},
clear: function (){
this.destroy();
}
});
var NoteList = Backbone.Collection.extend({
model:Note
});
var NoteView = Backbone.View.extend ({
el: "body",
initialize: function(){
alert("initialized");
var list = new NoteList;
return list;
},
events: {
"click #lol" : "createNote"
},
createNote : function(){
var note = new Note;
this.push(note);
alert("noted");
}
});
var ninja = new NoteView;
})(jQuery);
Update
I just took a look at #James Woodruff's answer, and that prompted me to take another look at your code. I didn't look closely enough the first time, but I'm still not sure what you're asking. If you're asking how to have a model or view listen for and handle events triggered on the other, then check out James's example of calling bind() to have the view listen for change (or change:attr) events on the model (although I'd recommend using on() instead of bind(), depending what version of Backbone you're using).
But based on looking at your code again, I've revised my answer, because I see some things you're trying to do in ways that don't make sense, so maybe that's what you're asking about.
New Answer
Here's the code from your question, with comments added by me:
var NoteView = Backbone.View.extend ({
// JMM: This doesn't make sense. You wouldn't normally pass `el`
// to extend(). I think what you really mean here is
// passing el : $( "body" )[0] to your constructor when you
// instantiate the view, as there can only be one BODY element.
el: "body",
initialize: function(){
alert("initialized");
// JMM: the next 2 lines of code won't accomplish anything.
// Your NoteList object will just disappear into thin air.
// Probably what you want is one of the following:
// this.collection = new NoteList;
// this.list = new NoteList;
// this.options.list = new NoteList;
var list = new NoteList;
// Returning something from initialize() won't normally
// have any effect.
return list;
},
events: {
"click #lol" : "createNote"
},
createNote : function(){
var note = new Note;
// JMM: the way you have your code setup, `this` will be
// your view object when createNote() is called. Depending
// what variable you store the NoteList object in (see above),
// you want something here like:
// this.collection.push( note ).
this.push(note);
alert("noted");
}
});
Here is a revised version of your code incorporating changes to the things I commented on:
var NoteView = Backbone.View.extend( {
initialize : function () {
this.collection = new NoteList;
},
// initialize
events : {
"click #lol" : "createNote"
},
// events
createNote : function () {
this.collection.push( new Note );
// Or, because you've set the `model` property of your
// collection class, you can just pass in attrs.
this.collection.push( {} );
}
// createNote
} );
var note = new NoteView( { el : $( "body" )[0] } );
You have to bind views to models so when a model updates [triggers an event], all of the corresponding views that are bound to the model update as well. A collection is a container for like models... for example: Comments Collection holds models of type Comment.
In order to bind a view to a model they both have to be instantiated. Example:
var Note = Backbone.Model.extend({
defaults: {
text: "write here..."
},
initialize: function(){
},
// More code here...
});
var NoteView = Backbone.View.extend({
initialize: function(){
// Listen for a change in the model's text attribute
// and render the change in the DOM.
this.model.bind("change:text", this.render, this);
},
render: function(){
// Render the note in the DOM
// This is called anytime a 'Change' event
// from the model is fired.
return this;
},
// More code here...
});
Now comes the Collection.
var NoteList = Backbone.Collection.extend({
model: Note,
// More code here...
});
Now it is time to instantiate everything.
var Collection_NoteList = new NoteList();
var Model_Note = new Note();
var View_Note = new NoteView({el: $("Some Element"), model: Model_Note});
// Now add the model to the collection
Collection_NoteList.add(Model_Note);
I hope this answers your question(s) and or leads you in the right direction.

Resources