Backbone displaying nested json collections - javascript

Not sure where exactly I am going wrong. I cant seem to get the templates to render the title attributes within my nested json. I was following a tutorial online that was sort of holding my hand through this but I hit a brick wall and cant figure out why it wont render. Any help would be greatly appreciated.
Here is my templates and html.
<script id="albumTemplate" type="text/template">
<p><%= title %></p>
</script>
<script id="subalbumTemplate" type="text/template">
<p><%= title %></p>
</script>
<div class="row">
<div class="six columns"><h5>Albums</h5></div>
<div class="six columns"><h5>Sub-Albums</h5></div>
</div>
<div class="row" style="color: #333;">
<div class="six columns" id="categories"></div>
<div class="six columns" id="sub-cat"></div>
</div>
Here is my app.js
app.Subalbum = Backbone.Model.extend({
initialize: function () {
this.subId = this.get('id');
this.subTitle = this.get('title');
this.subImg = this.get('image');
this.subCanvas = this.get('canvas');
this.subSize = this.get('size');
}
});
app.Subalbums = Backbone.Collection.extend({
model: app.Subalbum
});
app.Album = Backbone.Model.extend({
initialize: function () {
this.subs = new app.Subalbums(this.get('subalbum'));
this.subs.parent = this;
this.albumId = this.get('id');
this.albumTitle = this.get('title');
this.albumImg = this.get('image');
}
});
app.Albums = Backbone.Collection.extend({
model: app.Album,
url: 'albums.json',
parse: function (data) {
return data;
}
});
app.AlbumCollectionView = Backbone.View.extend({
el: $("#categories"),
initialize: function () {
_.bindAll(this, 'render');
this.model.on('reset', function () {
this.render();
}, this);
},
render: function (event) {
_.each(this.model.models, function (album) {
//console.log(album.subs);
$(this.el).append(new app.AlbumView({
model: album
}).render().el);
}, this);
return this;
}
});
app.AlbumView = Backbone.View.extend({
template: _.template($("#albumTemplate").html()),
initialize: function () {
_.bindAll(this, 'render');
// Subalbum View should be instantiated and called from inside the initialize function of the Parent View
this.subView = new app.SubalbumView({
model: this.model.subs
});
this.subView.parentView = this; // this assignment connects the child view to the parent view
$("#sub-cat").append(this.subView.render().el); // subView should "return this" from child render() function
},
render: function () {
//console.log(this.model.subs);
//$(this.el).html("<p>" + this.model.get("title") + "</p>");
$(this.el).append(this.template(this.model.toJSON()));
return this;
}
});
app.SubalbumView = Backbone.View.extend({
template: _.template($("#subalbumTemplate").html()),
initialize: function () {
_.bindAll(this, 'render');
this.model.on('reset', function () {
this.render();
}, this);
},
render: function (event) {
_.each(this.model.models, function (subalbum) {
$(this.el).append("<p>" + subalbum.get("title") + "</p>");
//$(this.el).html(this.template(subalbum.toJSON()));
}, this);
return this;
}
});
app.AlbumRouter = Backbone.Router.extend({
routes: {
"": "indexRoute"
},
indexRoute: function () {
this.albumList = new app.Albums();
this.albumList.fetch();
this.albumAppView = new app.AlbumCollectionView({
model: this.albumList
});
}
});
var albumRoute = new app.AlbumRouter();
Backbone.history.start();
Here is the albums.json file structure.
[
{
"pid":0,
"title":"Blues Singer",
"image":"blues_singer.jpg",
"subalbum":[
{
"pid":0,
"title":"Another Realm",
"image":"another_realm.jpg"
},
{
"pid":1,
"title":"Ascendant",
"image":"ascendant.jpg"
},
{
"pid":2,
"title":"Ascent",
"image":"ascent.jpg"
}
]
},
{
"pid":1,
"title":"Destiny",
"image":"destiny.jpg",
"subalbum":[
{
"pid":0,
"title":"Cathedral of Trees",
"image":"cathedral_of_trees.jpg"
},
{
"pid":1,
"title":"Come Up Here",
"image":"come_up_here.jpg"
},
{
"pid":2,
"title":"Crystal Forest",
"image":"crystal_forest.jpg"
}
]
},
{
"pid":2,
"title":"Eagle",
"image":"eagle.jpg",
"subalbum":[
{
"pid":0,
"title":"Curved Road",
"image":"curved_road.jpg"
},
{
"pid":1,
"title":"Dawn Breaking",
"image":"dawn_breaking.jpg"
},
{
"pid":2,
"title":"Dawn",
"image":"dawn.jpg"
}
]
},
{
"pid":3,
"title":"Evening Harvest",
"image":"evening_harvest.jpg",
"subalbum":[
{
"pid":0,
"title":"Destiny",
"image":"destiny.jpg"
},
{
"pid":1,
"title":"Destiny2",
"image":"destiny2.jpg"
},
{
"pid":2,
"title":"Eagle Rising",
"image":"eagle_rising.jpg"
}
]
}
]

