How to mass assign all attributes of a Backbone model? - backbone.js

I would like to update the User model whenever signIn was successful. It includes the id assign by the backend which has not been present in the model before.
MyApp.module("User", function(User, App, Backbone, Marionette, $, _) {
User.Controller = Backbone.Marionette.Controller.extend({
initialize: function() {
this.model = new MyApp.User.Model();
},
signIn: function(credentials) {
var signInData = { user: credentials };
var self = this;
App.session.signIn(signInData, {
success: function(model, response, options) {
self.updateUserModel(model);
},
error: function(model, xhr, options) {}
});
},
updateUserModel: function(model) {
// TODO Update all attributes, including new onces e.g. id.
}
});
});
How would you update all attribtues at once? I know that I manually can set every single attribute but this seems to be wrong since the list of attributes may change over time.
Generally, I would expect such an update(model) method in the User model.
When I use Backbone's model.set() method as suggested by nikoshr and john-4d5 ...
signIn: function(credentials) {
var signInData = { user: credentials };
var self = this;
App.session.signIn(signInData, {
success: function(model, response, options) {
self.model.set(model);
},
error: function(model, xhr, options) {}
});
},
... the id attribute is copied into this.model but other properties such as name are missing.
The model returned in the success callback looks like this:
_changing: false
_pending: false
_previousAttributes: Object
attributes: Object
bind: function (name, callback, context) {
close: function (){
constructor: function (){ return parent.apply(this, arguments); }
created_at: "2013-07-22T19:03:24Z"
email: "user#example.com"
id: 3
initialize: function () {
listenTo: function (obj, name, callback) {
listenToOnce: function (obj, name, callback) {
logout: function () {
model: child
name: "Some User"
off: function (name, callback, context) {
on: function (name, callback, context) {
once: function (name, callback, context) {
options: Object
signIn: function (credentials) {
signUp: function (credentials) {
stopListening: function (obj, name, callback) {
trigger: function (name) {
triggerMethod: function (event) {
unbind: function (name, callback, context) {
updated_at: "2013-08-05T13:20:43Z"
user: Object
__proto__: Object
changed: Object
cid: "c3"
id: 3
__proto__: Surrogate

You are moving around a Backbone.Model,
Model.set accepts a hash of attributes,
you can convert a Backbone.Model to a hash of attributes with Model.toJSON
You could write your callback as
success: function(model, response, options) {
self.model.set(model.toJSON());
}

You can simply use set, giving the other model's attributes property value (an object with all the attribute values) as an argument.
self.model.set(model.attributes);

You can use this.model.set(model) as #nikoshr said. Iterate over the attributes and setting each one will do the same thing as model.set already does. Refer to the backbone's model.set function:
// Set a hash of model attributes on the object, firing `"change"`. This is
// the core primitive operation of a model, updating the data and notifying
// anyone who needs to know about the change in state. The heart of the beast.
set: function(key, val, options) {
var attr, attrs, unset, changes, silent, changing, prev, current;
if (key == null) return this;
// Handle both `"key", value` and `{key: value}` -style arguments.
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
[...]
// For each `set` attribute, update or delete the current value.
for (attr in attrs) {
val = attrs[attr];
if (!_.isEqual(current[attr], val)) changes.push(attr);
if (!_.isEqual(prev[attr], val)) {
this.changed[attr] = val;
} else {
delete this.changed[attr];
}
unset ? delete current[attr] : current[attr] = val;
}
[...]
}
Another option is to instantiate a new model:
this.model = new MyApp.User.Model(model);

Related

Populating data from two different url in a backbone collection

I have a Marionette.LayoutView which calls a backbone collection and fetches the data and renders based on response. Now the issue that I am facing is, this collection needs to get data from two different endpoints, both should be independent, and then return the combined result. Below is my code:
My Marionette.LayoutView
var View = Marionette.LayoutView.extend({
template: _.template(some.html),
regions: {
div1: '[data-region="div1"]',
div2: '[data-region="div2"]',
},
initialize: function () {
this.collection = new MovieCollection();
},
onRender: function () {
if (this.collection.length) {
this.div1.show(new TopMoviesByLikesView({
collection: this.collection,
movieCount: 10,
}));
this.div2.show(new TopMovieByRatingsView({
collection: this.collection,
movieCount: 10,
}));
}
},
});
module.exports = AsyncView.extend({
ViewConstructor: View,
});
My Collection
module.exports = Backbone.Collection.extend({
model: TopMovieModel,
initialize: function (response) {
let movieCollection = [];
let movieSourceOne = new TopMovieFromSourceOne();
movieSourceOne.fetch({
success: function (collection, response) {
movieCollection = [...movieCollection, ...response.data];
},
error: function (collection, response, options) {
console.info('~ Response::ERROR', collection, response, options);
}
});
let movieSourceTwo = new movieSourceTwo();
movieSourceTwo.fetch({
success: function (collection, response, options) {
movieCollection = [...movieCollection, ...response.data];
},
error: function(collection, response, options) {
console.info('~ Response::ERROR', collection, response, options);
}
});
this.collection = movieCollection;
},
The error I get is A “url” property or function must be specified is there a way where I can do this without using a url in backbone collection? Note: I want to keep two endpoints independent since I don't want the collection to fail if primary API fails.
To avoid that error with url, you should override your fetch method, to call both collections fetch instead.
function promisifyFetch(collection) {
return new Promise(function(resolve, reject) {
collection.fetch({
success() {
resolve(collection);
},
error() {
reject();
}
});
});
}
module.exports = Backbone.Collection.extend({
model: TopMovieModel,
initialize() {
this.movieSourceOne = new TopMovieFromSourceOne();
this.movieSourceTwo = new movieSourceTwo();
},
fetch(options) {
return Promise.all([
promisifyFetch(this.movieSourceOne),
promisifyFetch(this.movieSourceTwo)
]).then(([one, two]) => {
const response = [
...one.toJSON(),
...two.toJSON()
];
this.set(response, options);
this.trigger('sync', this, response, options);
});
}
});
You probably want to handle errors here aswell.

Backbone.js View with multiple collections and sorting

I have a view with multiple collections, implemented like this:
collection: {
folders: new FolderCollection(),
images: new ImageCollection(),
files: new FileCollection()
},
And example collection is like this:
var FolderCollection = Backbone.Collection.extend({
model: folderObj,
initialize:function (){
// this.bindAll(this);
// this.setElement(this.at(0));
},
comparator: function(model) {
return model.get("id");
},
getElement: function() {
return this.currentElement;
},
setElement: function(model) {
this.currentElement = model;
},
next: function (){
this.setElement(this.at(this.indexOf(this.getElement()) + 1));
return this;
},
prev: function() {
this.setElement(this.at(this.indexOf(this.getElement()) - 1));
return this;
}
});
As you can imagine, this View is a display for files, images, and folders. I then populate the view by calling three different functions; one to populate the view with folders, another for files, and another for images. Each of these functions is a separate ajax request. So, because these calls are asynchronous, there's no way to first load folders, then images, then files and there is no consistency when the page loads.
So, my problem is, I need to be able to order these three collections in multiple ways. The first problem is, since the calls are async, sometimes the folders load first, or maybe the files, etc. I can think of two ways to fix this:
Only call the next function after the previous is completed. Is this the best way? If so, how do I do that
After all the collections are loaded, sort them. If so, how is the best way to sort and order multiple collections?
If more code is needed (ie: model or view) please let me know and I can provide what ever is needed.
thanks
jason
EDIT - SHOWING VIEW
var FileManagementView = TemplateView.extend({
viewName: 'fileManagement',
className: 'fileManagement',
events: {
//my events
},
collection: {
folders: new FolderCollection(),
images: new ImageCollection(),
files: new FileCollection()
},
//life cycle
initialize: function (options) {
TemplateView.prototype.initialize.apply(this, [options]);
},
templateContext: function (renderOptions) {
},
postRender: function () {
//more functions to set up the view
this.repopulateViewWithFoldersAndFiles(currentFolderId);
},
template: function (renderOptions) {
return 'MyMainTemplate';
},
repopulateViewWithFoldersAndFiles: function(currentFolderId){
//code to do stuff to create view
//these functions are all async, so theres no way to know what will finish first
this.getFolders(currentFolderId);
this.getImages();
this.getFiles();
},
getFiles: function(){
try{
var that = this;
var url = '?q=url to function';
$.ajax({
url: url,
context: that,
data:{'methodName': 'getFiles'}
}).done(function(data) {
var results = jQuery.parseJSON(data.result.results);
if(results){
$.each(results, function( key, value ) {
var file = new fileObj;
file.set('id', value.id);
file.set('fileName', value.fileName);
//...set more attributes
that.collection.files.add(file);
that.renderFile(file);
});
}
});
} catch(e){
throw e;
}
},
renderFile: function(file){
try{
if(file) {
var template = window.app.getTemplate('AnotherTemplate');
var html = $(template({
id: file.get('id'),
fileName: file.get('fileName'),
fileIconPath: file.get('fileIconPath')
}));
this.$el.find('#fileDropZone').append(html);
}
} catch(e){
throw e;
}
},
getImages: function(){
try{
var url = '?q=url to function';
$.ajax({
url: url,
context: that,
data:{'methodName': 'getImages'}
}).done(function(data) {
var results = jQuery.parseJSON(data.result.results);
if(results){
$.each(results, function( key, value ) {
var image = new imageObj;
image.set('id', value.id);
image.set('imgTitle', value.image_name);
//....set more attributes
that.collection.images.add(image);
that.renderImage(image);
});
}
});
} catch(e){
throw e;
}
},
renderImage: function(image){
try{
if(image) {
var template = window.app.getTemplate('myViewTemplate');
var html = $(template({
imgId: image.get('id'),
imgTitle: image.get('imgTitle'),
//..more attributes
}));
this.$el.find('#fileDropZone').append(html);
}
} catch(e){
throw e;
}
},
getFolders:function(parentId){
var that = this;
var url = '?q=...path to function';
$.ajax({
url: url,
context: that,
data:{'methodName': 'getFolders'}
}).done(function(data) {
var results = jQuery.parseJSON(data.result.results);
if(results){
$.each(results, function( key, value ) {
var folder = new folderObj();
folder.set('folderName', value.folder_name);
folder.set('id', value.folder_id);
//more attributes
that.collection.folders.add(folder);
that.renderFolders(folder);
});
}else{
this.renderFolders(null);
}
});
},
//renders the folders to the view
renderFolders: function(folder){
try{
if(folder) {
var template = window.app.getTemplate('myFolderTemplate');
var html = $(template({
folderId: folder.get('id'),
folderName: folder.get('folderName'),
}));
this.$el.find('#fileDropZone').append(html);
}
} catch(e){
throw e;
}
}
});
What I ended up doing was rewriting my models and creating one model that the others inherit from. Example:
var DataModel =MyBaseModel.extend({
defaults: {
id: null,
//other shared fields
}
});
All my other models inherited, like this:
var folderObj = DataModel.extend({
// Whatever you want in here
urlRoot: '?q=myApp/api/myClassName/',
defaults: {
//other fields here
},
validate: function(attributes){
//validation here
}
});
I then used deferred, which I answered here: Jquery Promise and Defered with returned results

Validate on save and save().complete - in backbone

I have problem with validation in my model. It seems that it is impossible to use save().complete(function() {..... in the same time as validation- here is the code:
my model:
App.Models.Task = Backbone.Model.extend({
defaults: {
title:'',
completed: 0
},
validate: function (attrs, options) {
if(attrs.title == '' || attrs.title === undefined) {
return "fill title pls"
}
},
urlRoot: 'tasks'
});
and then in my view i try to save it in add method :
App.Views.TaskAdd = Backbone.View.extend({
tagName: 'div',
template: template('taskTemplateAdd'),
events : {
'click .addTask' : 'add'
},
initialize: function () {
this.model.on('add',this.render, this)
},
add : function () {
var title = $("#addNew input:eq(0)").val();
var completed = $("#addNew input:eq(1)").val();
this.model.set('title', title);
this.model.set('completed', completed);
this.model.save({},
{
success: function (model, response) {
console.log("success");
},
error: function (model, response) {
console.log("error");
}
}).complete(function () {
$("<div>Data sent</div>").dialog();
$('#list').empty();
});
},
render: function () {
this.$el.html(this.template(this.model.toJSON()));
return this
}
});
when validate fires i get error :
Uncaught TypeError: Object false has no method 'complete'
I understand that it tries probably to run complete callback on the return value but how to solve this problem ???
Model.save is documented returning the jqHXR object if successful or false if not.
So, unless your server never fails, you need to handle the case where save returns false. Here's a simple example of the logic you would need:
var valid=this.model.save();
if(!valid) {
// do something when not valid
else {
valid.complete(function() {}); // this is a jqHXR when valid
}
And, as of jQuery 1.8, the use of complete is deprecated. You should consider using always instead.
Use.
...
add : function () {
var self = this;
this.model.save({'title':$("#addNew input:eq(0)").val(),'completed':$("#addNew input:eq(1)").val()},
{
success: function (model, response) {
console.log("success");
self.complete();
},
error: function (model, response) {
console.log("error");
self.complete();
}
});
},
complete: function () {
$("<div>Data sent</div>").dialog();
$('#list').empty();
},
...
model.save() performs a validation first (validate method on the model). If it successfull, it then does the POST/PUT to the server. In other words, you get a false if the client side validation fails. It won't post to server then. You can't use the deferred object if this fails because false.always() will probally result in an error.
Alsoo, if you don't pass a wait: true in the model.save options, it will update the model with its validated object. I usually pass wait: true just to be sure. (I don't want to render the element twice).
If the model fails the client side validation, then it should also fail the server side validation. In this case there is an "invalid" event to listen to. So you only should be interested in the success call. Which in theory should only be interesting if it really has updates (would fire a "change" event)
add: {
var self = this;
this.model.on('invalid', function(error){
console.log(error, 'model is invalid. Check model.validate')
});
this.model.on('change', function(model){
console.log(model.toJSON(), 'model has successfully changed')
});
this.model.on('error', function(error){
console.log("server failed to acknowledge (server connection not made)")
});
this.model.on('sync', function(resp){
console.log("server successfull acknowledged (server connection made)")
});
this.model.save(
{
title:$("#addNew input:eq(0)").val(),
completed:$("#addNew input:eq(1)").val()
},
{
wait: true,
success: function (model, response) {
console.log("success");
#fires an change event if the model is updated
self.complete();
},
error: function (model, response) {
console.log("error");
self.complete();
}
}
);
},
complete: function(){
console.log("show this")
}

Collate Backbone collection success and errors

I have two functions that loop through all models in a Backbone collection, and save those that have been changed, or destroy those that have been selected for deletion. What I need to do is collate the success and errors, so that I can notify "X number of changes/deletions were successful" and/or "There was an error changing/saving X number of domains".
I've no experience with saving/destroying Backbone collections, only models; and can't find anything on the internet that shows how to do this.
The save and remove are called by events in the parent view.
The relevant code:
App.Views.SiteDomains = Backbone.View.extend({
el: '.site-domains',
initialize: function() {
this.collection.on('all',this.render, this);
},
render: function() {
$('.site-domains').empty();
this.collection.each( function(model)
{
var view = new App.Views.SiteDomain({model: model});
this.$('.site-domains').append(view.render().el);
return this;
});
},
saveDomainChanges: function() {
this.collection.each( function(model)
{
var ref = model.get('ref');
if ($('#' + ref).val() != model.get('domain')) {
$('.save-domains').prop('disabled', true);
var fields = $(this.el).find('form').serializeArray(), data = {};
$.each(fields, function(i, pair)
{
data[pair.name] = pair.value;
});
model.save(data, {
wait:true,
success: function(model, response, event)
{
// Pass each success to notification function
},
error: function(model, response, event)
{
// Pass each error to notification function
}
});
}
});
$('.save-domains').prop('disabled', false);
},
removeDomain: function() {
this.collection.each( function(model)
{
var ref = model.get('ref');
if ($('#remove-' + ref).prop('checked'))
{
model.destroy({
wait:true,
success:function() {
// Pass each success to notification function
},
error: function() {
// Pass each error to notification function
}
});
}
});
}
});
Many, many, many thanks in advance to anyone that can help with this! :)
You could use an event aggregator and create Model/View or just POJO for the notifications according to your app design. Something like this:
// Event aggregator
App.vent = _.extend({}, Backbone.Events);
// POJO for the notifications
App.notifications = {
var onCreateSuccess = function (model, response) {
...
};
var onCreateError = function (model, response) {
...
};
App.vent.on('sitedomain:create:success', onCreateSuccess);
App.vent.on('sitedomain:create:error', onCreateError);
};
// Add event triggering to corresponding callbacks
model.save(data, {
wait:true,
success: function(model, response, event) {
App.vent.trigger('createdomain:create:success', model, response);
},
error: function(model, response, event) {
App.vent.trigger('createdomain:create:error', model, response);
}
});

Backbone fetch doesn't work as expected

When I call fetch on my collection the app is calling the server and server returns an array of object. In the success function of the fetch call I've got an empty collection and the original response holding all objects that was responded by the server.
Collection
var OpenOrders = BaseCollection.extend({
model: Order,
url: baseUrl + '/api/orders?status=1'
});
Model
var Order = BaseModel.extend(
{
url:baseUrl + "/api/order",
defaults:{
order_items: new OrderList(),
location: 1,
remark: "remark"
},
initialize: function(options) {
var orderItems = this.get('order_items');
if (orderItems instanceof Array) {
orderItems = new OrderList(orderItems);
this.set({'order_items': orderItems})
}
orderItems.bind('change', _.bind(function() {
this.trigger('change')
}, this))
.bind('remove', _.bind(function() {
this.trigger('change')
}, this));
return this;
},
sum: function() {
return this.get('order_items').sum();
},
validate: function() {
return !!this.get('order_items').length;
},
add:function(product) {
this.get('order_items').add(product);
},
remove: function(product) {
this.get('order_items').remove(product);
}
);
Fetching the collection
this.collection.fetch({success:_.bind( function(collection, response){
console.log('OpenOrdersListView', collection.toJSON())
// logs []
console.log('OpenOrdersListView', response)
// logs [Object, Object ...]
}, this)})
Damm, its the validate method in my model. I've though validate have to return a boolean, but after reading the docs, it has to return an error message only if the model is not valid.
validate: function() {
if (!this.get('order_items').length){
return 'set minium of one product before save the order'
}
},

Resources