I am producing a single page website with Wordpress and Backbone.js, i have come into a problem with when i fetch new data. It simply adds DOM elements onto the container el rather than replacing them. The collection updates correctly as i can see it has the right amount of elements in the console.
var PostItem = Backbone.Model.extend();
var PostItems = Backbone.Collection.extend({
model: PostItem,
url: '/wp-admin/admin-ajax.php'
});
var postItems = new PostItems();
var PostView = Backbone.View.extend({ /* Model View */
tagName : 'article',
className : 'widget',
template : _.template( $('#widgetPost').html() ),
render: function(){
var attributes = this.model.toJSON();
this.$el.html( this.template( attributes ) );
return this;
}
});
var PostListView = Backbone.View.extend({ /* Collection View */
el : '#content',
initialize: function(){
this.collection.on('add', this.addOne, this);
this.collection.on('reset', this.addAll, this);
},
addOne: function(postItem){
var postView = new PostView({ model : postItem });
this.$el.append( postView.render().el );
},
addAll: function(){
this.collection.forEach(this.addOne, this);
},
render: function(){
this.addAll();
},
});
var postListView = new PostListView({
collection : postItems
});
$(function(){
$('a#posts').click(function(){
postItems.fetch({
data: {
action: 'do_ajax',
fn: 'get_the_posts'
}
});
return false;
});
$('a#pages').click(function(){
postItems.fetch({
data: {
action: 'do_ajax',
fn: 'get_the_pages'
}
});
return false;
});
});
You need to clear out your collectionView's $el! :)
addAll: function(){
this.$el.empty();
this.collection.forEach(this.addOne, this);
}
This should do the trick.
Related
I have backbone.js collection and collectionview. collection view listening to its collection add event. But when I add new models to it's collection it renders mutiple times for each model.
Please Check the JSFiddle
var ImageCollectioView = Backbone.View.extend({
initialize: function() {
this.collection.bind('add', this.render, this);
},
collection: imgColection,
el: '#cont',
render: function() {
var els = [], self = this;
this.collection.each(function(image){
var imageView = new ImageView({model: image});
self.$el.append(imageView.render().el);
});
return this;
}
});
Your render method renders the entire collection. So after adding a model you should clear the existing item views:
render: function() {
var els = [], self = this;
this.$el.empty();
//------^---- clear existing
this.collection.each(function(image){
var imageView = new ImageView({model: image});
self.$el.append(imageView.render().el);
});
return this;
}
That being said, it's better to add a separate method that just appends single item view rather than rendering the entire collection:
var ImageCollectioView = Backbone.View.extend({
initialize: function() {
this.render();
this.listenTo(this.collection, 'add', this.renderItem);
},
el: '#cont',
render: function() {
this.collection.each(this.renderItem, this);
return this;
},
renderItem: function(image) {
var imageView = new ImageView({
model: image
});
this.$el.append(imageView.el);
}
});
Updated Fiddle
I am really new to backbone, and in a world of confusion behind it. Right now I am trying to render a view of a single person, preferably without iterating through the entire collection, using the collection.get() method.
Here is the user collection:
App.Collections.UserCollection = Backbone.Collection.extend({
url: "/users",
model: App.Models.User,
initialize: function(){
console.log('users collection');
},
});
The user model:
App.Models.User = Backbone.Model.extend({
rootURL: '/users',
initialize: function(){
console.log('User model being generated');
}
});
the Users View (all users):
App.Views.Users = Backbone.View.extend({
el: "body",
model: 'user',
initialize: function(){
console.log('User view rendering');
this.collection.fetch();
this.listenTo(this.collection, 'reset', this.addAll);
},
clearDiv: function(){
var container = $('#render-area');
$('body').removeClass('homepage');
$('#main-nav').fadeIn(100);
container.empty();
this.addUser();
},
addUser: function(){
var current_user_model = this.collection.get(currentUser);
var user = new App.Views.ViewUser({ model: current_user_model });
$('#render-area').append(user.el);
},
events: {
'click #view-profile' : 'clearDiv'
}
});
and the viewUser view (single user):
App.Views.ViewUser = Backbone.View.extend({
initialize: function(){
console.log("single user view");
this.template = HandlebarsTemplates['user_profile'];
this.render();
},
render: function(){
$('#render-area').html(this.template(this.model.toJSON()));
},
});
I thought that I was grabbing the single user model in the Users view, and passing it to the ViewUser view with these lines
var current_user_model = this.collection.get(currentUser);
var user = new App.Views.ViewUser({ model: current_user_model });
But it seems that I am not. Any help and/or explanation is appreciated, thanks!
did you check the this.collection.get is return an object?
Maybe it can not fetch the currentUser?
before render the view you should
a) check the model is a valid model before calling the view and render a "not found" view or
b) in the view check if the model is valid and render a "not found" view
c) or you can handle it in handlebars template
Example A (untested):
var current_user_model = this.collection.get(currentUser);
if (current_user_model) {
var user = new App.Views.ViewUser({ model: current_user_model });
} else {
new AppViews.ViewUserNotFound();
}
Example B :
App.Views.ViewUser = Backbone.View.extend({
initialize: function(){
console.log("single user view");
this.template = HandlebarsTemplates['user_profile'];
this.templateNotFound = HandlebarsTemplates['user_profile_not_found'];
this.render();
},
render: function(){
if(this.model) {
$('#render-area').html(this.template(this.model.toJSON()));
} else {
$('#render-area').html(this.templateNotFound());
}
},
});
var current_user_model = this.collection.get(currentUser);
var user = new App.Views.ViewUser({ model: current_user_model });
Example C:
App.Views.ViewUser = Backbone.View.extend({
initialize: function(){
console.log("single user view");
this.template = HandlebarsTemplates['user_profile'];
this.templateNotFound = HandlebarsTemplates['user_profile_not_found'];
this.render();
},
render: function(){
if(!this.model) {
this.model = new this.model(); // create model with defaults
}
$('#render-area').html(this.template(this.model.toJSON()));
},
});
var current_user_model = this.collection.get(currentUser);
var user = new App.Views.ViewUser({ model: current_user_model });
all examples untested..
hope this helps a little bit,
best Carsten
I would do that:
App.Views.Users = Backbone.View.extend({
el: "body",
model: 'user',
initialize: function(){
console.log('User view rendering');
this.listenTo(this.collection, 'reset', this.addAll);
},
clearDiv: function(){
var self = this;
this.collection.fetch({
success: function() {
var container = $('#render-area');
$('body').removeClass('homepage');
$('#main-nav').fadeIn(100);
container.empty();
self.addUser();
}
});
},
addUser: function(){
var current_user_model = this.collection.get(currentUser);
var user = new App.Views.ViewUser({ model: current_user_model });
$('#render-area').append(user.el);
},
events: {
'click #view-profile' : 'clearDiv'
}
});
I am trying to use collections to list my data coming from my api.
But the problem is, when I use forEach, the function that I called (addOne) doesn't run.
There is also something I suspect working wrong. Should my collection save the returning JSON under the models like that?
Object -> models -> 0 -> attributes -> ...
My View:
s.Views.Fs = Backbone.View.extend({
className: "",
template: _.template("<%= name %>"),
initialize: function() {
},
render: function() {
this.collection.forEach(this.addOne, this);
},
addOne: function(f) {
alert("a");
var fV = new s.Views.PF({model: f});
this.$el.append(fV.render().el);
}
});
My Collection:
s.Collections.FL = Backbone.Collection.extend({
url: "api/fs/",
model: s.Models.F,
});
My Model:
s.Models.F = Backbone.Model.extend( {
urlRoot: 'api/fs/',
defaults: {
...
},
...
parse: function(response) {
return response;
},
});
My Route (And App):
var sApp = new (Backbone.Router.extend({
f_a: function() {
this.fL= new s.Collections.FL();
this.fLV= new s.Views.Fs({collection: this.fL});
this.fL.fetch();
this.fLV.render();
},
});
Listening for events is made by this.collection.on('add', this.addOne, this); under collection's view. Here is tested code's summary (Thanks for the tip 'mu is too short'):
VIEW
s.Views.Fs = Backbone.View.extend({
className: "",
template: _.template("<%= name %>"),
initialize: function() {
this.collection.on('add', this.addOne, this);
this.collection.on('reset', this.render, this);
},
render: function() {
this.collection.forEach(this.addOne, this);
},
addOne: function(f) {
var fV = new s.Views.PF({model: f});
fV.render();
this.$el.append(fV.el);
}
});
COLLECTION
s.Collections.FL = Backbone.Collection.extend({
url: "api/fs/",
model: s.Models.F,
});
MODEL
s.Models.F = Backbone.Model.extend( {
urlRoot: 'api/fs/',
// No need to parse here.
});
ROUTER
var sApp = new (Backbone.Router.extend({
f_a: function() {
this.fL= new s.Collections.FL();
this.fLV= new s.Views.Fs({collection: this.fL});
this.fLV.render();
$("#content").html(this.fLV.el);
this.fL.fetch();
},
});
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 have a View whit nested views inside that when rendered has a link on it with a click event attached. First time I click on al link, all works. But when re-render all views, links events are disappeared. I tried forcing delegateEvents() in render and remove views before re-render but nothing.
Here is my code:
var SlideView = Backbone.View.extend({
tagName: 'li',
events: {
'click .nested':'destroy'
},
template: _.template($('#slides-nested-template').html()),
render: function(e){
var _el = $(this.el);
_el.html(this.template(this.model.toJSON()));
this.delegateEvents(this.events);
return this;
},
destroy: function(e){
e.preventDefault();
Presentations.eliminate($(e.target).attr('rel'));
}
});
var SectionView = Backbone.View.extend({
tagName: 'li',
template: _.template($('#slides-template').html()),
events: {
'click .no-nested':'destroy'
},
initialize: function(options){
this.views = [];
_.bindAll(this, 'destroy');
},
render: function(){
var _el = $(this.el), self=this;
_el.html(this.template(this.model.toJSON()));
_ul = this.$('ul');
var data = Presentations.filter(function(slide){
return slide.get('padre') == self.model.get('id');
});
_.each(data, function(slide){
view = new SlideView({
model: slide
});
self.views.push(view);
_ul.append(view.render().el);
});
this.delegateEvents(this.events);
return this;
},
destroy: function(e){
e.preventDefault();
Presentations.eliminate($(e.target).attr('rel'));
},
removeViews: function(){
while (this.views.length){
var view = this.views.shift();
view.undelegateEvents();
view.remove();
view = null;
}
}
});
var SectionList = Backbone.View.extend({
tagName: 'ul',
className: 'sortable',
initialize: function(options){
Presentations.on('add', this.render, this);
Presentations.on('reset', this.render, this);
Presentations.on('remove', this.render, this);
this.views = [];
},
render: function(e){
//this.removeViews();
$('#slides .content').empty();
var data = Presentations.filter(function(slide){
return slide.get('padre') === false;
});
var view, self=this, _el=$(this.el);
_.each(data, function(slide){
view = new SectionView({
model: slide
});
_el.append(view.render().el);
self.views.push(view);
});
$('#slides .content').append(_el);
return this;
},
removeViews: function(){
while (this.views.length){
var view = this.views.shift();
view.undelegateEvents();
view.removeViews();
view.remove();
view = null;
}
}
});
var Presentation = Backbone.Model.extend({
});
var PresentationList = Backbone.Collection.extend({
model: Presentation,
url: BASE_URL+'api/slides/'+PRESENTATION,
eliminate: function(id){
this.each(function(slide){
if (slide.get('padre')==id){
slide.set('padre', false);
slide.save();
}
});
var ref = this.get(id);
ref.destroy();
this.remove(ref);
}
});
Presentations = new PresentationList();
SectionListView = new SectionList();
Presentations.fetch();