backbone.js - Overridden parse() does not set model attributes - backbone.js

I have a model which links to two sub-models like so:
var SubModel = Backbone.Model.extend({
defaults: {
headline: null,
image_url: null,
url: null
}
});
var MainModel = Backbone.Model.extend({
defaults: {
subModelA: null,
subModelB: null,
title: null
},
urlRoot: function() {
if (this.isNew()) {
return '/mainmodel/new';
}
return '/mainmodel';
},
initialize: function() {
this.fetch();
},
parse: function(data) {
var response = {};
response.subModelA = new SubModel(data.subModelA);
response.subModelB = new SubModel(data.subModelB);
response.title = data.title;
return response;
}
});
The issue I'm currently having is that calling var mainModelInstance = new MainModel() does correctly fetch from /mainmodel/new but mainModelInstance.attributes is always a blank object {}.
var mainModelInstance = new MainModel();
mainModelInstance.attributes; // Returns {}
Here is a sample of the server's response to /mainmodel/new:
{
"title": "Politics",
"subModelA": {
"headline": "Investigators: Iran tried to smuggle suicide belts, missiles by boat into Yemen",
"url": "http://dailycaller.com/2013/02/09/yemen-minister-says-weapons-came-from-iran/",
"image_url": "http://cdn01.dailycaller.com/wp-content/uploads/2013/02/54c7d52e1a384db489ab9ea568afddb0-e1360455589316.jpg"
},
"subModelB": {
"headline": "Review: Who needs Windows RT? Acer's Iconia W510 runs the real thing",
"url": "http://arstechnica.com/gadgets/2013/02/review-who-needs-windows-rt-acers-iconia-w510-runs-the-real-thing/",
"image_url": "http://cdn.arstechnica.net/wp-content/uploads/2013/02/w510-main-640x388.jpg"
}
}
It seems as though the model's attributes are not being updated via parse. Why aren't the model's attributes being updated?

Your code might as well be working, but you are not testing it correctly
You are calling this.fetch in yout initialize method.
calling model.fetch is an asynchronous call and when you are trying to evaluate mainModelInstance.attributes, the http request call is not yet completed.
You should test this with:
var mainModelInstance = new MainModel();
mainModelInstance.on('change', function() {
console.log(mainModelInstance.toJSON());
});
or even better, dont auto fetch on initialize (its not a best practice anyway)
and use the jQuery promise pattern:
var mainModelInstance = new MainModel();
mainModelInstance.fetch().done(function () {
console.log(mainModelInstance.toJSON());
});

This is just a work in progress answer, feel free to discuss in the comments.
I would change your MainModel definition like so:
subModelA: new SubModelA(),
subModelB: new SubModelB(),
parse: function(data){
this.subModelA.set(data.subModelA);
this.subModelB.set(data.subModelB);
return data; // we keep two copies of the data, in mainModel and submodels.
}
So assuming your server responds exactly like in your answer
var model = new MainModel();
model.get('title'); // Politics
model.subModelA.get('headline'); // Investigators: Iran tr...
Then, you may have to override the save method depending on how you wish to persist things back to the server — this might work.
save: function(key, val, options) {
this.set({
subModelA: this.subModelA.toJSON(),
subModelB: this.subModelB.toJSON()
});
Backbone.Model.prototype.save.apply(this, arguments);
}

First, default attributes values must be put into the defaults option, like so:
var SubModel = Backbone.Model.extend({
defaults: {
headline: null,
image_url: null,
url: null
}
});
Then, you'll have value to save to the server when creating a new instance. That will also fill up your mainModelInstance.attributes hash.
For the parsing problem, have you logged (console.log) in what you get back from the server?

Related

implicit load() in sencha touch store

