Backbone not making a put request with save() after save - javascript

I am experiencing a really interesting problem with backbone, I have a function like this in one of my views:
addpeople :function(){
var authArray = _.clone(this.model.get("authorizedUsers"));
var values = $(".add-input").val().split(",");
values.forEach(function(value) {
authArray.push(value);
});
this.model.set("authorizedUsers" , authArray);
this.model.save();
},
this function gets called when a button is clicked. This version of the function triggers a change event because I am cloning my array, but for some reason this.model.save()never gets called, aka the server never receives a PUT request. When I refresh the page I go back to the old state of the model..
However if I dont clone the array and change the function to, this:
addpeople :function(){
var authArray = this.model.get("authorizedUsers");
var values = $(".add-input").val().split(",");
values.forEach(function(value) {
authArray.push(value);
});
this.model.set("authorizedUsers" , authArray);
this.model.save();
},
This time the PUT request is sent successfully, but the page is not re-rendered because a change event is not triggered. When I refresh the page I can see the updated model..
I know that I can manually trigger a change event in the second example but I am more curious about why my this.model.save() is not called in the first example..
To help you understand the problem more my model looks something like:
var PostModel = Backbone.Model.extend({
urlRoot : '/tweet',
idAttribute: '_id',
defaults:{
name: '',
comments: [],
tags: [],
authorizedUsers: [],
postedBy : '',
dateCreated: ''
},
});
and my node.js update function looks like:
exports.updateTweet = function(req,res){
console.log("Getting called ! ")
var update = req.body;
var id = req.url.split("/")[2];
Post.update({ _id: id }, { $set: { authorizedUsers: req.body.authorizedUsers }}, function (err, post) {
if (err) return handleError(err);
});
res.end();
};

The reason why change didn't trigger for your second example is because it is the same object and Backbone ignore it. Hence no change event triggered.
As for why the first one failed; do you have validator for your model? May be something that validating for empty string perhaps? val() can return an empty string and split() on empty string will return [""]
Also, your defaults should be a function otherwise all your model would have the same instance of comments, tags and authorizedUsers
From Backbone doc.
Remember that in JavaScript, objects are passed by reference, so if you include an object as a default value, it will be shared among all instances. Instead, define defaults as a function.
Arrays are object too.
var PostModel = Backbone.Model.extend({
urlRoot : '/tweet',
idAttribute: '_id',
defaults: function(){
return {
name: '',
comments: [],
tags: [],
authorizedUsers: [],
postedBy : '',
dateCreated: ''
}
}
});
Lastly, array.forEach() is not available on IE8 and older.

Related

Giving a single reference to multiple Backbone.Models

I have a Backbone.Model which looks something like:
var FooModel = Backbone.Model.extend({
defaults: {
details: '',
operatingSystem: ''
};
});
There are many instances of FooModel which are stored in a collection:
var FooCollection = Backbone.Collection.extend({
model: FooModel
});
FooModel's OperatingSystem is a property which only needs to be calculated once and is derived asynchronously. For example:
chrome.runtime.getPlatformInfo(function(platformInfo){
console.log("Operating System: ", platformInfo.os);
});
If I perform this logic at the FooModel level then I will need to perform the logic every time I instantiate a FooModel. So, I think that this operation should be performed at a higher level. However, it is bad practice to give properties to a Backbone.Collection.
As such, this leaves me thinking that I need a parent model:
var FooParentModel = Backbone.Model.extend({
defaults: {
platformInfo: '',
fooCollection: new FooCollection()
},
initialize: function() {
chrome.runtime.getPlatformInfo(function(platformInfo){
this.set('platformInfo', platformInfo);
}.bind(this));
},
// TODO: This will work incorrectly if ran before getPlatformInfo's callback
createFoo: function(){
this.get('fooCollection').create({
details: 'hello, world',
operatingSystem: this.get('platformDetails').os
});
}
});
This works and is semantically correct, but feels over-engineered. The extra layer of abstraction feels unwarranted.
Is this the appropriate way to go about giving a property to a model?
Although Backbone Collections may not have attributes, they may have properties (as well as any object) which you can use to store shared data.
var FooCollection = Backbone.Collection.extend({
model: FooModel
initialize: function() {
this.platformInfo = null; // shared data
chrome.runtime.getPlatformInfo(function(platformInfo){
this.platformInfo = platformInfo;
}.bind(this));
},
// wrapper to create a new model within the collection
createFoo: function(details) {
this.create({
details: details,
operatingSystem: this.platformInfo? this.platformInfo.os : ''
});
}});
});