The problem is you wait for the reset event from this.albumList.fetch(), however the reset isn't triggered by default, so you need to do pass {reset:true} to fetch. Here is a JSFIDDLE.
indexRoute: function () {
this.albumList = new app.Albums();
// This view will render when the model's reset event is triggered.
// Since albumList is a collection, it should identified as such.
this.albumAppView = new app.AlbumCollectionView({
// albumList is a collection and it should identified as such.
// instead of model:this.albumList - now within albumAppView, you will have
// access to the collection via this.collection instead of this.model
collection: this.albumList
});
this.albumList.fetch({reset:true});
}
Some side suggestions (I'm going to pick on your AlbumCollectionView) but the same goes for your other views as well:
Instead of el: $("#categories") you can just use a string el:'#categories'
In initialize, You are using this.model.on when you could take advantage of listenTo. The main advantage of using listenTo is when call remove on a view, the event listeners are cleaned up for you.
// this works.
this.collection.on('reset', function () {
this.render();
}, this);
// can be written as (remember the we changed model to collection above).
this.listenTo(this.collection,'reset',this.render);
Moving onto your render function, a Backbone.Collection has a slew of underscore methods attached to them.
// while this works
_.each(this.collection.models, function (album) { ... });
// can be written as:
this.collection.each(function(album) { ... });
});
$(this.el), is sort of dated, you can use this.$el, which is just a cached jQuery object for the view's element. $el documentation.
So when we put it all together we wind up with:
app.AlbumCollectionView = Backbone.View.extend({
el: '#categories',
initialize: function () {
this.listenTo(this.collection,'reset', this.render);
},
render: function () {
this.collection.each(function (album) {
var albumView = new app.AlbumView({model: album});
this.$el.append(albumView.render().el);
}, this);
return this;
}
});

Related

Model method error while trying to navigate

