The goal: Use Backbone to generate a table of users, be able to edit those users inline, and on save, re-render the row with the updated data.
I'm generating a container table for users with a main UsersView.
I'm then looping through a users collection, and using a UserView to generate one table row per user.
Clicking edit on any row needs to replace the row with an edit form. (There are more fields associated with a user than are displayed in the table.)
Clicking Save should replace the edit form with the updated table row.
Everything works except the re-render of the table row. (If I reload the page, the updated user data is displayed correctly, so I know it's saving.) What I'm trying to do (I think) is get the parent view (UsersView) to listen to the change and re-render the UserView. After dozens of searches and different attempts, I'm here asking for help as to what I'm doing wrong. Thanks in advance.
// Main users view
var UsersView = Backbone.View.extend( {
el: '#content',
template: _.template( usersTemplate ),
initialize: function( ) {
var that = this;
this.listenTo( Users, 'change', this.updateOne );
Users.fetch( );
that.render( );
},
render: function( ) {
this.$el.html( this.template );
},
// Add a single user to the table
addOne: function( user ) {
var userView = new UserView( { model: user } );
userView.render( ).el;
},
// Add all items in the Users collection
addAll: function( ) {
Users.each( this.addOne, this );
}
});
// Individual user view
var UserView = Backbone.View.extend({
el: '#usersTableBody',
tagName: 'tr',
attributes : function( ) {
return {
'data-user' : this.model.get( 'display_name' )
};
},
template: _.template( userTemplate ),
events: {
'click .editUser': 'editUser'
},
initialize: function( ){ },
render: function( ) {
var html = this.template( this.model.attributes );
this.$el.html( html );
},
// Switch user row into edit mode
editUser: function( e ) {
e.preventDefault( );
var userAddEditView = new UserAddEditView( { model: this.model } );
userAddEditView.render( );
}
});
// User edit view
var UserAddEditView = Backbone.View.extend({
el: $( '#content' ),
template: _.template( userEditTemplate ),
events: {
'click .saveUser': 'saveUser'
},
initialize: function( options ) { },
render: function( ) {
element = '[data-user="' + this.model.get( 'display_name' ) + '"]'
this.$el.find( element ).hide( ).after( this.template( this.model.attributes ) );
},
saveUser: function( ) {
var display_name = this.$( '#display_name' ).val( ).trim( );
var email_address = this.$( '#email_address' ).val( ).trim( );
var key_email_address = this.$( '#key_email_address' ).val( ).trim( );
var password = this.$( '#password' ).val( ).trim( );
var partner_name = this.$( '#partner_name' ).val( ).trim( );
var hash = this.$( '#hash' ).val( ).trim( );
var salt = this.$( '#salt' ).val( ).trim( );
Users.create( { display_name: display_name, key_email_address: key_email_address, email_address: email_address, password: password, partner_name: partner_name, hash: hash, salt: salt } );
}
});
// User model
var UserModel = Backbone.Model.extend( {
idAttribute: 'key_email_address'
});
// Users collection
var UsersCollection = Backbone.Collection.extend( {
model: User,
url: 'users',
initialize : function( models, options ) { },
parse: function( data ) {
this.result = data.result;
return data.users;
}
});
There is no change event fired when any changes occur on the items of a collection.
So your following line shouldn't really do anything:
this.listenTo( Users, 'change', this.updateOne );
you need to include the following in your UserView initialize method:
this.listenTo( this.model, 'change', this.render );
so now whenever the model is changed (on editing), the current user row is rendered again.
Related
I am using the save method when my data is submitted. On success callback of the save method, the collection should be updated with the model which i have saved since i want to get the id of the model from my server. My code is as below
var app = app || {};
app.AllDoneView = Backbone.View.extend({
el: '#frmAddDone',
events:{
'click #addDone':'addDone'
},
addDone: function(e ) {
e.preventDefault();
var formData = {
doneHeading: this.$("#doneHeading").val(),
doneDescription: this.$("#doneDescription").val(),
};
var donemodel = new app.Done();
donemodel.save(formData,
{
success :function(data){
/*my problem is here how do i listen to collection event add that has been
instantiated in intialize property to call renderDone . My tried code is
var donecollection = new app.AllDone();
donecollection.add(donemodel);
and my response from server is
[{id:145, doneHeading:heading , doneDescription:description,
submission_date:2014-08-27 03:20:12}]
*/
},
error: function(data){
console.log('error');
},
});
},
initialize: function() {
this.collection = new app.AllDone();
this.collection.fetch({
error: function () {
console.log("error!!");
},
success: function (collection) {
console.log("no error");
}
});
this.listenTo( this.collection, 'add', this.renderDone );
},
renderDone: function( item ) {
var doneView = new app.DoneView({
model: item
});
this.$el.append( doneView.render().el );
}
});
Collection is
var app = app || {};
app.AllDone = Backbone.Collection.extend({
url: './api',
model: app.Done,
});
Model is
var app = app || {};
app.Done = Backbone.Model.extend({
url: "./insert_done",
});
View is
var app = app || {};
app.DoneView = Backbone.View.extend({
template: _.template( $( '#doneTemplate' ).html() ),
render: function() {
function
this.$el.html( this.template( this.model.attributes ) );
return this;
}
});
In your success callback you create an entirely new collection, which doesn't have any listeners registered. This is the reason why the renderDone isn't triggered.
The model you receive from the server should be added to the collection which is attached directly to your view, this.collection:
var self = this,
donemodel = new app.Done();
donemodel.save(formData, {
success :function(data){
// this is the collection you created in initialize
self.collection.add(donemodel);
},
error: function(data){
console.log('error');
}
});
I'm facing a very weird problem using ListenTo on subviews.
Basically I've a main view that contains multiple subviews.
Those subviews are initialized when their parent view is also initialized.
Some of those subviews are listening to the same global collection.
One of those views is a form that allow me to insert new entries into the collection
The second one is a list of all those entries contained into the collection.
So, basically it looks like this
$('button').on('click'....
app.views.MyMainView = new MyMainView()
//launch new modal window
//with html body = app.views.MyMainView.el
var MyMainView = Backbone.view.extend({
initialize: function(){
// new Form_View()
// new List_View()
}
});
var Form_View = Backbone.view.extend({
//get input value
// create new entrie into the collection
});
var List_View = Backbone.view.extend({
initialize: function(){
// listenTo(this.collection, 'add', this.addOne)
// new List_View()
this.addAll();
},
addAll: function(){...},
addOne: function(model){ .... }
});
The problem is the following:
When the user launch the modal for the first time and we add a new entry to the collection
the listenTo add on the List_View fires as expected.
If I close the modal and the user clicks on 'button' to launch the modal window once again if I add a new entrie the view will fire 2 times the function that is listening to the collection add event.
If i close the modal again and re-open it the function will fire 3 times and so on.
This is weird because I'm creating a new instance of the view and their subviews everytime the user clicks on the 'button'. That's why it doesn't make sense to me.
Any help?
EDIT
I also checked my collection by listening to the 'add' event inside of it.
var MyCollection = Backbone.View.extend({
initialize: function(){
this.listenTo( this, 'add', doSomething );
},
doSomething: function( model ){
//do something fires as it should be firing the event: 1 time per each item inserted
}
})
EDIT 2
var MyMainView = Backbone.View.extend({
.....
close_modal: function(e){
if(e){
e.preventDefault();
}
var viewsLen = this.views.length,
_that = this;
_.each(this.views, function(view, key){
view.remove();
if(key + 1 == viewsLen )
_that.dialog.close();
})
}
...
})
EDIT 3:
ALL CODE
//initialize modal
$('button').on('click', function(){
app.views.storePayment_View = new StorePayment_View();
})
var TMPL_StorePayment = '<div class="store-payment">'
+ '<div class="store-payment-header">'
+ '<div class="client"></div>'
+ '<div class="status"></div>'
+ '</div>'
+ '<div class="payment-form"></div>'
+ '<div class="payment-list"></div>'
+ '<div class="x-form-actions">'
+ '</div>'
+ '</div>';
var StorePayment_View = Backbone.View.extend({
views: {},
wrappers: {},
collections: {},
events: {
"click .back": "close_modal",
"click .finish-payment": "finish_payment"
},
initialize: function(){
var _that = this;
this.dialog = new BootstrapDialog({
title: appLang["h67"],
message: this.$el,
closable: true,
onhide: function(dialogRef){
_that.remove();
}
});
this.dialog.realize();
this.dialog.getModalFooter().hide();
this.dialog.open();
this.$el.html('').append( TMPL_StorePayment );
this.wrappers.$client = this.$el.find('.client');
this.wrappers.$status = this.$el.find('.status');
this.wrappers.$payment_form = this.$el.find('.payment-form');
this.wrappers.$payment_list = this.$el.find('.payment-list');
this.wrappers.$form_actions = this.$el.find('.x-form-actions');
this.render()
},
render: function(){
this.views.StorePaymentForm_View = new StorePaymentForm_View();
this.wrappers.$payment_form.html('').append( this.views.StorePaymentForm_View.el );
this.views.StorePaymentList_View = new StorePaymentList_View();
this.wrappers.$payment_list.html('').append( this.views.StorePaymentList_View.el );
},
close_modal: function(e){
if(e){
e.preventDefault();
}
var viewsLen = this.views.length,
_that = this;
_.each(this.views, function(view, key){
view.remove();
if(key + 1 == viewsLen ){
_that.dialog.close();
}
})
}
})
var StorePaymentForm_View = Backbone.View.extend({
error_tmpl: _.template('<div class="alert alert-warning"><%= message %></div>') ,
template: _.template('<div> <div class="input-field"> <input type="text" class="montant form-control" value="<%= restant %>"> </div> <div class="input-select"> <select name="payment-type"><% _.each(paymentTypeList, function(paymentType){ %> <option value="<%= paymentType.typeMode %>"><%= paymentType.libelle %></option> <% }) %></select> </div> <div class="actions">Add newRemove All </div></div><div class="error_placeholder"></div>'),
events:{
"click .add-new": "add_new",
"click .remove-all": "remove_all"
},
initialize: function(){
this.collection = app.collections.StorePaymentList;
this.listenTo( this.collection, 'add', this.render )
this.listenTo( this.collection, 'destroy', this.render )
this.listenTo( this.collection, 'change', this.render )
this.listenTo( this.collection, 'reset', this.render )
this.render()
},
render: function(){
console.log("RENDER FIRED ON STOREPAYMENTFORM")
var restant = this.collection.getRestant();
if ( restant <= 0){
restant = 0;
}
this.$el.html('').append( this.template( { "restant" : restant, "paymentTypeList": app.collections.PaymentTypeList.toJSON() } ) )
var _that = this;
setTimeout(function(){
_that.$el.find('select').selectBoxIt({
native: true,
autoWidth: false
})
_that.$el.find('input').focus();
}, 50 )
},
add_new: function(e){
console.log("add_new");
if(e){
e.preventDefault();
}
var _that = this,
input_val = this.$el.find('input').val(),
select_val = this.$el.find('select :selected').val(),
libelle = this.$el.find('select :selected').text(),
wasNaN = false;
input_val = parseInt(input_val);
if (isNaN(input_val)){
wasNaN = true;
input_val = 0;
}
if (wasNaN){
_that.$el.find('.error_placeholder').html( _that.error_tmpl( { "message": appLang["h69"] } ) );
} else {
if ( input_val <= 0 ){
_that.$el.find('.error_placeholder').html( _that.error_tmpl( { "message": appLang["h70"] } ) );
} else {
this.collection.add( new StorePaymentModel( { "libelle": libelle , "paymentId": select_val, "montant": input_val } ) )
}
}
},
remove_all: function(e){
if(e){
e.preventDefault();
}
var _that = this;
//dialog are you sure?
var dialog = new BootstrapDialog({
title: "Do you want to continue",
message: "Do you really want to empty your current list of payments?",
buttons: [{
label: appLang["a187"], //cancel
action: function(dialog) {
dialog.close();
}
}, {
label: appLang["a1621"], //ok
cssClass: 'btn-primary',
action: function(dialog) {
_that.collection.reset([]);
dialog.close();
}
}]
})
dialog.realize();
dialog.open();
}
})
var StorePaymentListItem_View = Backbone.View.extend({
events:{
"click .remove": "remove_item",
"click .save": "save"
},
template: _.template( '<%= libelle %> <%= montant %> <i class="fa fa-trash-o"></i>' ),
tagName: 'li',
className: 'list-group-item',
initialize: function(){
this.render()
//console.log("StorePaymentListItem_View initialized")
this.listenTo( this.model, 'hide', this.remove )
},
render: function(){
this.$el.html('').append( this.template( this.model.toJSON() ) )
},
edit: function(){
this.render_edit();
},
save: function(e){
if (e){
e.preventDefault(); e.stopPropagation();
}
this.render();
},
remove_item: function(e){
if (e){
e.preventDefault(); e.stopPropagation();
}
this.model.destroy();
}
})
var StorePaymentList_View = Backbone.View.extend({
$wrapper: $('<ul />', {'class': 'list-group' }),
initialize: function(){
this.$el.html('');
this.collection = app.collections.StorePaymentList;
this.listenTo( this.collection , 'add', this.addOne );
this.listenTo( this.collection , 'change', this.render );
this.listenTo( this.collection , 'reset', this.render );
this.render()
},
render: function(){
var totalItems = this.collection.length;
this.$wrapper.html('')
if (totalItems == 0){
this.appendToRoot();
} else {
this.addAll()
}
},
addAll:function(){
var _that = this,
totalItems = this.collection.length;
this.collection.forEach(function(model, key){
_that.addOne(model)
if (totalItems == key + 1)
_that.appendToRoot();
})
},
addOne:function( model ){
var storePaymentListItem_View = new StorePaymentListItem_View({ model: model });
this.$wrapper.append( storePaymentListItem_View.el );
},
appendToRoot:function(){
this.$el.html('').append( this.$wrapper );
}
})
My guess is closing the modal doesn't call view.remove, and so we end up with a zombie view that is removed from the DOM but still lives in memory listening to events.
Another guess is you never remove the List_View instance, so they are the zombies.
In other words, this is likely to be related to insufficient garbage collection.
This is a guess - it's impossible to tell without seeing the relevant parts of the code.
I have the following view and I'm trying to bind a click event to a button with the id of #add. However, it won't even register the preventDefault function.
var app = app || {};
app.LibraryView = Backbone.View.extend({
el: '#books',
events: {
'click #add' : 'addBook'
},
initialize: function() {
this.collection = new app.Library();
this.render();
this.listenTo( this.collection, 'add', this.renderBook);
},
render: function() {
this.collection.each(function( item ) {
this.renderBook( item );
}, this );
},
renderBook: function( item ) {
var bookView = new app.BookView({
model: item
});
this.$el.append( bookView.render().el );
},
addBook: function(e) {
e.preventDefault();
var formData = {};
$('#addBook div').children('input').each(function(index, el) {
if($(el).val() != '') {
formData[el.id] = $(el).val();
}
});
this.collection.add(new app.Book(formData));
console.log("book added");
}
});
Also, I can call renderBook in the console and it will add a new bookView, however, the render function doesn't seem to be working when the page loads initially.
Here is my html in jade.
block content
#books
form#addBook(action'#')
div
label(for='coverImage').span5 Image URL:
input#coverImage(type='text')
label(for='title').span5 Title:
input#title(type='text').span5
label(for='author') Author:
input#author(type='text').span5
label(for='date') Date:
input#date(type='text').span5
label(for='tags') Tags:
input#tags(type='text').span5
button#add Add
.bookContainer
ul
li Title
li Author
li Date
button Delete
You have to instantiate your view new app.LibraryView() in order for your view's events to be binds.
I have problem with render of collection. Its simple model with title and boolean 'completed', when you click on list item it's changing to completed value (true/false). Value is changed ( I know it because when I refresh page, in initialize after fetch() I have collection.pluck, where order made by comparator is correct), but view looks all the time the same.
In collection I have comparator which works like I described upper, after collection.fetch() I have pluck, and pluck gives me well sorted list (but in view I see bad, default order). I dont know how to refresh collection to be well sorted.
Collection is just:
var TodolistCollection = Backbone.Collection.extend({
model: TodoModel,
localStorage: new Store('todos-backbone'),
// Sort todos
comparator: function(todo) {
return todo.get('completed');
}
});
Model is:
var TodoModel = Backbone.Model.extend({
defaults: {
title: '',
completed: false
},
// Toggle completed state of todo item
toggle: function(){
this.save({
completed: !this.get('completed')
});
}
});
return TodoModel;
Single todoView is:
var TodoView = Backbone.View.extend({
tagName: 'li',
template: JST['app/scripts/templates/todoView.ejs'],
events: {
'click .js-complete': 'toggleCompleted'
},
initialize: function(){
this.listenTo(this.model, 'change', this.render);
},
render: function() {
this.$el.html( this.template( this.model.toJSON() ));
this.$el.toggleClass( 'l-completed', this.model.get('completed') );
return this;
},
// Toggle the `"completed"` state of the model.
toggleCompleted: function() {
this.model.toggle();
}
and app View:
var ApplicationView = Backbone.View.extend({
el: $('.container'),
template: JST['app/scripts/templates/application.ejs'],
events: {
'click #add' : 'createOnEnter',
'keypress #addTodo' : 'createOnEnter'
},
// aliasy do DOMu,
// nasluchiwanie w kolekcji czy zaszlo jakies zdarzenie, jesli tak, to wykonuje funkcje
initialize: function() {
this.$input = this.$('.js-input');
this.listenTo(todoList, 'add', this.addOne);
this.listenTo(todoList, 'reset', this.addAll);
this.listenTo(todoList, 'all', this.render);
todoList.fetch();
console.log(todoList.pluck('title'));
},
render: function() {
},
// Generate the attributes for a new Todo item.
newAttributes: function() {
return {
title: this.$input.val().trim(),
completed: false
};
},
// Tworzy nowy model dzieki newAtributes() do localStorage
addTodo: function ( e ) {
e.preventDefault();
if( e.which !== Common.ENTER_KEY || !this.$input.val().trim() ){
return;
}
todoList.create( this.newAttributes() );
this.$input.val('');
},
// Tworzy model i dopisuje go do listy
addOne: function( todo ){
var view = new todoView({ model: todo });
$('.js-todolist').append( view.render().el );
},
// Tworzy nowego todo gdy nacisniemy enter
createOnEnter: function( e ) {
if( e.which !== Common.ENTER_KEY || !this.$input.val().trim() ){
return;
}
todoList.create( this.newAttributes() );
this.$input.val('');
},
// Przy rerenderze, dodaj wszystkie pozycje
addAll: function() {
this.$('.js-todolist').html('');
todoList.each(this.addOne, this);
}
});
return ApplicationView;
When I change listenTo render like that: this.listenTo(todoList, 'all', function(){console.log('whateva')}); I can see that on my click 'all' is triggering (even three times per one click ;s ).
Its hard for me to put it on jsfiddle, but here's git link with all files: https://github.com/ozeczek/ozeczek/tree/master/bb-todo-yo/app/scripts
In app view initialize I've changed todoList.fetch() to todoList.fetch({reset:true});
Second problem was to show in browser right order of todos, I've added to initialize:
this.listenTo(todoList, 'change', this.walcze);
this.listenTo(todoList, 'remove', this.walcze);
and walcze body function is:
walcze: function(){
todoList.sort();
this.$('.js-todolist').html('');
todoList.each(this.addOne, this);
}
Now every time Todo paremeter complete is changed, Im sorting list (comparator by itself isn't), clearing div with list, and rewriting whole list. I think it is not the best way of doing it, but it works.
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.