Add multiple records to model's collection Sailsjs

I have the following models in my Sailsjs application with a many-to-many relationship:
event.js:
attributes: {
title : { type: 'string', required: true },
description : { type: 'string', required: true },
location : { type: 'string', required: true },
maxMembers : { type: 'integer', required: true },
currentMembers : { collection: 'user', via: 'eventsAttending', dominant: true },
creator : { model: 'user', required: true },
invitations : { collection: 'invitation', via: 'eventID' },
tags : { collection: 'tag', via: 'taggedEvents', dominant: true },
lat : { type: 'float' },
lon : { type: 'float' },
},
tags.js:
attributes: {
tagName : { type: 'string', unique: true, required: true },
taggedEvents : { collection: 'event', via: 'tags' },
},
Based on the documentation, this relationship looks correct. I have the following method in tag.js that accepts an array of tag strings, and an event id, and is supposed to add or remove the tags that were passed in:
modifyTags: function (tags, eventId) {
var tagRecords = [];
_.forEach(tags, function(tag) {
Tag.findOrCreate({tagName: tag}, {tagName: tag}, function (error, result) {
tagRecords.push({id: result.id})
})
})
Event.findOneById(eventId).populate('tags').exec(function(error, event){
console.log(event)
var currentTags = event.tags;
console.log(currentTags)
delete currentTags.add;
delete currentTags.remove;
if (currentTags.length > 0) {
currentTags = _.pluck(currentTags, 'id');
}
var modifiedTags = _.pluck(tagRecords, 'id');
var tagsToAdd = _.difference(modifiedTags, currentTags);
var tagsToRemove = _.difference(currentTags, modifiedTags);
console.log('current', currentTags)
console.log('remove', tagsToRemove)
console.log('add', tagsToAdd)
if (tagsToAdd.length > 0) {
_.forEach(tagsToAdd, function (tag) {
event.tags.add(tag);
})
event.save(console.log)
}
if (tagsToRemove.length > 0) {
_.forEach(tagsToRemove, function (tagId) {
event.tags.remove(tagId)
})
event.save()
}
})
}
This is how the method is called from the event model:
afterCreate: function(record, next) {
Tag.modifyTags(tags, record.id)
next();
}
When I post to event/create, I get this result: http://pastebin.com/PMiqBbfR.
It looks as if the method call itself is looped over, rather than just the tagsToAdd or tagsToRemove array. Whats more confusing is that at the end, in the last log of the event, it looks like the event has the correct tags. When I then post to event/1, however, the tags array is empty. I've also tried saving immediately after each .add(), but still get similar results.
Ideally, I'd like to loop over both the tagsToAdd and tagsToRemove arrays, modify their ids in the model's collection, and then call .save() once on the model.
I have spent a ton of time trying to debug this, so any help would be greatly appreciated!
There are a few problems with your implementation, but the main issue is that you're treating certain methods--namely .save() and .findOrCreate as synchronous methods, when they are (like all Waterline methods) asynchronous, requiring a callback. So you're effectively running a bunch of code in parallel and not waiting for it to finish before returning.
Also, since it seems like what you're trying to do is replace the current event tags with this new list, the method you came up with is a bit over-engineered--you don't need to use event.tags.add and event.tags.remove. You can just use plain old update.
So you could probably rewrite the modifyTags method as:
modifyTags: function (tags, eventId, mainCb) {
// Asynchronously transform the `tags` array into an array of Tag records
async.map(tags, function(tag, cb) {
// For each tag, find or create a new record.
// Since the async.map `cb` argument expects a function with
// the standard (error, result) node signature, this will add
// the new (or existing) Tag instance to the resulting array.
// If an error occurs, async.map will exit early and call the
// "done()" function below
Tag.findOrCreate({tagName: tag}, {tagName: tag}, cb);
}, function done (err, tagRecords) {
if (err) {return mainCb(err);}
// Update the event with the new tags
Event.update({id: eventId}, {tags: tagRecords}).exec(mainCb);
});
}
See the full docs for async.map here.
If you wanted to stick with your implementation using .add and .remove, you would still want to use async.map, and do the rest of your logic in the done method. You don't need two .save calls; just do run all the .add and .remove code first, then do a single .save(mainCb) to finish it off.
And I don't know what you're trying to accomplish by deleting the .add and .remove methods from currentTags (which is a direct reference to event.tags), but it won't work and will just cause confusion later!

Knockback.js backbone collection only adds first element to UI