I have several Backbone Models rendered in a Collection View, and also I have a route that should render a view of that model. So, here come the views
resume.js
// this renders a single model for a collection view
var ResumeView = Backbone.View.extend({
model: new Resume(),
initialize: function () {
this.template = _.template($('#resume').html());
},
render: function () {
this.$el.html(this.template(this.model.toJSON));
return this;
}
});
#resume template
<section id="resume">
<h1><%= profession %></h1>
<!-- !!!!! The link for a router which should navigate to ShowResume view -->
View Details
</section>
Collection view:
var ResumeList = Backbone.View.extend({
initialize: function (options) {
this.collection = options.collection;
this.collection.on('add', this.render, this);
// Getting the data from JSON-server
this.collection.fetch({
success: function (res) {
_.each(res.toJSON(), function (item) {
console.log("GET a model with " + item.id);
});
},
error: function () {
console.log("Failed to GET");
}
});
},
render: function () {
var self = this;
this.$el.html('');
_.each(this.collection.toArray(), function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
});
return this;
}
});
The code above works perfectly and does exactly what I need -- an array of models is fetched from my local JSON-server and each model is displayed within a collection view. However, the trouble starts when I try to navigate through my link in the template above. Here comes the router:
var AppRouter = Backbone.Router.extend({
routes: {
'': home,
'resumes/:id': 'showResume'
},
initialize: function (options) {
// layout is set in main.js
this.layout = options.layout
},
home: function () {
this.layout.render(new ResumeList({collection: resumes}));
},
showResume: function (cv) {
this.layout.render(new ShowResume({model: cv}));
}
});
and finally the ShowResume view:
var ShowResume = Backbone.View.extend({
initialize: function (options) {
this.model = options.model;
this.template = _.template($('#full-resume').html());
},
render: function () {
this.$el.html(this.template(this.model.toJSON()));
}
});
I didn't provide the template for this view because it is quite large, but the error is following: whenever I try to navigate to a link, a view tries to render, but returns me the following error: Uncaught TypeError: this.model.toJSON is not a function. I suspect that my showResume method in router is invalid, but I can't actually get how to make it work in right way.
You are passing the string id of the url 'resumes/:id' as the model of the view.
This should solve it.
showResume: function (id) {
this.layout.render(new ShowResume({
model: new Backbone.Model({
id: id,
profession: "teacher" // you can pass data like this
})
}));
}
But you should fetch the data in the controller and react accordingly in the view.
var AppRouter = Backbone.Router.extend({
routes: {
'*otherwise': 'home', // notice the catch all
'resumes/:id': 'showResume'
},
initialize: function(options) {
// layout is set in main.js
this.layout = options.layout
},
home: function() {
this.layout.render(new ResumeList({ collection: resumes }));
},
showResume: function(id) {
// lazily create the view and keep it
if (!this.showResume) {
this.showResume = new ShowResume({ model: new Backbone.Model() });
}
// use the view's model and fetch
this.showResume.model.set('id', id).fetch({
context: this,
success: function(){
this.layout.render(this.showResume);
}
})
}
});
Also, this.model = options.model; is unnecessary as Backbone automatically picks up model, collection, el, id, className, tagName, attributes and events, extending the view with them.

Backbone: Move a model to another Collection

