Dynamically change size of collection in Backbone.js from HTML Select form - backbone.js

I have this table that has a list of items from an array called books. i.e.
var books = [
{"title": "Harry Potter 1", "author": "J. K. Rowling"},
{"title": "Harry Potter 2", "author": "J. K. Rowling"}
//.... More books
]
I separated my views into a view for a single book row, the entire book list that gets rendered into a table, and also a view for the select form.
var Book = Backbone.Model.extend({});
var BooksCollection = Backbone.Collection.extend({
model: Book
});
var Books = new BooksCollection(books);
//
var SelectView = Backbone.View.extend({
el: ".container",
events: {
"change #num-entries": "updateBookList"
},
initialize: function() {
this.render();
},
render: function() {
this.collection.models.length = $('#num-entries').val();
},
updateBookList: function() {
this.collection.models.length = $('#num-entries').val();
//console.log(this.collections.models.length);
booksview.$el.empty();
booksview.unbind();
booksview.render();
}
});
var selectView = new SelectView({
model: Book,
collection: Books
});
//
var BooksView = Backbone.View.extend({
type: "BooksView",
el: "#books-table",
initialize: function() {
//this.collection.models.length = $('#num-entries').val();
this.render();
},
render: function() {
this.collection.each(function(book) {
var bookView = new BookView({
model: book
});
bookView.render();
this.$el.append(bookView.$el);
}, this)
}
});
var BookView = Backbone.View.extend({
type: "BookView",
className: 'book',
tagName: 'tr',
template: _.template($("#books-template").html()),
events: {
"click .delete": "bookRemove"
},
initialize: function() {
this.render();
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
bookRemove: function(book) {
this.remove();
this.model.trigger('destroy', this.model);
//Books.remove(book);
},
});
var booksview = new BooksView({
collection: Books
})
this is in my html
<div class="container">
<div class="form-group" id="entries-per-page">
<label>Number of Entries:</label>
<select class="form-control" id="num-entries">
<option value="10">Show 10 entries</option>
<option value="25" selected>Show 25 entries</option>
<option value="50">Show 50 entries</option>
<option value="100">Show 100 entries</option>
</select>
</div>
<table class="table table-bordered table-hover">
<thead>
<tr class="header-names">
<th class="book-category">Title</th>
<th class="book-category">Author</th>
<th class="book-category">Meta Data</th>
<th class="book-category">Options</th>
</tr>
</thead>
<tbody id="books-table">
<script type="text/template" id="books-template">
<td><%- title %></td>
<td><%- author %></td>
<td><span class='glyphicon glyphicon-remove delete'></span></td>
</script>
</tbody>
</table>
</div>
Currently my messy code for the select view can make the list collection shrink (so my table goes from 25 entries to 10 for example) but then if i try to make the table display more books i get an error - Uncaught TypeError: Cannot read property 'toJSON' of undefined. I would think using the collection.slice function should help, but I can't figure it out. Also, getting my delete book function to work with this is hard. Can I get any help using the slice method to dynamically change the size of my book array or book collection so the table displays correctly?

When you are modifying the models array directly, you are losing the reference to models, and you would have to re-add the to the collection to show them again.
Instead of changing the actual collection, you should pick those out you need in the render method. This also makes more sense on a conceptual level, since the number of entries to display is a view-concern rather than a data-concern.
Your view will now look something like this:
var BooksView = Backbone.View.extend({
type: "BooksView",
el: "#books-table",
// Define how many books we want to display
booksToShow: 25,
initialize: function() {
this.render();
},
render: function() {
_.each(this.collection.slice(0, this.booksToShow), this.renderBook, this);
},
renderBook: function(bookModel) {
var bookView = new BookView({
model: book
});
bookView.render();
this.$el.append(bookView.$el);
}
});
Then within updateBookList you can call
this.collection.booksToShow = $('#num-entries').val();
Another thing; for good measures sake, you should remember to unbind the individual BookViews as well when you remove them. Preferably by creating a method in BooksView to clean up.

Related

Update backbone view partially using timer in collection and get subset of a model

Am trying to update the some of the columns in table view when a timer triggered on collection am getting a subset of (only changed) properties. tried all kinds of codes from google but no luck.
Want to update only age property out of name and designation in view not intended to rerender the entire view.
here is my code
<div class='mainBody'>
<table class="table">
<tr class="success">
<td>Name</td>
<td>Age</td>
<td>Ocupation</td>
</tr>
</table>
</div>
<script id="externalViewTemplate" type="text/template">
<td width='25%'> <%= name %> </td>
<td width='25%' id="age"> <%= age %> </td>
<td width='25%'> <%= occupation %> </td>
</script>
Script:
//Person Model
var PersonModel = Backbone.Model.extend({
_id:"id"
});
//Person collection - People
var PeopleCollection = Backbone.Collection.extend({
model: PersonModel,
view: PeopleView,
initialize: function(){
this.updateAge = setInterval(this.getAge.bind(this), 3000);
},
getAge: function()
{
var per = [{
id:1,
age: 31
},
{
id:2,
age: 32
},
{
id:3,
age: 37
}];
this.reset(per);
}
});
//Person view - Responsible for rendering one person
var PersonView = Backbone.View.extend({
tagName: 'tr',
template: _.template( $('#externalViewTemplate').html() ),
initialize: function() {
this.model.on("change:age", this.update, this);
},
update: function() {
this.el.cells["age"].innerHTML = this.model.get("age");
},
render: function(){
this.$el.append(this.template(this.model.toJSON()));
//console.log(this.$el.find('.edit'));
return this;
}
});
//person view collection - rendering the rows collection
var PeopleView = Backbone.View.extend({
view: PersonView,
initialize: function(options){
this.collection = options.collection,
this.collection.on("reset", this.update, this);
},
render: function(){
this.collection.each(function(person){
var personRow = new PersonView({model: person});
this.$el.append(personRow.render().el);
}, this);
},
update: function(){
this.collection.each(function(person){
//var personRow = new PersonView({model: person});
this.model.age = person.age;
}, this);
}
});
//Try this code
var personCollection = new PeopleCollection([
{
id:1,
name: 'Raju',
age: 31,
occupation: 'Dotnet Programmer'
},
{
id:2,
name: 'Rajesh',
age: 32,
occupation: 'Developer'
},
{
id:3,
name: 'Ramesh',
age: 33,
occupation: 'Designer'
}
]
);
var peopleList = new PeopleView({ el: '.table', collection: personCollection});
peopleList.render();
Want to update only age property out of name and designation in view not intended to rerender the entire view.
According to <tr> - MDN cells property returns an HTMLCollection. Your code for updating a cell should be
update: function() {
this.el.cells.namedItem("age").innerHTML = this.model.get("age");
// or this.el.cells.item(1).innerHTML = this.model.get("age");
// this.el.cells[1].innerHTML = this.model.get("age"); might work
},
Since you most likely has jQuery with backbone, you can just do
this.$el.find(".age").text(this.model.get("age"));
Note that I use className "age" because you shouldn't use id in templates which gets duplicated. You can also use index(), eq() etc to access the cell using jQuery instead of className

Calling variables within Underscore template

Im trying to make an Underscore template in my Backbone application, but my scoping must be off or something, because Underscore thinks my variable is not defined. Im getting "Uncaught ReferenceError: dictionary is not defined."
Here is the template code:
<script type="text/template" id="new_template">
<table class="table striped">
<tbody>
<% _.each(dictionary, function(user){ %>
<tr>
<td> <%= user.get('word')%></td>
<td> <%= user.get('definition')%></td>
</tr>
<% }) %>
</tbody>
</table>
</script>
And here is the logic in app.js that defines my inline template variable calls:
(function($){
//---------SINGLE ENTRY MODEL----------
var Entry = Backbone.Model.extend({
defaults: function(){
return{
word: '',
definition: ''
}
}
});
//------------ENTRY MODEL COLLECTION------------
EntryList = Backbone.Collection.extend({
model: Entry
});
//-----INSTANCIATE COLLECTION----
var dictionary = new EntryList();
var saved = new EntryList();
//-----SINGLE ENTRY VIEW------
var EntryView = Backbone.View.extend({
model: new Entry(),
tagName:'div',
className: 'singleEntry',
events:{
'click .edit': 'edit',
'click .delete': 'delete',
'keypress .definition': 'updateOnEnter',
'click .save': 'save'
},
initialize: function(){
// this.template = _.template($("#dictionary_template").html());
this.template = _.template($("#new_template").html());
},
delete: function(ev){
ev.preventDefault;
dictionary.remove(this.model);
saved.remove(this.model);
},
edit: function(ev){
ev.preventDefault;
this.$('.definition').attr('contenteditable', true).focus();
},
save: function(ev){
ev.preventDefault;
saved.add(this.model);
dictionary.remove(this.model);
saved.comparator = 'word';
console.log(this.model.toJSON());
},
close: function(){
var definition = this.$('.definition').text();
this.model.set('definition', definition);
this.$('.definition').attr('contenteditable', false).blur();
},
updateOnEnter: function(ev){
if(ev.which == 13){
this.close();
}
},
render: function(){
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
//--------------DICTIONARY VIEW------------
var DictionaryView = Backbone.View.extend({
model: dictionary,
el: $('#entries'),
initialize: function(){
this.model.on('add', this.render, this);
this.model.on('remove', this.render, this);
},
render: function(){
var self = this;
self.$el.html('');
_.each(this.model.toArray(), function(entry, i){
self.$el.append((new EntryView({model: entry})).render().$el);
});
return this;
}
});
//---------SAVED ENTRY VIEW-----------
var SavedView = Backbone.View.extend({
model: saved,
el: $('#saved'),
initialize: function(){
this.model.on('add', this.savedRender, this);
this.model.on('remove', this.savedRender, this);
},
savedRender: function(){
var self = this;
self.$el.html('');
_.each(this.model.toArray(), function(entry, i){
self.$el.append(new EntryView({model: entry}).render().$el);
});
return this;
}
});
//---------TEST VIEW------------------
var TestView = Backbone.View.extend({
el: $('#saved'),
render: function(){
this.$el.html('new event route');
}
});
//-------BINDING DATA ENTRY TO NEW MODEL VIEW-------
$(document).ready(function(){
$('#new-entry').submit(function(ev){
var entry = new Entry({word: $('#word').val(), definition: $('#definition').val() });
dictionary.add(entry);
dictionary.comparator = 'word';
console.log(dictionary.toJSON());
$('.form-group').children('input').val('');
return false;
});
var appView = new DictionaryView();
var savedView = new SavedView();
});
//--------------ROUTER----------------
var Router = Backbone.Router.extend({
routes:{
'':'home',
'new': 'newEvent'
}
});
var router = new Router();
router.on('route:home', function(){
console.log('router home');
router.on('route:newEvent', function(){
console.log('router new');
var testView = new SavedView();
})
});
Backbone.history.start();
})(jQuery);
Update
The way Underscore templates work (once you compile them) is by replacing any non JavaScript keywords they find in ERB-style delimiters (the bee-stings: <% %>) with the values in the object passed in to the compiled template. In your sample code, Underscore is expecting an object with a property named 'dictionary', for example.
The way your template is set up right now, it's expecting a collection (EntryList) of models with the words and definition attributes. However, if you look at your code,
render: function(){
this.$el.html(this.template(this.model.toJSON()));
return this;
}
is the only time populate your template, in your Entry view, which does not take a collection, but an Entry model.
What you really want to do is to get rid of the for each loop in your template, and let it simply render the model. I would rewrite it like this,
<script type="text/template" id="new_template">
<tr>
<td> <%= word %></td>
<td> <%= definition %></td>
</tr>
</script>
Now, I hope that el in dictionary view is a <tbody>. If it is, then these models will comfortably park their <tr> where they have to go and you can call it a day.
Looking at the defaults of the Entry model it looks like all you'll be populating that model with is { word: '', definition: '' }. In your template, underscore is looking for a variable called dictionary. Yet, you pass an object with the variables word and definition in to the template.
More importantly, I am not sure your view building logic makes compete sense. You're building child views out of EntryView in your DictionaryView and then you run a for each loop in your template?

backbone-froms multiple views submit a form

I have a master view App.MainView (table view), a App.TaskView (row view) and using backbone forms to add and edit the records. Here is my code:
App.MainView = Backbone.View.extend({
template: $("#home-template").html(),
initialize: function() {
_.bindAll(this, 'render');
this.listenTo(tasks, 'add change', this.addOne);
this.subViews = [];
},
addOne: function(model) {
var view = new App.TaskView({model: model});
this.$('tbody').append(view.render().el);
},
render: function() {
var template = _.template(this.template);
this.$el.append(template);
//this.$el.find("#filter").append(this.createSelect());
var self = this;
this.collection.fetch({
wait: true,
success: function(model, response) {
var TasksLen = model.length;
for (var i = 0; i < TasksLen; i++) {
var taskView = new App.TaskView({
model: model.models[i]
});
//self.subViews.push(taskView);
$('#record-list-table', this.el).append(taskView.render().el);
}
}
});
},
Now my TaskView:
App.TaskView = Backbone.View.extend({
tagName: 'tr',
template: _.template($('#record-template').html()),
initialize: function() {
var self = this;
},
events: {
"click .edit": "editrecord",
},
render: function() {
this.$el.html(this.template(this.model.toJSON()))
return this;
},
editrecord: function() {
form.setValue(this.model.toJSON());
}
and my Form and submit button
var form = new Backbone.Form({
model: task
});
window.form = form;
$('#form').html(form.render().el);
$('#submit-btn').click(function() {
var data = form.getValue();
form.commit();
task.save(data, {wait: true,
success: function(task, response) {
tasks.add(task);
}
});
Templates:
<script type="text/template" id="home-template">
<table id="recordtable">
<input type='reset' id="reset-btn" onclick="reset()" class="add-new" value='' name='reset' /></table></script>
<script type="text/template" id="record-template">
<td id="edit-name" class="edit"> <%- name %></td>
<td class="edit date" class="edit"> <%- due_date %></td>
</script>
I have two issues:
On model change,it adds another model (However it does the put request and updates in database but on front end backbone adds a new row in the table instead of updating the previous row). But after page refresh it shows correct data. Looks like some issue with change event.
I want to create direct route to a task i.e task/id and want to highlight that task in the table and edit it in the form. Right Now I am able to edit any model on click in the form but I want to have direct route too.
I think now I found the problem of your code. You are doing
tasks.add(task);
in success method of model save. But I think you need to re render the task.
1. You can bind a change event in taskview so that it will render task with updated value.
2. You can add a function in your taskview like this
refresh: function(){
this.render();
}
and you can call this function after model.save success found.

Event triggered in all instances of Backbone.View

I'm displaying a table of categories with Backbone. I created two views:
RowView (containing a single tr)
TableView (containing table structure)
The definitions:
RowView = Backbone.View.extend({
el: "#content table tbody",
initialize: function() {
this.render();
},
render: function(){
var params = { name: this.model.get('name'), route: this.options.route };
var template = _.template( $("#rowTemplate").html(), params);
this.$el.append(template);
},
events: {
"click #name": "clickHandler"
},
clickHandler: function( event ) {
console.log('Browse subcategories of ' + this.model.get('name'));
}
});
TableView = Backbone.View.extend({
el: "#content",
initialize: function(){
this.render();
},
render: function(){
var row = new this.collection();
var that = this;
row.fetch({
success: function() {
console.log('Collection fetch succeeded');
var params = { title: that.options.title,
counter: row.at(0).get('counter'),
route: that.options.route
};
var template = _.template( $("#tableTemplate").html(), params);
that.$el.html( template );
// RowView's are created by iteration here
for(var x = 1; x < row.length; x++) {
var params = { model: row.at(x), route: that.options.route };
var view = new RowView(params);
}
}
});
}
});
As you can see, I've attached a click event at the RowView.
RowView template:
<script type="text/template" id="rowTemplate">
<tr>
<td id="name" class="fill"><%= name %></td>
<td>Editar</td>
</tr>
</script>
Clicking any #name triggers the handler in all instance of the view. So when clicking one category I get:
Browse subcategories of category1 127.0.0.1:68
Browse subcategories of category2 127.0.0.1:68
etc...
As far as I know, that's because all RowView's are delegated to the same el.
The first thing I though about was adding the category name to the rowTemplate and compare the value in the DOM with the value in the view to see which one actually triggers the event.
But that solutions look really ugly. What's the correct way of accomplishing this in Backbone?
EXTRA: Is it considered better if I only create one view, and iterate in the template to generate the rows?
EDIT: I think the provided code is enough. Otherwise I can add them.
you can modify RowView like this :
RowView = Backbone.View.extend({
container: '#content table tbody',
tagName: 'tr',
initialize: function() {
this.render();
},
render: function() {
var params = {
name: this.model.get('name'),
route: this.options.route
};
var template = _.template($("#rowTemplate").html(), params);
this.$el.html(template).appendTo(this.container);
},
events: {
"click .fill": "clickHandler"
},
clickHandler: function(event) {
console.log('Browse subcategories of ' + this.model.get('name'));
}
});
and RowView template:
<script type="text/template" id="rowTemplate">
<td class="fill"><%= name %></td>
<td>Editar</td>
</script>
Backbone.js will create a tr element. then this.$el.html(template).appendTo(this.container) fill the tr element with template and append to #content table tbody.
just like that, RowView's events be delegated on RowView's el, not #content table tbody.
You have more than one element with the same id on your page, due to all of your rows having the
<td id="name" class="fill"> element.
Element IDs should be unique within your document.
One solution would be to distinguish the rows in your template, and use events as a function to set the proper ID.
Template:
<script type="text/template" id="rowTemplate">
<tr>
<td id="name-<%= name %>" class="fill"><%= name %></td>
<td>Editar</td>
</tr>
Events function:
events: function(){
_events = {};
_events["click #name-" + this.model.get('name')] = "clickHandler";
return _events;
}
Try this
RowView = Backbone.View.extend({
container: '#content table tbody',
tagName: 'tr',
// initialize: function() {
// this.render();
// },
render: function() {
var params = {
name: this.model.get('name'),
route: this.options.route
};
var template = _.template($("#rowTemplate").html(), params);
this.$el.append(this.template);
},
events: {
"click .name": "clickHandler"
},
clickHandler: function(event) {
console.log('Browse subcategories of ' + this.model.get('name'));
}
});
RowView template (no need for identifying each row view):
<script type="text/template" id="rowTemplate">
<td class="name"><%= name %></td>
<td>Editar</td>
</script>
Then the table view:
...
that.$el.html( template );
// RowView's are created by iteration here
for(var x = 1; x < row.length; x++) {
var params = { model: row.at(x), route: that.options.route };
var view = new RowView(params);
that.$el.find('tbody').append(view.el);
view.render()
}
...

How to display collection to template?

I have been trying to display my data in collection to the template. On my rest console, I can see that the collection is populated; but I am not able to do it in my application. The code is as follows:
In view, the render is like:
render : function() {
var template = _.template(tpl,{
CollectionForTemplate: this.Collection.toJSON(),
this.el.html(template);
},
In view, the function that calls the fetch is as follows:
loadTariffGrids: function(){
//fetch done here
if (Collection.length > 0) {
_.each(Collection, function(Model) {
JSON.stringify(Model);
alert(JSON.stringify(Model));
}, this);
};
this.render;
}});
},
And finally, the template is:
<span>
<% _.each(CollectionForTemplate, function(model) { %>
</span>
<td><%= model.cost %></td>
<% }); %>
Where am I going wrong?
In your render, just use:
CollectionForTemplate: this.Collection;
Modify the view as follows:
var self = this;
//fetch done here
if (Collection.length > 0) {
_.each(Collection, function(Model) {
JSON.stringify(Model);
}, this);
};
self.Collection = Collection;
self.render;
I think the collection that you pass to the template was not getting instanciated. So I use self to store the reference before control passes into the fetch function. I know my explanation is not good; I would like someone more experienced to offer you a better explanation. Hope this helps
Ok #Amateur let's try to solve your need properly.
Attention: all code has been written in the fine air don't expect it to work just out of the box, use it as inspiration.
First of all is not very good idea to use programatic logic in your templates, it is just a design decision, I don't want to look very academic here but I think if you are too flexible with this you can finish with a very difficult to maintenance code.
So, the idea is to manufacture the data so the template has not to make any decision/calculation. And if you need a loop into your template let's use sub-templates.
What I understand from your code example is that you have a Collection like this:
[
{
"id": "1",
"cost": 100,
"name": "Amateur"
},
{
"id": "2",
"cost": 200,
"name": "Other name"
},
{
"id": "3",
"cost": 300,
"name": "Other name again"
},
]
And you want to render something like this:
<table>
<tr>
<td>Name</td>
<td>Cost</td>
</tr>
<tr>
<td>Amateur</td>
<td>100</td>
</tr>
<tr>
<td>Other name</td>
<td>200</td>
</tr>
<tr>
<td>Other name again</td>
<td>300</td>
</tr>
</table>
As I have read in another (duplicated) question you have sent, you also want to only render cost if the name == "Amateur", like this:
<table>
<tr>
<td>Name</td>
<td>Cost</td>
</tr>
<tr>
<td>Amateur</td>
<td>100</td>
</tr>
<tr>
<td>Other name</td>
<td>--</td>
</tr>
<tr>
<td>Other name again</td>
<td>--</td>
</tr>
</table>
Iteraction
For the iteration we are gonna solve it with sub-templates.
Master template:
<script type="text/template" id="template-elements">
<table>
<tr>
<td>Name</td>
<td>Cost</td>
</tr>
</table>
</script>
Sub-template
<script type="text/template" id="template-element">
<tr>
<td><%= name %></td>
<td><%= cost %></td>
</tr>
</script>
In order to make it more elegant lets use also a View for the Collection and a SubView for each Model.
var MyModel = Backbone.Model.extend();
var MyCollection = Backbone.Collection.extend({
model: MyModel
});
var MyModelView = Backbone.View.extend({
template: _.template( $("#template-element").html() ),
render: function(){
this.$el.html( this.template( this.model.toJSON() ) );
return this;
}
});
var MyCollectionView = Backbone.View.extend({
template: _.template( $("#template-elements").html() ),
render: function(){
this.$el.html( this.template() );
this.collection.each( function( model ){
var view = new MyModelView({ model: model });
this.$el.find( "table" ).append( view.render().el );
});
return this;
}
});
Decorators
We have solved the iteration issue, now we are gonna implement the logic that hides the Model.cost when Model.name != "Amateur".
Let's implement a method that returns the attributes of the Model already cooked for the template:
var MyModel = Backbone.Model.extend({
toJSONCooked: function(){
var json = this.toJSON();
if( this.get( "name" ) != "Amateur" ) {
json.cost = "--";
}
return json;
}
});
We can use this method to feed the template:
var MyModelView = Backbone.View.extend({
template: _.template( $("#template-element").html() ),
render: function(){
this.$el.html( this.template( this.model.toJSONCooked() ) );
return this;
}
});

Resources