I'm trying to set something new up with Knockback.js, and right now I'm running into an issue with the knockout/knockback integration. The problem is, I've successfully written an event handler which adds a new model to the Objectives collection, but the UI only registers and adds the first such addition. It does successfully add the new objective to the list, but only the first one--after that, while the collection does successfully add a new model to the list, it doesn't appear in the UI.
<a class="btn" id="click">Click me!</a>
<div id="objectives" data-bind="foreach: objectives">
<h3 data-bind="text: name"></h3>
</div>
And this script:
// Knockback script MUST be located at bottom of the page
$(document).ready(new function() {
// instantiate the router and start listening for URL changes
var page_router = new PageRouter();
Backbone.history.start();
// Get JSON value
var objectives;
$.getJSON('json.php', {table: 'objectives'}).done(function(data) {
objectives = new ObjectiveCollection(data);
var view_model = {
objectives: kb.collectionObservable(objectives, {view_model: kb.ViewModel})
};
ko.applyBindings(view_model, $('#objectives').get(0));
});
$('#click').click(function() {
var objective_model = new Objective({category: 3, name: Math.random(), descriptor: 'What up'});
objectives.add(objective_model);
console.log(objectives);
});
});
Where the only custom models are as seen here:
/**
* Objectives model
*/
var Objective = Backbone.Model.extend({
// Defaults
defaults: {
id: null,
category: null,
weight: null,
name: null,
descriptor: null
},
// Url to pass to
url : function() {
    // Important! It's got to know where to send its REST calls.
    // In this case, POST to '/donuts' and PUT to '/donuts/:id'
    return this.id ? '/objectives/' + this.id : '/objectives';
}
});
/**
* Basic objectives collection
*/
var ObjectiveCollection = Backbone.Collection.extend({
model: Objective,
initialize: function(models,options) {}
});
Turns out, the issue was located here:
var Objective = Backbone.Model.extend({
// Defaults
defaults: {
id: null,
category: null,
weight: null,
name: null,
descriptor: null
}
It kept generating models with an ID of null, and the program will only display objects with a unique ID. Since the ID defaults to null, it will consider two objects without defined IDs as being the same. By erasing the id: null; line, this problem stopped being an issue.

Backbone model.save() is sending PUT instead of POST

I call save using this:
console.log(this.model.isNew());
console.log(this.model);
this.model.save({}, {
success: function (model, response, options) {
console.log(response);
},
error: function (model, xhr, options) {
console.log(xhr.result.Errors);
}
});
The isNew() returns false. But the output of this.model has an ID of 0. (this.model.id is 0 as well)
My url is url: ROOTAREA + "/Expenses/Entry/",
Updating works fine, and uses PUT as expected.
Edit : here's part of my model:
defaults: function () {
return {
DocumentDate: "",
JobNo_: "",
PhaseCode: "",
WorkTypeCode: "",
Description: "",
Quantity: 0,
UnitCost: 0,
ExpenseCurrencyCode: "",
ReimbursementCurrencyCode: "",
UnitofMeasureCode: "DIEM",
LineNo_: 0
};
},
idAttribute: "LineNo_",
ID should not even exist for a new entry.
The issue is in the part you didn't show - in the part where you instantiate, create and populate the model.
Here is a quote from the Backbone documentation:
If the model does not yet have an id, it is considered to be new.
It is clear from your code that you are assigning an id attribute.
Your backend should be doing that.
And since you are doing it on a client, backbone presumes it it not new, and uses PUT
The above answers are correct in that if the model you are .save'ing has an id attribute backbone will do a PUT rather than a POST.
This behavior can be overridden simply by adding type: 'POST' to your save block:
var fooModel = new Backbone.Model({ id: 1});
fooModel.save(null, {
type: 'POST'
});
You can specify the ID in defaults, just make sure it's set to null (isNew will be set to true).
In your case it must be
LineNo_: null

How to get id on a successful model.save()?

Have checked some backbone.js tutorials and can't
understand how to get model id from the server within
the model saving process. I have a model:
var Game = Backbone.Model.extend({
defaults: {
name: '',
releaseDate: ''
},
url: function(){
return '/data.php';
}
});
How to implement getting the id algorithm? It seams to me, there should
be a kind of callback function, but can't realise where to put it.
See Backbone's documentation on model save.
You can pass a success callback function to save, something like this:
var game = new Game({
name: 'Duke Nukem 3D',
releaseDate: '1996'
});
game.save({}, {
success: function(model, response) {
// get model id from response?
}
);

Categories

Resources