This is my first time using backbone, so I'm pretty confused about everything. I'm trying to make a todo list. Once I click "finished" on the todo, I want it to append to the "Completed" list.
I've been following this tutorial, and I tried to replicate the code(I tried to create a new completedTodo view and stuff like that), and I tried to do when clicking "finished" it would delete the $el, and I would add to the completedTodos. I think the problem here is even if it's added, it's not doing anything.
done: function() {
var completed = new CompletedTodo({
completedTask: this.$('.task').html(),
completedPriority: this.$('.priority').html()
});
completedTodos.add(completed);
this.model.destroy();
},
I put in a debugger there to see if it actually added to the collection, and when i did completedTodos.toJSON();, it does give me back the new thing I just added.
However, it does not append to my collection list.
Here is my whole entire script file, in case I named anything wrong.
var Todo = Backbone.Model.extend({
defaults: {
task: '',
priority: ''
}
});
var CompletedTodo = Backbone.Model.extend({
defaults: {
completedTask: '',
completedPriority: ''
}
});
var Todos = Backbone.Collection.extend({});
var todos = new Todos();
var CompletedTodos = Backbone.Collection.extend({});
var completedTodos = new CompletedTodos();
//Backbone view for one todo
var TodoView = Backbone.View.extend({
model: new Todo(),
tagName: 'tr',
initialize: function() {
this.template = _.template($('.todos-list-template').html());
},
events: {
'click .finished-todo': 'done',
'click .delete-todo' : 'delete'
},
done: function() {
var completed = new CompletedTodo({
completedTask: this.$('.task').html(),
completedPriority: this.$('.priority').html()
});
completedTodos.add(completed);
this.model.destroy();
},
delete: function() {
this.model.destroy();
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
//Backbone view for all todos
var TodosView = Backbone.View.extend({
model: todos,
el: $('.todos-list'),
initialize: function() {
this.model.on('add', this.render, this);
this.model.on('remove', this.render, this);
},
render: function() {
var self = this;
this.$el.html('');
_.each(this.model.toArray(), function(todo) {
self.$el.append((new TodoView({model: todo})).render().$el);
});
return this;
}
});
//View for one Completed Todo
var CompletedTodoView = Backbone.View.extend({
model: new CompletedTodo(),
tagName: 'tr',
initialize: function() {
this.template = _.template($('.completed-todos-template').html());
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
//View for all Completed Todos
var CompletedTodosView = Backbone.View.extend({
model: completedTodos,
el: $('.completed-todos-list'),
initialize: function() {
this.model.on('add', this.render, this);
},
render: function() {
var self = this;
this.$el.html('');
_.each(this.model.toArray(), function(completedTodo) {
self.$el.append((new CompletedTodoView({model: completedTodo})).render().$el);
});
return this;
}
});
var todosView = new TodosView();
$(document).ready(function() {
$('.add-todo').on('click', function() {
var todo = new Todo({
task: $('.task-input').val(),
priority: $('.priority-input').val()
});
$('.task-input').val('');
$('.priority-input').val('');
todos.add(todo);
});
});
After this, I also have to figure out how to use Parse to make it persist to the database. I figured I'd get everything working in backbone first, and then try to do put in the database. I'm also suppose to use node/express, so would that help? I'm pretty much a Ruby on Rails kind of person, so I don't really know any of these javascript framework type of stuff.
Thanks for your help!
Alright,
It was just because I didn't initialize the view.
var completedTodosView = new CompletedTodosView();
This fixed it.

Backbone.js: Using Embeded Javascript inside the View

I'm trying to make the View dynamic as when someone touches the item, but using the '<%= myClassName %>' inside the View doesn't work. I can't use this technique inside the HTML file as it would draw another element and that's not the idea. Also I have set a template but it has nothing on it. I just did that to relate jQuery Mobile into a data-role="content" and render the view inside the content. Any ideas?
Here is what I have:
app.js
var TodoItem = Backbone.Model.extend({
toggleStatus: function(){
if(this.get('status') === 'incomplete'){
this.set({'status': 'complete'});
} else {
this.set({'status': 'incomplete'});
}
this.save();
// PUT /TODOS/1
}
});
var TodoItems = Backbone.Collection.extend({
model: TodoItem,
localStorage: new Backbone.LocalStorage("button"),
initialize: function () {
this.on('remove', this.hideModel, this);
},
hideModel: function (model) {
model.trigger('hide');
}
});
var TodosView = Backbone.View.extend({
initialize: function () {
this.collection.on('add', this.addOne, this);
},
addOne: function (todoItem) {
var todoView = new TodoView({ model: todoItem });
this.$el.append(todoView.render().el);
},
addAll: function () {
this.collection.forEach(this.addOne, this);
},
render: function() {
this.collection.forEach(this.addOne, this);
this.addAll;
return this;
}
});
var TodoView = Backbone.View.extend({
tagName: 'span',
// THIS IS THE MAIN PROBLEM
className: '<%= status %>',
// END COMMENT
template: _.template( $('#personTemplate').html() ),
events: {
"touchstart": "toggleStatus",
"touchend": "toggleStatus"
},
toggleStatus: function () {
this.model.toggleStatus();
},
remove: function(){
this.$el.remove();
},
initialize: function(){
this.render();
this.model.on('change', this.render, this);
this.model.on('destroy', this.remove, this);
this.model.on('hide', this.remove, this);
},
render: function () {
var attributes = this.model.toJSON();
this.$el.html(this.template(attributes));
return this;
}
});
var todoItems = new TodoItems([
{
description: 'Jeffrey Way',
status: "incomplete",
id: 1
},
{
description: 'John Doe',
status: "incomplete",
id: 2
},
{
description: 'Sally Doe',
status: "incomplete",
id: 3
}
]);
var todosView = new TodosView({
el: $('#elem'),
collection: todoItems
});
todosView.render().el
You can do
this.$el.addClass(this.model.get('status'))
inside the view's render method.
Trying to use a template value in the view code doesn't make any sense; those properties are set when the object is parsed, so how would it know which object to fetch the status from?
The best thing todo, if i understand correctly, is to listen to the change:status in your view, and adding/removing a class to your view according to the value of status.
in your initialize of TodoView:
this.listenTo(this.model, 'change:status', this.changeStatus);
in your TodoView declare:
changeStatus : function(model, value, options)
{
// add a class or toggle it or change something in the view..
}
Solution 1
var TodoView = Backbone.View.extend({
tagName: 'span',
...})
var x=new TodoView ({className:'sample'});
Solution 2
Use a template!
var TodoView = Backbone.View.extend({
template="<span class=<%-className%>>Some Stuff Goes Here</span>",
...
render:function(){
var $ele=$(_.template(this.template,{className:'sample'}));
this.$el.replaceWith($ele);
this.$el=$ele;
this.delegateEvents(); //inbuilt-method, to re-bind all event handlers
});

Model is not deleted from backbone collection

I still trying to remove (destroy) model from my collection. Data is groupedBy and rendered into accordion style. But when I click to X in my console is notice :
Uncaught Uncaught TypeError: Cannot call method 'destroy' of undefined
(function() {
window.App = {
Models: {},
Collections: {},
Views: {},
Router: {}
};
window.vent = _.extend({}, Backbone.Events);
})();
// !models.js
App.Models.Item = Backbone.Model.extend({});
// !collections.js
App.Collections.Items = Backbone.Collection.extend({
model: App.Models.Item,
url: 'api/items.json'
});
// !views.js
App.Views.Items = Backbone.View.extend({
el: '#items',
events: {
'click .cccc':'deleteItem',
},
deleteItem: function() {
this.model.destroy();
},
initialize: function() {
this.listenTo( this.collection, "change", this.render );
this.template = _.template( document.getElementById('productsCategoriesTemlate').innerHTML );
this.render();
this.$el.accordion({ animate: 0 });
},
getGroups : function(){
return _.groupBy(this.collection.toJSON(), 'category');
},
render: function() {
this.el.innerHTML = this.template({ data : this.getGroups() });
},
addOne: function(item) {
// ????????????
}
});
App.Views.Item = Backbone.View.extend({
deleteItem: function() {
this.model.destroy();
},
// ???????????
});
// !router.js
App.Router = Backbone.Router.extend({
routes: {
'':'index',
},
index: function() {
console.log('index page !');
},
});
new App.Router;
Backbone.history.start();
App.items = new App.Collections.Items;
App.items.fetch().then(function() {
new App.Views.Items({ collection: App.items });
});
template :
<script id="productsCategoriesTemlate" type="text/template">
<% _.each( data, function( category, i ){ %>
<h3 class="category-name"><%= i %></h3>
<div><% _.each( category, function( item ){ %>
<li class="product"><%= item.title %><p style="float:right;" class="cccc">X</p></li>
<% }) %>
</div>
<% }) %>
</script>
Where do you instantiate Apps.Views.Items? Is this your 'collection view'? If this view is representing your collection, you have to somehow pass or reference the model on 'deleteItem'.
App.Views.Items does not represent a single model, so this.model would be incorrect.
UPDATE
You should have a separate view for each item, such as App.Views.Item, and loop through and create this view for each model in App.Views.Items' collection.
2nd UPDATE
Yeah, you are getting it. Here's some sample code I threw together (I haven't tested it, so you might have to adjust it, but it gives a good idea. The template rendering syntax might be incorrect as I don't usually do it manually).
App.Views.Items = Backbone.View.extend({
render: function() {
this.collection.each(function(model) {
var view = new App.Views.Item({ model: model });
this.$el.append(view.render().el);
});
},
});
App.Views.Item = Backbone.View.extend({
template: _.template($('#itemViewTemplate')),
render: function() {
this.$el.html(this.template(this.model.toJSON()));
},
});
App.items = new App.Collections.Items;
App.items.fetch().then(function() {
var items = new App.Views.Items({ collection: App.items });
$('body').append(items.render().$el);
});
By the way, once you get the hang of Backbone and how it works, you should try out Marionette.js. It makes all of this kind of thing much simpler.

How to filter Backbone.js Collection and Rerender App View?

Is is a total Backbone.js noob question. I am working off of the ToDo Backbone.js example trying to build out a fairly simple single app interface. While the todo project is more about user input, this app is more about filtering the data based on the user options (click events).
I am completely new to Backbone.js and Mongoose and have been unable to find a good example of what I am trying to do. I have been able to get my api to pull the data from the MongoDB collection and drop it into the Backbone.js collection which renders it in the app. What I cannot for the life of me figure out how to do is filter that data and re-render the app view. I am trying to filter by the "type" field in the document.
Here is my script:
(I am totally aware of some major refactoring needed, I am just rapid prototyping a concept.)
$(function() {
window.Job = Backbone.Model.extend({
idAttribute: "_id",
defaults: function() {
return {
attachments: false
}
}
});
window.JobsList = Backbone.Collection.extend({
model: Job,
url: '/api/jobs',
leads: function() {
return this.filter(function(job){ return job.get('type') == "Lead"; });
}
});
window.Jobs = new JobsList;
window.JobView = Backbone.View.extend({
tagName: "div",
className: "item",
template: _.template($('#item-template').html()),
initialize: function() {
this.model.bind('change', this.render, this);
this.model.bind('destroy', this.remove, this);
},
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
this.setText();
return this;
},
setText: function() {
var month=new Array();
month[0]="Jan";
month[1]="Feb";
month[2]="Mar";
month[3]="Apr";
month[4]="May";
month[5]="Jun";
month[6]="Jul";
month[7]="Aug";
month[8]="Sep";
month[9]="Oct";
month[10]="Nov";
month[11]="Dec";
var title = this.model.get('title');
var description = this.model.get('description');
var datemonth = this.model.get('datem');
var dateday = this.model.get('dated');
var jobtype = this.model.get('type');
var jobstatus = this.model.get('status');
var amount = this.model.get('amount');
var paymentstatus = this.model.get('paymentstatus')
var type = this.$('.status .jobtype');
var status = this.$('.status .jobstatus');
this.$('.title a').text(title);
this.$('.description').text(description);
this.$('.date .month').text(month[datemonth]);
this.$('.date .day').text(dateday);
type.text(jobtype);
status.text(jobstatus);
if(amount > 0)
this.$('.paymentamount').text(amount)
if(paymentstatus)
this.$('.paymentstatus').text(paymentstatus)
if(jobstatus === 'New') {
status.addClass('new');
} else if (jobstatus === 'Past Due') {
status.addClass('pastdue')
};
if(jobtype === 'Lead') {
type.addClass('lead');
} else if (jobtype === '') {
type.addClass('');
};
},
remove: function() {
$(this.el).remove();
},
clear: function() {
this.model.destroy();
}
});
window.AppView = Backbone.View.extend({
el: $("#main"),
events: {
"click #leads .highlight" : "filterLeads"
},
initialize: function() {
Jobs.bind('add', this.addOne, this);
Jobs.bind('reset', this.addAll, this);
Jobs.bind('all', this.render, this);
Jobs.fetch();
},
addOne: function(job) {
var view = new JobView({model: job});
this.$("#activitystream").append(view.render().el);
},
addAll: function() {
Jobs.each(this.addOne);
},
filterLeads: function() {
// left here, this event fires but i need to figure out how to filter the activity list.
}
});
window.App = new AppView;
});
Have you tried resetting the collection with the result of the "leads" filter?
Something like
window.AppView = Backbone.View.extend({
el: $("#main"),
events: {
"click #leads .highlight" : "filterLeads"
},
initialize: function() {
Jobs.bind('add', this.addOne, this);
Jobs.bind('reset', this.render, this); //render on reset
Jobs.fetch(); //this triggers reset
},
addOne: function(job) {
var view = new JobView({model: job});
this.$("#activitystream").append(view.render().el);
},
//add a render function
render: function() {
this.$("#activitystream").empty(); //empty out anything thats in there
Jobs.each(this.addOne);
},
filterLeads: function() {
Jobs.reset(Jobs.leads()); //reset Jobs with only the leads
}
});
Also your AppView has no 'render' method, yet you reference it here:
Jobs.bind('all', this.render, this);

Categories