Setting my model with Backbone.js - javascript

I'm attempting to use Backbone.js to simplify data (JSON) management and interaction with DOM.
Firstly, I'm not sure if Backbone.js can indeed simplify and improve the current process, but I'd like to assume it can.
Previously I'm retrieving the data with jQuery AJAX function. Now, I'm retrieving the data(still with AJAX) Backbone style into the Backbone model.
For update, previously I was parsing through the JSON object itself to update data. I would then send back the updated json to the back-end (just as I've received it).
Now, is it possible to use the set function in Backbone to simplify something like the below and ideally where should the set attribute behaviour (and all other UI bindings like change events) be constructed? Would it be on the fetch() success handler, which is in the View initializer?
function setBucketOffer(bucketName, newId) {
var segments = json.segments;
for (var i = 0; i < segments.length; i++) {
if (segments[i].market.toLowerCase() === g_market) {
var genders = segments[i].gender;
for (var i = 0; i < genders.length; i++) {
if (genders[i].name.toLowerCase() === g_segment) {
var buckets = genders[i].buckets;
for (var i = 0; i < buckets.length; i++) {
if (buckets[i].name === bucketName) {
buckets[i].confirm = newId;
return;
}
}
}
}
}
}
}
Example JSON
{
"segments": [
{
"market": "Market1",
"gender": [
{
"name": "male",
"buckets": [
{
"name": "Market1_M_CBD",
"subscribers": "50,000",
"postcode": "20000-2010",
"lastsend": "13/03/12 4:30PM",
"suggest": "10054",
"confirm": ""
},
{
"name": "Market1_M_North",
"subscribers": "50,000",
"postcode": "20000-2010",
"lastsend": "13/03/12 4:30PM",
"suggest": "10054",
"confirm": ""
}
]
},
{
"name": "female",
"buckets": [
{
"name": "Market1_F_CBD",
"subscribers": "50,000",
"postcode": "20000-2010",
"lastsend": "13/03/12 4:30PM",
"suggest": "10054",
"confirm": "10054"
}
]
}
]
},
{
"market": "Market2",
"gender": [
{
"name": "male",
"buckets": [
{
"name": "Market2_M_CBD",
"subscribers": "50,000",
"postcode": "20000-2010",
"lastsend": "13/03/12 4:30PM",
"suggest": "10054",
"confirm": "10054"
},
{
"name": "Market2_M_North",
"subscribers": "50,000",
"postcode": "20000-2010",
"lastsend": "13/03/12 4:30PM",
"suggest": "10054",
"confirm": "10054"
},
{
"name": "Market2_M_South",
"subscribers": "50,000",
"postcode": "20000-2010",
"lastsend": "13/03/12 4:30PM",
"suggest": "10054",
"confirm": "10054"
}
]
}
]
}
]
}
Edit 1
From here, I'm trying to make good use of Parse and to get just segments from my JSON:
var Offers = Backbone.Collection.extend({
url: 'URL',
parse: function (response) {
return response.segments;
}
});
Here, I'm getting more than just response.segments. Also not sure if it's right for me to use the render function or fetch success function to populate the DOM. Suppose I have my html template in the DOM... I want to clone it using jQuery clone() and populate the clone using a forEach on segments, and push back all the clones into the html body. Is this workable in backbone, how would you do it? (I'm able to do this without backbone.js, but would like to see how I can improve with backbone.js, and bind all the data on the clones to model changes)
var OfferView = Backbone.View.extend({
initialize: function () {
this.model = new Offers();
this.model.fetch({
success: function (collection, response) {
console.log(response);
}
});
this.model.on('change', this.modelChange);
this.model.on('change', this.render);
this.modelChange = function () {
alert('model changed');
};
},
render: function () {
}
});
Edit 2
I'm up to creating individual views through a forEach but am having trouble inserting these back into the DOM. What am I doing wrong? (Not sure around the return this part)
// DEFINE VIEW
var OfferView = Backbone.View.extend({
initialize: function () {
this.model = new Offers();
this.model.fetch();
this.model.on('change', this.modelChange);
this.model.on('change', this.render);
this.modelChange = function () {
alert('model changed');
};
this.render();
},
render: function () {
var self = this;
this.model.forEach(function (s) {
var view = new OfferMarketView({
id: "container" + s.get('name').toLowerCase().replace(/\s*/g, '')
});
$('#leftCol').append(view.el);
});
return this;
}
});
var OfferMarketView = Backbone.View.extend({
tagName: "div",
className: "marketContainer",
events: {},
render: function() {
}
});

Whenever you call fetch on a model the response is passed through a parse method that can be defined in your model. parse takes one parameter, the ajax response:
parse: function(response) {
}
In that function you can do whatever you want with the data that comes back from your ajax request and eventually return that object. The object returned by the parse method will be set on your model.
For event binding, you'll want to do that in your view. In the initialize method of your view you can do something like:
this.collection.on("change", this.someFunction);
Now, any time something causes that model to trigger its change event someFunction ( also defined in your view ) will run.
EDIT
The sample json you added to the question looks to be pretty normalized. With that data, I'd be fetching it into a collection. If that's the structure you want your models to look like then you don't need to do much parsing.
in you collection file if you create a parse method that does the following:
parse: function(response) {
return response.segments;
}
When you call your fetch, this.collection.fetch() on a successful request, your collection will be filled with models that contain attributes in a structure that matches your response.
EDIT 2
Your binding looks ok.
in this section of code:
this.collection.fetch({
success: function (model, attributes) {
initAll(attributes);
// populate ui with attributes from model
}
})
The parameters that are passed back on a success in a collection fetch are (collection, response) collection is the result of collection call and what this.collection will end up being. response is the response of your ajax request.
I'm not sure what initAll(attributes) is supposed to be doing. If you add a parse method like I posted above, your collection will contain a set of models with the attributes of each segment.
Also, rather than calling this.render() at the end, you could do bind render to the change event:
this.collection.on('change', this.render);
That way any time your collection changes, that view will automatically render again so your changes will show up.

Related

Viewing in backbone data from RESTful service

I am trying to view the collection i import from the service, using backbone on the client side, and python/flask as the service. When i make the GET request, i get the following data back:
{"entries": [{"Title": "my title 1", "Description": "My desc 1", "Info": "Info 1", "ids": 1}, {"Title": "my title 2", "Description": "My desc 2", "Info": "Info 2", "ids": 2}]}
But those entries are not showing on my page, even though i use fetch. This is my ListView code:
var app = app || {};
app.ContactListView = Backbone.View.extend({
el: '#contacts',
initialize: function () {
this.collection = new app.ContactList();
this.collection.fetch({reset: true});
this.render();
this.listenTo( this.collection, 'reset', this.render );
},
render: function () {
this.collection.each(function( item ){
this.renderContact( item );
}, this );
},
renderContact: function ( item ) {
var contactView = new app.ContactView({
model: item
});
this.$('#ContactTable').append( contactView.render().el );
}
});
What might be the cause?
The reason is because your collection is expecting an array of models as it's response but your service is returning the array under entries. From the documentation
parse is called by Backbone whenever a collection's models are
returned by the server, in fetch. The function is passed the raw
response object, and should return the array of model attributes to be
added to the collection.
To work around that you can simply override your parse method to return the array of models.
For example:
app.ContactList = Backbone.Collection.extend({
//...
parse: function (response) {
return response.entries;
}
})

Mapping JSON to backbone.js collections

Alright, it looks like I need a hint to point me in the right direction. This question is two part - working with mult-dimensional JSON and Collections of Collections from JSON.
Background
I have some JSON that is going to be retrieved from a server and have control over how it could be formatted.
Multi-Dimentional JSON
I'm having some trouble being able connecting the model to the parts in the JSON. Say I wanted to render just each of the posts author name, and the content of status in the sample JSON below. I'm having no problem getting the status into the model, but the author name I'm a bit confused how to get to it. From my understanding I have to override the parse.
Is this bad standards / is there a better JSON structure I should use? Would it be better to keep it as flat as possible? That is move the author name and photo up one level?
I was reading How to build a Collection/Model from nested JSON with Backbone.js but it is still a little unclear to me.
Collection in Collections
Is there a nice way to make a collection within a collection for backbone.js? I will have a collection of posts, and then would have a collection of comments on that post. As I'm developing in backbone is that even possible?
From what I understand in Backbone.js Collection of Collections and Backbone.js Collection of Collections Issue, it would look something like this?
var Comments = Backbone.Model.extend({
defaults : {
_id : "",
text : "",
author : ""
}
})
var CommentsCollection = Backbone.Collection.extend({ model : Comments })
var Posts = Backbone.Model.extend({
defaults : {
_id : "",
author : "",
status : "",
comments : new CommentsCollection
}
})
var PostsCollection = Backbone.Collection.extend({ model : Posts })
Sample JSON
{
"posts" : [
{
"_id": "50f5f5d4014e045f000002",
"author": {
"name" : "Chris Crawford",
"photo" : "http://example.com/photo.jpg"
},
"status": "This is a sample message.",
"comments": [
{
"_id": "5160eacbe4b020ec56a46844",
"text": "This is the content of the comment.",
"author": "Bob Hope"
},
{
"_id": "5160eacbe4b020ec56a46845",
"text": "This is the content of the comment.",
"author": "Bob Hope"
},
{
...
}
]
},
{
"_id": "50f5f5d4014e045f000003",
"author": {
"name" : "Chris Crawford",
"photo" : "http://example.com/photo.jpg"
},
"status": "This is another sample message.",
"comments": [
{
"_id": "5160eacbe4b020ec56a46846",
"text": "This is the content of the comment.",
"author": "Bob Hope"
},
{
"_id": "5160eacbe4b020ec56a46847",
"text": "This is the content of the comment.",
"author": "Bob Hope"
},
{
...
}
]
},
{
...
}
]}
I appreciate even any hints to guild me. Thanks!
It can be overwhelming when trying to write up code to make it work for nested objects. But to make it simpler lets break it up into smaller manageable pieces.
I would think in these lines.
Collections
Posts
Comments
Models
Post
Comment
Author
Main collection -- Posts collection
(Which contains list of Post Models)
And each model in the Posts collection will have 3 sets of attributes(May not be the right term).
1st - level of attributes (status , id).
2nd - Author attribute which can be placed in a separate Model(Authod Model).
3rd - Collection of comments for each Post Model.
Collection in Collections would be a bit confusing here.
As you would have Models in Collection(Post Model inside Posts Collection) and each Model will nest a collection again(Comments collection inside Post Model). Basically you would be Handling a Collection inside a Model.
From my understanding I have to override the parse.
Is this bad standards / is there a better JSON structure I should use?
It is a perfectly plausible solution to handle the processing this in the Parse method. When you initialize a Collection or a Model , Parse methods is first called and then initialize is called. So it is perfectly logical to handle the logic inside the Parse method and it is not at all a bad standard.
Would it be better to keep it as flat as possible?
I don't think it is a good idea to keep this flat at a single level, as the other data is not required on the first level in the first place.
So the way I would approach this problem is write up the parse method in the Post Model which processes the response and attach the Author model and Comments collection directly on the Model instead as an attribute on the Model to keep the attributes hash clean consisting of 1st level of Post data. This I feel will be cleaner and lot more scalable on the long run.
var postsObject = [{
"_id": "50f5f5d4014e045f000002",
"author": {
"name": "Chris Crawford",
"photo": "http://example.com/photo.jpg"
},
"status": "This is a sample message.",
"comments": [{
"_id": "5160eacbe4b020ec56a46844",
"text": "This is the content of the comment.",
"author": "Bob Hope"
}, {
"_id": "5160eacbe4b020ec56a46845",
"text": "This is the content of the comment.",
"author": "Bob Hope"
}]
}, {
"_id": "50f5f5d4014e045f000003",
"author": {
"name": "Brown Robert",
"photo": "http://example.com/photo.jpg"
},
"status": "This is another sample message.",
"comments": [{
"_id": "5160eacbe4b020ec56a46846",
"text": "This is the content of the comment.",
"author": "Bob Hope"
}, {
"_id": "5160eacbe4b020ec56a46847",
"text": "This is the content of the comment.",
"author": "Bob Hope"
}]
}];
// Comment Model
var Comment = Backbone.Model.extend({
idAttribute: '_id',
defaults: {
text: "",
author: ""
}
});
// Comments collection
var Comments = Backbone.Collection.extend({
model: Comment
});
// Author Model
var Author = Backbone.Model.extend({
defaults: {
text: "",
author: ""
}
});
// Post Model
var Post = Backbone.Model.extend({
idAttribute: '_id',
defaults: {
author: "",
status: ""
},
parse: function (resp) {
// Create a Author model on the Post Model
this.author = new Author(resp.author || null, {
parse: true
});
// Delete from the response object as the data is
// alredy available on the model
delete resp.author;
// Create a comments objecton model
// that will hold the comments collection
this.comments = new Comments(resp.comments || null, {
parse: true
});
// Delete from the response object as the data is
// alredy available on the model
delete resp.comments;
// return the response object
return resp;
}
})
// Posts Collection
var Posts = Backbone.Collection.extend({
model: Post
});
var PostsListView = Backbone.View.extend({
el: "#container",
renderPostView: function(post) {
// Create a new postView
var postView = new PostView({
model : post
});
// Append it to the container
this.$el.append(postView.el);
postView.render();
},
render: function () {
var thisView = this;
// Iterate over each post Model
_.each(this.collection.models, function (post) {
// Call the renderPostView method
thisView.renderPostView(post);
});
}
});
var PostView = Backbone.View.extend({
className: "post",
template: _.template($("#post-template").html()),
renderComments: function() {
var commentsListView = new CommentsListView({
// Comments collection on the Post Model
collection : this.model.comments,
// Pass the container to which it is to be appended
el : $('.comments', this.$el)
});
commentsListView.render();
},
render: function () {
this.$el.empty();
// Extend the object toi contain both Post attributes
// and also the author attributes
this.$el.append(this.template(_.extend(this.model.toJSON(),
this.model.author.toJSON()
)));
// Render the comments for each Post
this.renderComments();
}
});
var CommentsListView = Backbone.View.extend({
renderCommentView: function(comment) {
// Create a new CommentView
var commentView = new CommentView({
model : comment
});
// Append it to the comments ul that is part
// of the view
this.$el.append(commentView.el);
commentView.render();
},
render: function () {
var thisView = this;
// Iterate over each Comment Model
_.each(this.collection.models, function (comment) {
// Call the renderCommentView method
thisView.renderCommentView(comment);
});
}
});
var CommentView = Backbone.View.extend({
tagName: "li",
className: "comment",
template: _.template($("#comment-template").html()),
render: function () {
this.$el.empty();
this.$el.append(this.template(this.model.toJSON()));
}
});
// Create a posts collection
var posts = new Posts(postsObject, {parse: true});
// Pass it to the PostsListView
var postsListView = new PostsListView({
collection: posts
});
// Render the view
postsListView.render();
Check Fiddle
(Edited to correct my initial misreading of the question.)
There's no need to override parse method of the model unless you want to change its structure. But it sounds like you don't need to -- to render the author name, just use author.name in the view:
<%= author.name %>
As far as initializing the nested collection, your approach is exactly right. All you have to do is convert the JSON object to Backbone models, and pass them to the PostsCollection (the Backbone.Collection constructor accepts an array of Backbone models, not raw JSON). One way of doing this is to use map:
var postModels = json.posts.map(function(post) { return new Posts(post); });
var posts = new PostsCollection(postModels);
Note that you'll need to do something similar in the initialize method of the Posts model -- retrieve the comments JSON array, and convert it to an array of Comments models:
initialize: function() {
if (attributes.comments && attributes.comments.length > 0) {
var commentModels = attributes.comments.map(function(comment) {
return new Comments(comment);
});
this.set("comments", new CommentsCollection(commentModels));
}
}
Here is working example.
Update, I found a SuperModel for backbone which provides relationships between models and between collections. It has proved to be a great solution for Collections within Collections as well as Deep Nested Model data.
Models are pre-defined with their relationships to other models via key. During the initialize/parse of the model any values in the JSON at that key gets passed off to a new related model or collection. A relationship is created between the two models/collections.
This means with the above example we can do something like this with our models:
Setup
var Author = Supermodel.Model.extend({});
var Post = Supermodel.Model.extend({});
var Comment = Supermodel.Model.extend({});
var Posts = Backbone.Collection.extend({
model: function(attrs, options) {
return Post.create(attrs, options);
}
});
var Comments = Backbone.Collection.extend({
model: function(attrs, options) {
return Comment.create(attrs, options);
}
});
Post.has().one('author', {
model: Author,
inverse: 'post'
}).many('comments', {
collection: Comments,
inverse: 'post'
});
//reverse relationships could also be setup
Usage
var posts = new Posts( postsObject ); //where postsObject is an array of posts
//With SuperModel, we are able to navigate the related models
posts.first().comments();
posts.first().comments().author();
posts.last().author();
Fiddle
Working Example in JSFiddle

Backbone.js collection fetch, can't retrieve items

I'm new to Backbone.js, I'm following a tutorial trying to adapt it to my needs.
I'm calling the fetch method from the main app view in order to retrieve multiple objects to be inserted in the collection.
I can see in chrome that the json data are returned, the fetch functions returns success but the collection is not populated.
I'm using IcanHaz for rendering. It prints out only the default model as I defined it in the Job model.
var Job = Backbone.Model.extend({
defaults: {
title: 'Not specified',
status: 0,
created_at: 'Not specified'
}
});
var JobView = Backbone.View.extend({
render: function () {
// template with ICanHaz.js (ich)
this.el = ich.jobRowTpl(this.model.toJSON());
return this;
}
});
// define the collection of jobs
var JobCollection = Backbone.Collection.extend({
model: Job,
url: '/api/1.0/jobs/'
});
// main app, the collection view
var AppView = Backbone.View.extend({
tagName: 'tbody',
initialize: function() {
this.jobs = new JobCollection();
this.jobs.on('all', this.render, this);
this.jobs.fetch({
error: function () {
console.log("error!!");
},
success: function () {
console.log("no error");
}
}).complete(function () {
console.log('done');
console.log('length1:' + this.jobs.length);
});
console.log('length2: '+ this.jobs.length);
},
render: function () {
this.jobs.each(function (job) {
var tmpjob = new JobView({model: job}).render().el;
$(this.el).append(tmpjob);
console.log('job': + this.tmpjob);
}, this);
return this;
}
});
var app = new AppView();
$('#app').append(app.render().el);
In the Chrome console I get this:
length2:0
job:undefined
no error
done
Uncaught TypeError: Cannot read property 'length' of undefined //referring to the lenght1 logging
These are the data that I fetch, from the chrome inspector under network/xhr/response:
{"count": 2, "next": null, "previous": null, "results": [{"id": 1, "title": "testAlbum", "status": 0, "created_at": "2012-12-31"}, {"id": 2, "title": "testAlbum2", "status": 0, "created_at": "2012-12-31"}]}
I don't understand why the 'jobs' collection exists after calling the fetch method but it is undefined inside the fetch block when the 'success' helper has been called.
And why the collection is not being populated despite it returns success and the json data are returned from the server?
I'm quite lost.
Add a parse method to your collection that just returns the results arrays. A collection needs to be an array of models, not your whole JSON response.
The Backbone docs explain how to use parse.

Iteration in handlebar using backbone

I'm using backbone and handlebars for templating and i'm new to this.
My current json is in the below format and the code works fine.
[
{
"id": "10",
"info": {
"name": "data10"
}
},
{
"id": "11",
"info": {
"name": "data11"
}
}
]
But when i change my json structure to something like shown below i'm having difficulty in getting things to be populated.
{
"total_count": "10",
"dataElements": [
{
"id": "10",
"info": {
"name": "data10"
}
},
{
"id": "11",
"info": {
"name": "data11"
}
}
]
}
How can i populate name, info and total_count keeping the current code structure ?
JSFiddle : http://jsfiddle.net/KTj2K/1/
Any help really appriciated.
A few things that you need to do in order for this to work.
Replace Backbone's core 'reset' on your collection with a custom one that understands the data you are passing to it. For example:
reset: function (data) {
this.totalCount = data.total_count;
Backbone.Collection.prototype.reset.call(this, data.dataElements);
}
Now when you reset your collection, it will pull the total_count out of the object you are resetting it with, and use Backbone's core reset with the dataElement array. Keep in mind you may have to do a similar thing with 'parse' if you're intending on pulling this from the server.
I'd recommend that (if your example looks anything like the real code you're working with) you reset your collection before getting to rendering.
var dataCollectionList = new dataCollection();
dataCollectionList.reset(jsonData);
var App = new AppView({model : dataCollectionList});
Now in your view's "render" method you can grab the 'totalCount' property off the collection -
render : function() {
//Should spit the total count into the element, just as an example
this.$el.append(this.model.totalCount);
//or console.log it
console.log(this.model.totalCount);
return this;
}
Voila. Side note - as someone who works with Backbone a lot, it drives me nuts when people set an attribute of something like "model" (i.e. peopleModel, itemModel, etc) and it ends up being a backbone collection. It's much clearer to name it after what it is - though some MVC purists may disagree a bit.
Also, in this code block:
_.each(this.model.models, function (myData) {
$(this.el).append(new ItemView({model:myData}).render().el);
}, this);
You don't need to do _.each(this.model.models.......). Since you're working with a collection, the collection has a built in 'each' method.
this.model.each(function (myData) { ..... } , this);
Quite a bit cleaner.

Backbone.js Collections do not invoke "Reset" event after fetch operation

When requesting for data.json file for populating collection which has below data
[{
"Id": "BVwi1",
"Name": "Bag It",
"AverageRating": 4.6,
"ReleaseYear": 2010,
"Url": "http://www.netflix.com/Movie/Bag_It/70153545",
"Rating": "NR"
}, {
"Id": "BW1Ss",
"Name": "Lost Boy: The Next Chapter",
"AverageRating": 4.6,
"ReleaseYear": 2009,
"Url": "http://www.netflix.com/Movie/Lost_Boy_The_Next_Chapter/70171826",
"Rating": "NR"
}]
Collection does not invoke the "Reset" event as the documentation says it should. I can view the request and response are correct after the fetch method but nothing happens. Below is the code for my app.
Router that start's everything
Theater.Router = Backbone.Router.extend({
routes: {
"": "defaultRoute"
},
defaultRoute: function () {
Theater.movies = new Theater.Collections.Movies()
new Theater.Views.Movies({
collection: Theater.movies
});
Theater.movies.fetch();
}
})
var appRouter = new Theater.Router();
Backbone.history.start();
the Collection
Theater.Collections.Movies = Backbone.Collection.extend({
model: Theater.Models.Movie,
url: "scripts/data/data.json",
initialize: function () {}
});
View that subscribes to the reset event
Theater.Views.Movies = Backbone.View.extend({
initialize: function () {
_.bindAll(this, "render", "addOne");
this.collection.bind("reset", this.render);
this.collection.bind("add", this.addOne);
},
render: function(){
console.log("render")
console.log(this.collection.length);
},
addOne: function (model) {
console.log("addOne")
}
})
Reference Site
http://bardevblog.wordpress.com/2012/01/16/understanding-backbone-js-simple-example/
You should tell Backbone to fire the reset on fetch by passing {reset: true} when fetching as of Backbone 1.0
Replace :
Theater.movies.fetch()
With
Theater.movies.fetch({reset :true})
I had a similar issue, I hope my reply will be of any use to others. At first my data.json file was not valid. Then it turned out that I overlooked the following line of code:
Theater.Models.Movie = Backbone.Model.extend({}
Adding this line of code resolved the issue for me.
There might be a problem with your fetch if the collection is not being populated. See this answer to see how to pass an error handler into the fetch operation.
I have the same iusse.. and fixed it by:
youColloection.fetch({reset: true});

Categories

Resources