I don't understand the difference between putting a "click" listener in an initialize function inside a view and putting it in the events object in the same view. They both listen for DOM events and trigger functions, right? What's the difference?
for example:
var ViewName = Backbone.View.extend({
initialize: function(){
this.$el.on("eventName", this.functionName, this)
},
functionName: function(){
//whatever
}
});
versus:
var ViewName = Backbone.View.extend({
events: { "eventName": "fucntionName" }
},
functionName: function(){
//whatever
}
});
When you do:
var ViewName = Backbone.View.extend({
initialize: function(){
this.$el.on("eventName", this.functionName, this)
},
functionName: function(){
//whatever
}
});
You have to manually unbind the event when the view is being removed. So, you would have to do something like:
var ViewName = Backbone.View.extend({
initialize: function(){
this.$el.on("eventName", this.functionName, this)
},
functionName: function(){
//whatever
},
remove: function() {
this.$el.off("eventName", this.functionName);
Backbone.View.prototype.remove.apply(this, arguments);
}
});
If you use the events hash, Backbone takes care of undelegating events when the view is removed. This is all explained in this section of the Backbone.js annotated source.
Related
I have a problem when I call a REST service by POST to updates tables in MySQL.
I'm working with BackboneJS and when click Save button, call the service and passed POST parameters
$.ajax({
type: 'POST',
url: rootURL,
data: dataJson, //data send to REST service by POST
cacheControl: "no-cache",
dataType: "json",
success: function(data){
console.log("OK");
},
error: function(data){
console.log(data.msg);
}
});
But the problem occurs when income and leave the editing screen. Assuming I made a change and it worked perfectly bringing me back a single "OK", when I go back into the editing screen and record again, I get twice the word "OK".
If I repeat this step to enter and exit the screen edition, duplicate responses are based on the number of times into the screen edition.
I don't know if it's a problem about I'm doing wrong with BackboneJS...?? I'm doing this:
editAdverts: function(){
var editAdvertsView = new EditAdvertsView ();
$('#container-page').append(editAdvertsView.render(idAdverts).el);
}
It can also be a topic of AJAX?
I hope someone can help me with this issue because I am not an expert in BackboneJS
Thanks a lot!
Diego
It's hard to tell without more code but I'm pretty sure it's because you are creating a model each time you are using editAdvert.
Instead of creating a model each time just create one at the init of your view then call the fetch/save function anytime you need an update. Creating the model once is enough.
edit: zombies views is a good thing to check too as brent noticed in comment, we can't tell without more code.
This is de code about route:
var Index = {
start: function() {
var list_view = new MainView();
}
};
var AppRouter = Backbone.Router.extend ({
routes: {
'goAdvert' : 'goAdvert'
},
goAdvert: function(){
var viewAdvertsView = new ViewAdvertsView();
$('#container-page').append(viewAdvertsView.render().el);
}
});
var MainView= Backbone.View.extend({
el: $('#contenedor-body'),
initialize: function() {
this.render();
},
render: function() {
// here is the code about main page. It's not important!
},
});
new AppRouter;
Index.start();
Backbone.history.start();
This is the viewAdvertsView.js
define([
'jquery',
'underscore',
'backbone',
'jqueryuniform',
'bootstrap',
'handlebars',
'jqueryDataTables',
'dtBootstrap',
'../../view/editAdvertsView',
], function($, _, Backbone, jqueryuniform, Bootstrap, Handlebars, JQueryDataTables, DtBootstrap, EditAdvertsView){
var viewAdverts = Backbone.View.extend({
events: {
'click #edit' : 'editAdvert'
}
editAdvert: function(){
$('#container-page').empty();
var idAdvert = $(event.target).data('id');
editAdvertView = new EditAdvertsView();
$('#container-page').append(editAdvertView.render(idAdvert).el);
},
render: function() {
// load viewAdverts
}
});
return viewAdverts;
}
);
And this is editAdvertsView.js
define([
'jquery',
'underscore',
'backbone',
'jqueryuniform',
'bootstrap',
'handlebars',
'jqueryDataTables',
'functions',
'sessionManage',
'slimscroll',
'jqueryCustomSlimscroll',
'slimscrollMin',
'jqueryblockui',
'text!../../html/editAdverts.html',
'maps',
'fancybox'
], function($, _, Backbone, jqueryuniform, Bootstrap, Handlebars, JQueryDataTables,
Functions, SessionManage, Slimscroll, JqueryCustomSlimscroll, SlimscrollMin,
Jqueryblockui, EditAdverts, Maps, Fancybox){
var Advert= Backbone.View.extend({
el: $('#container-page'),
events: {
'click #saveChanges' : 'doSaveChanges'
},
doSaveChanges: function(){
var data = null;
var rootURL = "http://localhost/php/slim/slim/advert/update" + "?date=" + $.now();
dataJson = {
id: $("#id").val(),
product: $("#product").val(),
price: $("#price").val(),
client: $("#client").val(),
country: $("#country").val(),
tel: $("#telephone").val(),
cel: $("#cellphone").val()
};
$.ajax({
type: 'POST',
url: rootURL,
data: dataJson,
cacheControl: "no-cache",
dataType: "json",
success: function(data){
console.log("OK");
error: function(data){
console.log(data.msg);
}
}
},
render: function(codInmueble) {
var self = this;
var editTemplate = Handlebars.compile(EditAdverts);
self.$el.html(editTemplate());
return this;
}
});
return Advert;
}
);
This is a simple todo app that currently only lists a few li views within a ul collection view. I am trying to cause the click event to fire a simple showAlert function
If I keep my mouse over one of the task views(li) and refresh the page so that my mouse ends up hovering over that specific li once reloaded, the click event will fire off my showAlert function.
The problem is once I move my mouse(most likely triggering a mouseover event), I lose the click event on all task views(li) including the view I was initially hovering over.
Based on all posts I could find closely related to this problem, I've tried using this.delegateEvents() in various places, with no luck.
Preceding code
(function() {
window.App = {
Models: {},
Collections: {},
Views: {}
};
window.template = function(id) {
return _.template( $('#' + id).html() );
};
App.Models.Task = Backbone.Model.extend({});
App.Collections.Tasks = Backbone.Collection.extend({
model: App.Models.Task
});
App.Views.Tasks = Backbone.View.extend({
tagName: 'ul',
render: function() {
this.collection.each(this.addOne, this);
return this;
},
addOne: function(task) {
var taskView = new App.Views.Task({ model: task });
this.$el.append(taskView.render().el);
}
});
This is the view in question
App.Views.Task = Backbone.View.extend({
tagName: 'li',
events: {
'click': 'showAlert'
},
showAlert: function() {
alert('yes!');
},
render: function() {
this.$el.html( this.model.get('title') );
return this;
}
});
proceeding code
var tasksCollection = new App.Collections.Tasks([
{
title: 'Task 1',
priority: 3
},
{
title: 'Task 2',
priority: 4
},
{
title: 'Task 3',
priority: 5
}
]);
var tasksView = new App.Views.Tasks({ collection: tasksCollection });
$('.tasks').html(tasksView.render().el);
})();
I am rather new to backbone and wanted to test a simple script that handles a to do list. Here is the code i used so far:
(function() {
window.App = {
Models: {},
Collections: {},
Views: {}
};
window.template = function(id) {
return _.template($('#' + id).html());
}
App.Models.Task = Backbone.Model.extend({
validate: function(attributes) {
if ( !$.trim(attributes.title) ) {
return 'Invalid title';
}
}
});
App.Collections.Tasks = Backbone.Collection.extend({
model: App.Models.Task
});
App.Views.Task = Backbone.View.extend({
tagName: 'li',
template: template('taskTemplate'),
initialize: function () {
this.model.on('change', this.render, this);
this.model.on('destroy', this.remove, this);
},
events: {
'click .edit': 'editTask',
'click .delete': 'destroy'
},
destroy: function() {
if (confirm('Are you sure?')) {
this.model.destroy();
}
},
remove: function() {
this.$el.remove();
},
editTask: function() {
var newTaskTitle = prompt('New title:', this.model.get('title'));
this.model.set('title', newTaskTitle, {validate: true});
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
App.Views.AddTask = Backbone.View.extend({
el: 'form#addTask',
initialize: function() {
},
events: {
'submit': 'submit'
},
submit: function(event) {
event.preventDefault();
var newTaskTitle = $(event.currentTarget).find('input[type=text]').val();
var task = new App.Models.Task({ title: newTaskTitle });
this.collection.add(task, {add: true, merge: false, remove: false});
}
});
App.Views.Tasks = Backbone.View.extend({
tagName: 'ul',
initialize: function() {
this.collection.on('add', this.addOne, this);
},
render: function() {
this.collection.each(this.addOne, this);
return this;
},
addOne: function(task) {
var taskView = new App.Views.Task({ model: task });
this.$el.append(taskView.render().el);
}
});
var tasks = new App.Collections.Tasks([
{
title: 'Go to store',
priority: 4
},
{
title: 'Go to mall',
priority: 3
},
{
title: 'Get to work',
priority: 5
}
]);
var addTaskView = new App.Views.AddTask({ collection: tasks });
var tasksView = new App.Views.Tasks({ collection: tasks });
$('div.tasks').append(tasksView.render().el);
})();
So the model validation works fine ... the only pb is that collection.add does not validate the newly added model .... is the a way to force the validation?
Thanks,
Rares
From the fine manual:
validate model.validate(attributes, options)
[...] By default validate is called before save, but can also be
called before set if {validate:true} is passed.
Collection#add does not call save nor does it call set with the validate: true option. If you want to validate during add, say so:
collection.add(models, { validate: true });
That will get validate:true all that way down to Model#set.
A quick look at a simplified example may be helpful:
var M = Backbone.Model.extend({
set: function() {
console.log('setting...');
Backbone.Model.prototype.set.apply(this, arguments);
},
validate: function() {
console.log('validating...');
return 'Never!';
}
});
var C = Backbone.Collection.extend({
model: M
});
var c = new C;
c.on('add', function() {
console.log('Added: ', arguments);
});
c.on('invalid', function() {
console.log('Error: ', arguments);
});
Now if we do this (http://jsfiddle.net/ambiguous/7NqPg/):
c.add(
{ where: 'is', pancakes: 'house?' },
{ validate: true }
);
You'll see that set is called with validate: true, validate will be called, and you'll get an error. But if you say this (http://jsfiddle.net/ambiguous/7b2mn/):
c.add(
{ where: 'is', pancakes: 'house?' },
{add: true, merge: false, remove: false} // Your options
);
You'll see that set is called without validate: true, validate will not be called, and the model will be added to the collection.
The above behavior is quite strongly implied but not explicitly specified so you may not want to trust it. Model#initialize does say:
you can pass in the initial values of the attributes, which will be set on the model.
and set does explicitly mention the validate option. However, there is no guarantee that Collection#add will send options to the model constructor or set or that the model's constructor will send the options to set. So if you want to be really paranoid and future proof, you could add a quick check for this "options get all the way down to set" behavior to your test suite; then, if it changes you'll know about it and you can fix it.
if you pass options to your collection add method, the validation method will not be called and as your arguments in this case are all set to the default value, there is not need to pass them
this.collection.add(task);
you may want to take a look at this question.
Prevent Backbone.js model from validating when first added to collection
I would like to know how to tell backbone to wait until my collection has fetched a model and then render the underscore bit.
In the console returns an error from the underscore template that a field is missing. When I console.log(this.collection.toJSON()) it doesn't show any data. So I assume, that the view is rendered before the data was fetched. How do I tell the view to wait until it is fetched?
/////// View////////
define([
'jquery',
'underscore',
'backbone',
'collections/mitarbeiter',
'text!/aquilamus/templates/mitarbeiter/mitarbeiter.html'
], function($, _, Backbone, MitarbeiterCollection, MitarbeiterTemplate){
var MitarbeiterListView = Backbone.View.extend({
el: $("#container"),
initialize: function(){
this.collection = new MitarbeiterCollection;
this.collection.fetch();
var newtemplate = MitarbeiterTemplate;
this.template = _.template($(newtemplate).html());
},
render: function(){
var renderedContent = this.template(this.collection.toJSON());
$(this.el).html(renderedContent);
return this;
}
});
// Our module now returns our view
return MitarbeiterListView;
});
Using reset like others suggested works, but here is an alternative.
define([
'jquery',
'underscore',
'backbone',
'collections/mitarbeiter',
'text!/aquilamus/templates/mitarbeiter/mitarbeiter.html'
], function($, _, Backbone, MitarbeiterCollection, MitarbeiterTemplate){
var MitarbeiterListView = Backbone.View.extend({
el: $("#container"),
initialize: function(){
this.collection = new MitarbeiterCollection;
var newtemplate = MitarbeiterTemplate;
this.template = _.template($(newtemplate).html());
},
render: function(){
var self = this;
// show some loading message
this.$el.html('Loading');
// fetch, when that is done, replace 'Loading' with content
this.collection.fetch().done(function(){
var renderedContent = self.template(self.collection.toJSON());
self.$el.html(renderedContent);
});
return this;
}
});
// Our module now returns our view
return MitarbeiterListView;
});
Yet another alternative is to do something very similar to this, except use the success callback function which you can put in the fetch options.
This worked the best for me:
var ArtistListView = Backbone.View.extend({
el: '.left',
initialize:function(){
this.render();
},
render: function () {
var source = $('#my-list-template').html();
var template = Handlebars.compile(source);
var collection = new MyCollection;
collection.fetch({async:false});
var html = template(collection.toJSON());
$(this.el).html(html);
});
I do not know any way to say it to wait but I know you can tell backbone to (re)render when your collection is fetched.
initialize: function(){
this.collection = new MitarbeiterCollection;
this.collection.fetch();
var newtemplate = MitarbeiterTemplate;
this.template = _.template($(newtemplate).html());
this.collection.on('reset', this.render, this);
},
The last line listens to your collection's reset event. When it is triggered, it will call the render method.
Collection fetch triggers an event 'reset' when fetch is complete. You can utilise that fetch event
this.collection.on('reset', this.render);
fetch also allow you to pass a success handler function which will be called when fetch succeeds, i.e.:
this.collection.fetch({success: this.render});
I have a Backbone.js project which uses a comparator function defined in the collection. It sorts items when the page is refreshed, but I am trying to get it to sort when a button is clicked instead of on page refresh. Here is my code:
var Thing = Backbone.Model.extend({
defaults: {
title: 'blank',
rank: ''
}
});
var ThingView = Backbone.View.extend({
className: 'thingClass',
template: _.template('<b><button id="remove">X</button> <b><button id="edit">Edit</button> <%= title %> Rank:<%= rank %></b>'),
editTemplate: _.template('<input class="name" value="<%= name %>" /><button id="save">Save</button>'),
events: {
"click #remove": "deleteItem",
"click #edit": "editItem",
"click #save": "saveItem",
},
deleteItem: function () {
console.log('deleted');
this.model.destroy();
this.remove();
},
editItem: function () {
console.log('editing');
this.$el.html(this.editTemplate(this.model.toJSON()));
},
saveItem: function () {
console.log('saved');
editTitle = $('input.name').val();
console.log(editTitle);
this.model.save({
title: editTitle
});
this.$el.html(this.template(this.model.toJSON()));
},
render: function () {
var attributes = this.model.toJSON();
//console.log (attributes);
this.$el.append(this.template(attributes));
return this;
}
});
var ThingsList = Backbone.Collection.extend({
model: Thing,
localStorage: new Store("store-name"),
comparator: function(thing) {
return thing.get('rank');
},
});
var thingsList = new ThingsList;
var ThingsListView = Backbone.View.extend({
el: $('body'),
events: {
'click #add': 'insertItem',
'click #sort': 'sortItems',
},
initialize: function () {
thingsList.fetch();
thingsList.toJSON();
this.render();
this.collection.on("add", this.renderThing, this);
this.collection.on("reset", this.clearRender, this);
},
insertItem: function (e) {
newTitle = $('#new-item').val();
newRank = $('#rank').val();
newThing = new Thing({
title: newTitle,
rank: newRank
});
this.collection.add(newThing);
newThing.save();
console.log(this.collection.length);
},
sortItems: function (e) {
console.log('clicked sort button');
this.collection.sort();
this.$el.detach('.item');
},
render: function () {
_.each(this.collection.models, function (items) {
this.renderThing(items);
}, this);
},
renderThing: function (items) {
var thingView = new ThingView({
model: items
});
this.$el.append(thingView.render().el);
},
clearRender: function () {
console.log('clear render called')
_.each(this.collection.models, function (items) {
//this.remove();
this.$el.remove(".thingClass")
this.renderThing(items);
}, this);
},
test: function (items) {
console.log('test worked');
},
});
var thingsListView = new ThingsListView({
collection: thingsList
});
Are you sure your collection isn't resorting itself? keep in mind that the order of the models in the collection won't change the order of how they appear on the page if they are already rendered.
I'm guessing that what you are trying to do is resort the items that have already been rendered, to do so you would need re-render your collection. If you are going to do so I would recommend that you cache your views and on a sort detach the associated element from the DOM and reattach them in the correct order.
As an example
var ThingsListView = Backbone.View.extend({
_views: {},
initialize: function () {
this.collection.bind('add', this.add, this);
this.collection.bind('reset', this.render, this); //sort triggers a reset
},
add: function (thing) {
var view = new ThingView({model: thing});
this._views[thing.cid] = view; //use client id of model as key for the views cache
this.$el.append(view.render().el);
},
render: function() {
$('li, this.$el).detach(); //detach so that bound events aren't lost
_.each(this.collection.models, function(thing) {
this.$el.append(this.views[thing.cid].el); //get view from cache
},this);
},
sort: function() {
this.collection.sort();
}
}
})
(a couple of differences from my example code and yours I'm assuming here that the collection view has a 'el' referring to a container 'ul', I also don't show how your triggering the sort (basically something like thingListView.sort();)
Edit: It might not be so obvious from the example code I posted, so I should have mentioned to begin with what #Deeptechtons said that when you sort a collection it triggers a reset event
Edit2: If your not interested in caching your views, then the easiest way to remove your current views would probablly be to add a class to the rendered div
var ThingView = Backbone.View.extend({
className: 'thingClass',
//rest of your thingViewCode
Then in your clearRender method just add $(".thingClass", this.$el).remove(); to the beginning of the method.