I have a multiselect dropdown that I'm rendering with Backbone. As user selects or deselects options, I'd like those (de)selections to be saved asynchronously via Backbone.
I found Backbone.Picky, and thought it might be helpful in my endeavor, but I can't seem to get it to detect selects.
In my FieldView's clicked function below, console.log(this.model.selected); always writes undefined to the log. Why?
var Field = Backbone.Model.extend({
initialize: function(){
var selectable = new Backbone.Picky.Selectable(this);
_.extend(this, selectable);
}
});
var FieldView = Backbone.View.extend({
tagName: "option",
initialize: function(){
_.bindAll(this, 'render');
},
events: {
"click":"clicked"
},
clicked: function(e) {
var data_type = this.model.get("DATA_TYPE");
console.log(this.model.selected); // why is this undefined?
console.log("it's a " + data_type);
},
render: function(){
this.$el.attr('value', this.model.get('COLUMN_NAME')).html(this.model.get('display_name'));
return this;
}
});
Here's a jsfiddle http://jsfiddle.net/EAZCt/2/ for more context.
Using Backbone, how can I asynchronously save the select-status of options in a multiselect list?
Your model object doesn't ever have "selected" property because you never select the model. I have never used Backbone.Picky but it seems that you could try:
var FieldView = Backbone.View.extend({
tagName: "option",
initialize: function(){
this.model.on('selected', this.selected, this);
},
events: {
"click":"clicked"
},
clicked: function() {
this.model.select();
},
selected: function() {
var data_type = this.model.get("DATA_TYPE");
console.log(this.model.selected);
console.log("it's a " + data_type);
},
render: function(){
this.$el.attr('value', this.model.get('COLUMN_NAME')).html(this.model.get('display_name'));
return this;
}
});
http://jsfiddle.net/hGEYL/
Related
I'm learning backbone and now thinking of how to apply an infinite scroll and fetch/load lets say 20 items from my collection every time the scroll is at the bottom of the page.
I have been searching around a lot after different libs and such without really getting any closer. Anyone that can explain/show how this is best done?
I have now added the infiniscroll.js plugin and trying to get it to work. But on scroll it won't load new items. What am i supposed to do on the appendRender? and how?
var StartView = Backbone.View.extend({
tagName: "section",
id: "start",
className: "content",
events: {
},
initialize: function(){
$(".container").html(this.el);
console.log("init start");
this.template = _.template($("#start_template").html(), {} );
this.collection = new VideoCollection();
_.bindAll(this, "render");
this.render();
this.infiniScroll = new Backbone.InfiniScroll(this.collection, {
success: this.appendRender,
pageSize: this.collection.length,
scrollOffset: 100
});
},
appendRender: function() {
var self = this;
self.$el.html(self.template);
self.$el.find(".videos").append("<div style='margin-bottom:30px; width:100%; height:170px; float:left; background-color:#e4e4e4;'>fff</div>")
},
render: function(){
var self = this;
this.$el.html("loading");
console.log("render start")
},
kill: function() {
console.log("kill start");
this.remove();
}
});
return StartView;
The backbone-pageable plugin supports infinite scrolling.
It's just a matter of your collection extending Backbone.PageableCollection, and you specifying some extra properties. There's also an example of a backbone view listening to the changing collection, as well as fetching on scroll.
It's all described on the github page. It's updated fairly often.
I would have done it something like this (although the document.addEventListener('scroll')-part isn't really elegant
(function() {
"use strict";
var Item = Backbone.Model.extend({});
var Items = Backbone.Collection.extend({
model: Item,
url: "/api/items/"
});
var ItemView = Backbone.View.extend({
tagName: "li",
render: function() {
this.$el.html(this.model.get("name"));
return this;
}
});
var ItemsList = Backbone.View.extend({
tagName: "ul",
offset: 0,
limit: 60,
initialize: function() {
this.collection = new Items();
this.collection.on("reset", this.addAll, this);
this.collection.on("add", this.addOne, this);
this.getItems();
},
render: function () {
return this;
},
getItems: function () {
this.collection.fetch({
"data": {"offset": this.offset, "limit": this.limit},
"success": _.bind(function(e){
this.offset += this.limit;
}, this)
});
},
addOne: function(item) {
var view = new ItemView({model: item});
this.$el.append(view.render().$el);
},
addAll: function() {
this.collection.each(this.addOne, this);
}
});
var itemsList = new ItemsList();
$(document.body).append(itemsList.render().$el);
document.addEventListener('scroll', function (event) {
if (document.body.scrollHeight == document.body.scrollTop + window.innerHeight) {
itemsList.getItems();
}
});
}());
I have issue in rendering Shopping Bag Views using Backbone for my website.
I am using 1 collection for all Bag Views (“Quick_View” of items list & “Normal_View” of Items list). Also I created “Item_View”, which is being used to render each item in both the Views.
It is a SPA (Single Page Application) and “Quick_View” is initiated and rendered for all Backbone routes and hidden by default. Whenever user clicks on “Quick_View” link from any page it is showing. There is no route defined for it.
The “Normal_View”, can be accessed using Checkout button given in “Quick_View”. It is bind with “domain/checkout” route.
When I access “Normal_View” from “Quick_View” check button; it works fine and both (Quick and Normal) views are in Sync. Means, when we add, delete, update any item in any of the View, both Views are getting updated accordingly.
But when I access “domain/checkout” route directly in a new browser, both views are getting rendered fine, but they are not in sync. Means, change in 1 view does not update another view.
The reason, I tracked is, when I access “Normal_View” through “Quick_View”, model for each item in both the Views having same CID, so the both Views are in sync, if there is any change in a model from any of the View.
And, when I access “Normal_View” directly, model for each item in both the views are not having same CID, so they do not work as expected.
There are few more points to consider:
Collection is firing reset event twice for “Quick_View” and each item
in “Quick_View” is rendering twice.
When, I access “Normal_View” (in either way), “Quick_View” is again getting rendered but once “Normal_View” rendering is over.
// Main View
var mainView = Backbone.View.extend({
el: 'body',
template: {
header: Handlebars.compile(headerTemplate),
mainNav: Handlebars.compile(mainNavtemplate),
footer: Handlebars.compile(footerTemplate)
},
initialize: function() {
_.bindAll();
AW.collection.bag = new bagCollection();
//AW.collection.bag.fetch({reset:true});
},
render: function() {
this.$el.html(this.template());
this.loadSubView('bagQV');
},
loadSubView: function(subView) {
switch(subView) {
case 'home' :
if(!AW.view.home) AW.view.home = new homepageView();
AW.view.home.render();
break;
case 'bagQV' :
if(!AW.view.bagQV) AW.view.bagQV = new bagQuickView({collection: AW.collection.bag});
//AW.view.bagQV.render();
break;
case 'checkout' :
if(!AW.view.checkout) AW.view.checkout = new checkoutView({collection: AW.collection.bag});
AW.view.checkout.render();
break;
}
}
});
// Single Item View
var bagItemView = Backbone.View.extend({
tagName: 'tr',
template: Handlebars.compile(bagItemTemplate),
initialize: function() {
_.bindAll(this);
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'remove', this.removeItem);
$(document).on('keyup', this.listenKeyboard);
},
events: {
'click .qtyInput .button' : 'updateItem',
'click .controls a.remove' : 'removeModel'
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.$el.attr('data-id',this.model.cid);
return this;
},
updateItem: function(e) {
e.preventDefault();
e.stopPropagation();
var newQty = this.$el.find('.qtyInput input').val();
var newAmt = newQty * parseFloat(this.model.get('prodRate').replace('$',''));
this.model.set({prodQty: newQty, amount: '$' + newAmt});
this.cancelEdit(e);
},
removeModel: function(e) {
e.preventDefault();
e.stopPropagation();
if(AW.collection.bag) AW.collection.bag.remove(this.model);
},
removeItem: function() {
this.$el.remove();
}
});
// Bag Quick View
var bagQuickView = Backbone.View.extend({
tagName: 'div',
id: 'myBagQV',
template: Handlebars.compile(bagQuickViewTemplate),
initialize: function() {
_.bindAll(this);
this.collection.fetch({reset:true});
//this.collection.bind("reset", _.bind(this.render, this));
this.listenTo(this.collection, 'add', this.addItem);
this.listenTo(this.collection, 'reset', this.render);
},
render: function() {
if($('#myBagQV').length == 0) {
this.$el.html(this.template());
$('body').append(this.el);
}
this.addAllItems();
return this;
},
addItem: function(item) {
var parent = this;
var itemView = new bagItemView({model: item});
$('#itemsInBag table tbody').append(itemView.render().el);
},
addAllItems: function() {
if(this.collection.length > 0) {
$('#itemsInBag table tbody').html('');
this.collection.each(this.addItem, this);
}
},
});
// Normal Bag View
var bagView = Backbone.View.extend({
tagName: 'div',
id: 'myBag',
template: Handlebars.compile(checkoutBagTemplate),
initialize: function() {
_.bindAll(this);
this.collection.fetch({reset:true});
//this.collection.bind("reset", _.bind(this.render, this));
this.listenTo(this.collection, 'add', this.addItem);
this.listenTo(this.collection, 'reset', this.render);
},
render: function() {
this.$el.html(this.template());
$('#checkoutContainer #details').append(this.el);
this.addAllItems();
return this;
},
addItem: function(item) {
var parent = this;
var itemView = new bagItemView({model: item});
this.$el.find('table tbody').append(itemView.render().el);
},
addAllItems: function() {
if(this.collection.length > 0) {
this.$el.find('table tbody').html('');
this.collection.each(this.addItem, this);
}
}
});
Looking for you help.
Thank you in advance
Cheers,
Vikram
I am doing a sample application similar to the Backbone-Todo. But when I am invoking destroy on collection it's giving error:
Uncaught TypeError: Cannot read property 'destroy' of undefined
How can I solve this problem. Please suggest.
Following is my method code:
$(function(){
var Todo = Backbone.Model.extend({
defaults: function() {
return {
title: "empty todo...",
order: Todos.nextOrder(),
done: false
};
}
});
var TodoList = Backbone.Collection.extend({
model : Todo,
localStorage: new Backbone.LocalStorage("todos-backbone"),
done: function() {
return this.where({done: true});
},
remaining: function() {
return this.without.apply(this, this.done());
},
nextOrder: function() {
if (!this.length) return 1;
return this.last().get('order') + 1;
},
comparator: 'order'
});
var TodoView = Backbone.View.extend({
tagName: "li",
template: _.template($('#item-template').html()),
events: {
"click a.destroy" : "clear"
},
initialize: function() {
this.listenTo(this.model, 'destroy', this.remove);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
clear: function(){
this.model.destroy();
}
});
var AppView = Backbone.View.extend({
el: $("#todoapp"),
statsTemplate: _.template($('#stats-template').html()),
events: {
"keypress #new-todo": "createOnEnter",
"click #remove-all": "clearCompleted"
},
initialize: function() {
this.input = this.$("#new-todo");
this.main = $('#main');
this.footer = this.$('footer');
this.listenTo(Todos, 'add', this.addOne);
this.listenTo(Todos, 'all', this.render);
Todos.fetch();
},
render: function() {
var done = Todos.done().length;
var remaining = Todos.remaining().length;
if (Todos.length) {
this.main.show();
this.footer.show();
this.footer.html(this.statsTemplate({done: done, remaining: remaining}));
} else {
this.main.hide();
this.footer.hide();
}
},
createOnEnter: function(e){
if(e.keyCode != 13) return;
if (!this.input.val()) return;
Todos.create({
title: this.input.val()
})
this.input.val('');
},
addOne: function(todo){
var view = new TodoView({model: todo});
this.$("#todo-list").append(view.render().el);
},
clearCompleted: function(){
_.invoke(Todos, 'destroy');
return false;
}
});
for this answer I assume Todos is an instance of TodoList. I also assume that your error is fired by this function in your AppView
clearCompleted: function(){
_.invoke(Todos, 'destroy');
return false;
}
In there you're trying to treat your Backbone.js Collection instance like what it is, a collection eg a list. But Backbone collections are not simply lists, they are objects that have the property models which is a list that contains all your models. So trying to use underscore's invoke (which works on lists) on an object is bound to cause errors.
But don't worry, Backbone neatly implements many Underscore methods for its Model and Collection, including invoke. This means you can invoke destroy for each model in a collection like this
SomeCollection.invoke('destroy');
Hope this helps!
Consider a backbone app that has a select list of categories, and a table of files underneath it. All files, regardless of the category they belong to are in the App.collection.files collection. When a user selects a new category, I'd like to filter the table to only show those matching files.
I'd like to do this completely on the client side. I do NOT want to fetch a new list of files from the server each time a new category is picked.
I plan to to a App.collection.files.where({category_id: xx}) within App.View.Files.render() to accomplish the filtering itself.
What is the best Backbone-y way to trigger/respond to an event to get this working?
One idea I am playing with is another collection, that contains the filtered files, but not sure if this is the best approach.
App.View.FilesPage = Backbone.View.extend({
template: App.utils.template('files-page'),
className: 'container-fluid',
initialize: function(){
this.render();
},
render: function(){
this.$el.html(this.template());
this.$el.find('.categories').append(
new App.View.Categories({collection:App.collection.categories}).el
);
this.$el.find('.files table').append(
new App.View.Files({collection:App.collection.files}).el
);
return this;
}
});
App.View.Categories = Backbone.View.extend({
tagName: 'select',
events: {
'change': 'onChange'
},
initialize: function(){
this.$el.attr('size', this.collection.length + 1);
this.render();
},
render: function(){
var option = function(value, text, isSelected){
return $('<option></option>', {'value':value, 'text':text});
};
this.$el.html(option(0, 'All Documents'));
this.$el.append(
this.collection.invoke(function(){
return option(this.get('category_id'), this.get('title'));
})
).val(0);
return this;
},
onChange: function(event){
}
});
App.View.Files = Backbone.View.extend({
tagName: 'tbody',
initialize: function(){
this.render();
},
render: function(){
this.$el.html(
this.collection.invoke(function(){
return new App.View.File({model:this}).el;
})
);
return this;
}
});
Here is one solution I came up with, that seems fine and dandy on the surface.
Code changes are called out within comments.
Thoughts on this solution, and completely different solutions are welcome.
App.View.FilesPage = Backbone.View.extend({
template: App.utils.template('files-page'),
className: 'container-fluid',
initialize: function(){
// CHANGE: Store application wide reference to 'Categories' model
App.model.categories = new App.Model.Categories();
this.render();
},
render: function(){
this.$el.html(this.template());
this.$el.find('.categories').append(
new App.View.Categories({
// CHANGE: Pass model into 'Categories' view
model: App.model.categories,
collection: App.collection.categories
}).el
);
this.$el.find('.files table').append(
new App.View.Files({collection:App.collection.files}).el
);
return this;
}
});
App.View.Categories = Backbone.View.extend({
tagName: 'select',
events: {
'change': 'onChange'
},
initialize: function(){
this.$el.attr('size', this.collection.length + 1);
this.render();
},
render: function(){
var option = function(value, text, isSelected){
return $('<option></option>', {'value':value, 'text':text});
};
this.$el.html(option(0, 'All Documents'));
this.$el.append(
this.collection.invoke(function(){
return option(this.get('category_id'), this.get('title'));
})
).val(0);
return this;
},
onChange: function(){
// CHANGE: Update attribute within 'Categories' model
this.model.set('category_id', this.$el.val());
}
});
App.View.Files = Backbone.View.extend({
tagName: 'tbody',
initialize: function(){
this.collection.on('reset', this.render, this);
// CHANGE: Listen for change on 'Categories' model, and re-render this view
App.model.categories.on('change:category_id', this.render, this);
this.render();
},
render: function(){
this.$el.html(
_.invoke(this.getFilteredFiles(), function(){
return new App.View.File({model:this}).el;
})
);
return this;
},
// CHANGE: Return a filtered set of models from collection, based on 'Categories' model attributes
getFilteredFiles: function(){
var categoryId = App.model.categories.get('category_id');
if (categoryId!=0){
return this.collection.where({'category_id': categoryId});
}
else{
return this.collection.models;
}
}
});
I am very new to Backbone and just trying to figure out the way things work. How do I iterate through the "fetched" results?
(function($) {
console.log('Formcontainer init.');
var Field = Backbone.Model.extend({
defaults: {
id: "0",
memberlist_id: "3",
field_name: "sample-field-name",
},
initialize: function() {
console.log('Field model init.');
}
});
var FieldCollection = Backbone.Collection.extend({
defaults: {
model: Field
},
model: Field,
url: 'http://localhost:8888/getstuff/3.json',
initialize: function() {
console.log('FieldCollection init.');
}
});
var ListView = Backbone.View.extend({
el: '#app-container',
initialize: function() {
_.bindAll(this,"render");
console.log('ListView init.')
this.counter = 0;
this.collection = new FieldCollection();
this.collection.fetch();
//this.collection.bind('reset', function() { console.log('xxx')});
//this.collection.fetch();
this.render();
},
events: {
'click #add': 'addItem'
},
render: function() {
var self = this;
console.log('Render called.');
},
addItem: function() {
this.counter++;
this.$("ul").append("<li>hello world" + this.counter + "</li>");
}
});
var listView = new ListView();
})(jQuery);
I get this in my Firebug console:
Formcontainer init.
ListView init.
FieldCollection init.
GET http://localhost:8888/getstuff/3.json 200 OK
Render called.
Field model init.
Field model init.
Field model init.
Field model init.
Field model init.
It seems that the fetch() was called - since "Field model init." is in the console 5 times. But how to I output that? I would want to append the items in an unordered list.
Thanks!
Bind the reset event to your render function...
this.collection.bind('reset', this.render, this);
This gets triggered after the fetch is complete, so you can create the list...
render: function() {
var self = this;
console.log('Render called.');
this.collection.each(function(i,item) {
this.$el.append("<ul>" + item.get("field_name") + "</ul>");
});
},