I have a sencha touch store defined simply by a model and autoLoad=false. The model has only fields in its config, so I guess the data is stored within the memory, global to the application, am I right? (I'm new to sencha)
Ext.define('App.store.DataSession', {
extend: 'Ext.data.Store',
requires: ['App.model.DataSession'],
config: {
model: 'App.model.DataSession',
autoLoad: false
}
});
My question is that I'm seeing a code that doesn't load the data from the store explicitly, even though the store has autoLoad false
var storesession = Ext.getStore('DataSession');
var datasession = storesession.getAt(0);
if(datasession.get('state') == "1"){
....
does the getStore() function load the data implicitly? what are the implications of not using load()?
getStore() gets you a reference to the store.
If you want to load the data you have to e.g.:
var storesession = Ext.getStore('DataSession');
storesession.load({
params: {
id: 1 // some parameters
},
callback: function(records, operation, success) {
if(success) {
var datasession = storesession.getAt(0);
if(datasession.get('state') == "1") {
....
}
}
});
Have a look here: https://docs.sencha.com/extjs/7.0.0/modern/Ext.data.Store.html

Backbone pass specific models in the collection to view?

var theseMessages = window.App.data.messages.where({ chat_id: 2 })
this.messagesView = new MessagesView({ collection: theseMessages });
I am not sure the proper way to pass in to the view constructor the collection that has a specific property.
Example my collection may look like
[
{
"chat_id": "1",
"facebook_name": "Tim Parker",
"facebook_email": "tim#aol.com",
"facebook_id": "1663565265",
"gravatar": null,
"message": "Whats up",
"_id": "533b6a4c7cc6647012f441ad",
"__v": 0,
"sent": "2014-04-02T01:39:24.747Z"
},
{
"chat_id": "2",
"facebook_name": "nick matthews",
"facebook_email": "john#aol.com",
"facebook_id": "1663565265",
"gravatar": null,
"message": "Hey admin",
"_id": "434636",
"__v": 0,
"sent": "2014-04-02T01:48:45.585Z"
}
]
So when I want to render this collection in my app and only pass the models that have chat_id : 2 I would think something like this is sufficient?
var theseMessages = window.App.data.messages.where({ chat_id: 2 })
this.messagesView = new MessagesView({ collection: theseMessages });
But it gives me an error any ideas?
Edit: Should have been more specific from the get go, I am using a marionette collection view. So I need to pass a collection, but there has to be a way to limit the collection to items with specific attributes, maybe I need to change my logic?
var Marionette = require('backbone.marionette');
var MessageView = Marionette.ItemView.extend({
className: 'message row',
template: require('../../templates/message.hbs')
});
module.exports = CollectionView = Marionette.CollectionView.extend({
className: 'collection',
initialize: function() {
this.listenTo(this.collection, 'change', this.render);
},
itemView: MessageView
});
I think you meant to do this:
this.messagesView = new MessagesView({ model: theseMessages });
In your line where you filter the Collection:
var theseMessages = window.App.data.messages.where({ chat_id: 2 })
the variable theseMessages just holds an array of items from the Collection, not another Collection itself -- that's probably why you're getting an error in MessagesView.
I would do the filtering dynamically in either the Collection or in MessagesView itself when rendering, rather than trying to pass an already-filtered Collection to MessagesView's constructor. Most of the time, I'd probably do it in MessageView, since I tend to think of filtering as a view-specific thing. But filtering in the Collection would be fine if that Collection isn't going to be used in another view at the same time.
Either way, here's some example code. To filter in the MessageView:
// within MessageView
serializeData: function() {
var data = Marionette.CollectionView.prototype.serializeData.call(this);
return _.where(data, { chat_id: 2 });
}
Or to filter in your Collection instead:
// within your Collection
toJSON: function() {
return this.chain()
.where({ chat_id: 2 })
.map(function(model){ return model.toJSON(); }
.value();
}
I haven't tested that code, but something like that should work.
Incidentally, to clarify the collection: vs model: subject that was brought up in other comments here: you are correct in using the collection: option in the view's constructor. Backbone & Marionette want a collection: for a Collection, and a model: for a single Model.

Handling Subsidiary Views in Backbone.js

I have a basic Backbone application which obtain an array of JSON objects from a remote service and displays them: all good so far. However, each JSON object has an array of tags and I want to display the tags in a separate area of the webpage.
My question is: what is the most Backbone-friendly way of doing this? I could parse the existing data again in a second view, which is cleaner but takes up more computation (processing the entire array twice).
An alternative is gathering up the tag information in the primary view as it is working through the array and then passing it along to the subsidiary view, but then I'm linking the views together.
Finally, I'd like to filter based on those tags (so the tags will become toggle buttons and turning those buttons on/off will filter the information in the primary view); does this make any difference to how this should be laid out?
Bonus points for code snippets.
Hm. I'm not sure if this is the Backbone-friendly way, but I'll put the logic to retrieve a list of tags (I think that's what you meant by "parse") in the collection.
Both the main view and the subview will "listen" to the same collection, and the subview will just call collection.getTags() to get a list of tags it needs.
// Model that represents the list data
var ListDataModel = Backbone.Model.extend({
defaults: function() {
return {
name: null,
tags: []
};
}
});
// Collection of list data
var ListDataCollection = Backbone.Collection.extend({
model: ListDataModel,
initialize: function() {
var me = this;
// Expires tag collection on reset/change
this.on('reset', this.expireTagCache, this);
this.on('change', this.expireTagCache, this);
},
/**
* Expires tag cache
* #private
*/
expireTagCache: function() {
this._cachedTags = null;
},
/**
* Retrieves an array of tags in collection
*
* #return {Array}
*/
getTags: function() {
if (this._cachedTags === null) {
this._cachedTags = _.union.apply(this, this.pluck('tags'));
}
return this._cachedTags;
},
sync: function(method, model, options) {
if (method === 'read') {
var me = this;
// Make an XHR request to get data for this demo
Backbone.ajax({
url: '/echo/json/',
method: 'POST',
data: {
// Feed mock data into JSFiddle's mock XHR response
json: JSON.stringify([
{ id: 1, name: 'one', tags: [ 'number', 'first', 'odd' ] },
{ id: 2, name: 'two', tags: [ 'number', 'even' ] },
{ id: 3, name: 'a', tags: [ 'alphabet', 'first' ] }
]),
},
success: function(resp) {
options.success(me, resp, options);
},
error: function() {
if (options.error) {
options.error();
}
}
});
}
else {
// Call the default sync method for other sync method
Backbone.Collection.prototype.sync.apply(this, arguments);
}
}
});
var listColl = new ListDataCollection();
listColl.fetch({
success: function() {
console.log(listColl.getTags());
}
});
I guess two reasons for handling this in the collection:
It keeps the View code cleaner (This is given that we are not doing very complex logic in the tag extraction - It's just a simple _.pluck() and _.union().
It has 0 business logic involved - It can arguably belong to the data layer.
To address the performance issue:
It does go through the collection twice - However, if the amont of data you are consuming is too much for the client to process even in this case, you may want to consider asking the Backend to provide an API endpoint for this. (Even 500 pieces of data with a total of 1000 tags shouldn't bee too much for a somewhat modern browser to handle nowadays.)
Hmm. Does this help?
JSFiddle to go with this with the collection and the model: http://jsfiddle.net/dashk/G8LaB/ (And, a log statement to demonstrate the result of .getTags()).

backbone.js a url property must be defined

I'm trying to use Backbone.localStorage as a backend to an app.
I wrote a Model called Era, and a corresponding EraView. Pressing enter in the EraView causes the model to be saved, and this is where I get the error:
Uncaught Error: A "url" property or function must be specified
urlError backbone.js:1509
_.extend.url backbone.js:515
_.result underscore-min.js:1060
Backbone.sync backbone.js:1410
Backbone.sync backbone.localStorage.js:188
_.extend.sync backbone.js:276
_.extend.save backbone.js:476
karass.EraView.Backbone.View.extend.close era.js:61
karass.EraView.Backbone.View.extend.updateOnEnter era.js:75
Here is the code to the EraView
var karass = karass || {};
// Era Item View
// the DOM element for an era item
karass.EraView = Backbone.View.extend({
tagName: 'li',
className: 'era',
template: _.template( $('#era-template').html() ),
// The DOM events specified to an item
events: {
'dblclick .edit-input': 'edit',
'keypress .edit-input': 'updateOnEnter',
//'blur .edit': 'close',
},
// The EraView listens for changes to its model, re-rendering. Since there's
// a one-to-one correspondence between an era and a EraView in this karass,
// we set a direct reference on the model for convenience.
initialize: function(){
_.bindAll(this);
this.model.on('change', this.render, this);
},
// Re-renders the era item to the current state of the model and
// updates the reference to the era's edit input within the view
render: function(){
this.$el.html( this.template(this.model.attributes));
this.$era_start = this.$('.era-start');
this.$era_end = this.$('.era-end');
this.$era_start.attr('disabled', true);
this.$era_end.attr('disabled', true);
return this;
},
// Switch this view into editing mode, displaying the input field
edit: function(){
this.$('.edit-input').removeAttr('disabled');
this.$el.addClass('editing');
this.$('.edit-input').addClass('editing');
},
// Close the editing mode, saving changes to the era
close: function(){
this.$('.edit-input').attr('disabled', true);
var start = this.$era_start.val().trim();
var end = this.$era_end.val().trim();
if(start && end){
this.model.save({from: start, until: end});
}
this.$el.removeClass('editing');
this.$('.edit-input').removeClass('editing');
this.trigger('close');
},
updateOnEnter: function(e){
if(e.which !== ENTER_KEY && (!this.$era_start.val().trim() || !this.$era_end.val().trim())){
return;
}
this.close();
}
});
And this is the code for the era model:
var karass = karass || {};
karass.Era = Backbone.Model.extend({
defaults: {
from: '',
until: ''
},
});
I thought I didn't need a url while using localStorage.
Edit: I forgot to mention that while this behavior occurs in the Era/EraView itself, it also occurs in the Name model, which extends Era. Name in turn belongs in a Names collection. I don't know if this makes a difference, but I figured I add it.
Edit 2: The Names collection looks like this:
karass.Names = History.extend({
model: karass.Name,
localStorage: new Backbone.LocalStorage('karass-names'),
});
Edit 3: I posted all the code on jsfiddle: http://jsfiddle.net/herrturtur/CRv6h/
You don't need an url while using localStorage. But you need to set the localStorage property on your model or on your collection (if you set the localStorage on a collection the models inside the collection will "inherit" this setting):
karass.Era = Backbone.Model.extend({
localStorage: new Backbone.LocalStorage("EraCollection"),
// the EraCollection should be an unique name within your app.
defaults: {
from: '',
until: ''
},
});
If you don't setup the localStorage property the plugin falls back to the default ajax sync so you get the uri missing exception.

Backbone-relational fetchRelated not sending request

I'm using backbone.js and backbone relational 0.5.0 with a Rails 3.2 backend. I have a Card model which has_many Notes.
Here are my JS models and collections:
Workflow.Collections.Cards = Backbone.Collection.extend({
model: Workflow.Models.Card,
url: '/cards'
});
Workflow.Models.Card = Backbone.RelationalModel.extend({
modelName : 'card',
urlRoot : '/cards',
relations: [
{
type: Backbone.HasMany,
key: 'notes',
relatedModel: 'Workflow.Models.Note',
collectionType: 'Workflow.Collections.Notes',
includeInJSON: false,
reverseRelation: {
key: 'card',
includeInJSON: 'id'
}
}]
});
Workflow.Collections.Notes = Backbone.Collection.extend({
model: Workflow.Models.Note,
url: '/cards/74/notes' // intentionally hard-coded for now
});
Workflow.Models.Note = Backbone.RelationalModel.extend({
modelName : 'note',
urlRoot : '/notes'
});
Normal fetching works great, but when I try fetchRelated in the console, I get an empty array:
card = new Workflow.Models.Card({id: 74}) // cool
card.fetch() // hits the sever with GET "/cards/74" - works great
card.fetchRelated('notes') // [] - didn't even try to hit the server
What's weird is that this works:
card.get('notes').fetch() // cool - GET "/cards/74/notes"
I could use that method and parse the response text, but it feels really dirty.
Anyone know what I'm missing here?
Thanks in advance, this one is really torturing me!
Stu
You should create Card with Note ids array: card = new Workflow.Models.Card({id: 74, notes: [74, 75]}); and change the url method of Notes accordingly:
Workflow.Collections.Notes = Backbone.Collection.extend({
model: Workflow.Models.Note
});
Workflow.Models.Note = Backbone.RelationalModel.extend({
modelName : 'note',
urlRoot : function () {
return this.get('card').url() + '/notes';
}
});
card = new Workflow.Models.Card({id: 74, notes: [74, 75]});
card.fetchRelated('notes');
http://jsfiddle.net/theotheo/5DAzx/
I should have posted my solution a while back - there might well be a better way, but this is the convention I've gone with:
All of the following code is in the card view (which is where the notes are displayed).
First, I bind a renderNotes method to the 'reset' event on the card's notes collection:
initialize: function () {
_.bindAll(this);
this.model.get('notes').on('reset', this.renderNotes);
var self = this;
this.model.get('notes').on('add', function(addedNote, relatedCollection) {
self.renderNote(addedNote);
});
}
I also bind to the 'add' on that collection to call a singular renderNote.
The renderNotes and renderNote methods work like this:
renderNotes: function () {
if (this.model.get('notes')) {
this.model.get('notes').each(this.renderNote);
}
},
renderNote: function (note) {
var noteView = new Workflow.Views.Note({ model: note });
this.$('.notes').append(noteView.render().el);
},
Then, the last piece of the puzzle is to actually hit the server up for the card's notes (which will in turn fire the 'reset' event I bound to above). I do this in the card view's render method:
render: function () {
// render all of the eager-loaded things
this.model.get('notes').fetch();
return this;
},
As #user1248256 kindly helped me work out in the comments on my OP, the confusion was mainly in that I expected fetchRelated to pull down lazy-loaded records - that's actually not the case.
As a side-note, this view is actually a modal and be opened and closed (removed from the page). To prevent the zombie events problem described in this excellent post, I also manually unbind the events mentioned above.

Resources