I am trying to create simple Backbone example but i don't understand what is the problem with my code. Why is there 2 testAttr attributes(on directly on object and one in attributes object) and why isn't change event triggering on any of the changes? Also i don't understand what is the correct way to set attributes on model?
Heres my code:
<div id="note"></div>
<script>
var NoteModel = Backbone.Model.extend({
defaults: function() {
return {
testAttr: "Default testAttr"
}
}
});
var NoteView = Backbone.View.extend({
initialize : function() {
this.listenTo(this.model, "change", this.changed());
},
el: "#note",
changed: function () {
debugger;
console.log("change triggered");
this.render();
},
render : function() {
this.$el.html("<h1>" + this.model.get("testAttr") + "</h1>");
return this;
}
});
var note = new NoteModel();
var noteView = new NoteView({model: note});
noteView.render();
note.set("testAttr", "blah1");
note.testAttr = "blah2";
</script>
Change this line:
this.listenTo(this.model, "change", this.changed());
to this:
this.listenTo(this.model, "change", this.changed);
For the second part of your question, using .set() goes through a set of dirty-checking to see if you changed, deleted or didn't change anything, then triggers the appropriate event. It isn't setting the object.property value, it's setting the object.attributes.property value (tracked by backbone.js). If you directly change the object property, there's nothing to initiate that event for you.
...unless of course you use the AMAZING AND TALENTED Object.observe()!!!1! - ITS ALIVE (in Chrome >36)
Related
OK, I've done some reading on this and I'm pretty sure I know what the problem relates to I Just don't know the best way to fix it. I've got the standard backbone router that sets me up with an item details view, then when I click on a button called "start" it creates a new view which takes me to a sort of a game that people can play with some buttons on the bottom that have "click" events attached. This second view is not called through the router but directly from the first view.
The problem is the second time someones goes back to the homescreen and does it again, this time there are two events attached to each button. The third time there are three events. Obviously the original views are still listening to these buttons. I've read about this and calling the Remove() method but is this what I need to do? If so where do I call remove? Relevant Code below:
1ST VIEW
window.GameDrillView = Backbone.View.extend({
initialize: function () {
this.render();
},
render: function () {
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
events: {
"click .start" : "startGameDrill",
},
startGameDrill: function () {
var start = $('#start').val();.
var stop = $('#stop').val();.
var StartView = new GameDrillStartView({model: this.model, el: $('#content')[0], start: start, stop:stop});
}
});
START VIEW
window.GameDrillStartView = Backbone.View.extend({
// declare variables
initialize: function () {
this.render();
},
events: {
"click .nextstage" : "nextstage", // 2ND TIME THROUGH GETS CALLED TWICE
},
nextstage: function () {
// Do some stuff //
this.render(); //Re-render
},
render: function () {
// Do some variables stuff
this.$el.html(this.template(jQuery.extend(this.model.toJSON(), extended_options)));..
return this;
}
});
When changing view you need to call undelegateEvents() method from the Backbone.View. It disable listening all the elements events mentioned in events { } block. Also if you need to destroy old view you can call remove() method of the view which will call undelegateEvents() internally.
update (example from official site)
var Workspace = Backbone.Router.extend({
routes: {
"help": "help", // #help
"search/:query": "search", // #search/kiwis
"search/:query/p:page": "search" // #search/kiwis/p7
},
help: function() {
if (this.currentView)
this.currentView.undelegateEvents();
this.currentView = new HelpView();
},
search: function(query, page) {
if (this.currentView)
this.currentView.undelegateEvents();
this.currentView = new SearchView();
}
});
An option is to create only one instance of the view:
if(_.isUndefined(this.StartView))
this.StartView = new GameDrillStartView({model: this.model, el: $('#content')[0], start: start, stop:stop});
else
this.StartView.render();
In the render method of GameDrillStartView add the empty method
this.$el.html(this.template(jQuery.extend(this.model.toJSON(), extended_options)))
In this way you won't add more event listeners but you'll update the page everytime the user presses the button.
You can manage the life cycle of StartView in GameDrillView since it seems like a better place to do so.
Got same trouble. Messy solution:
var current_view = false;
var prev_view = false;
var AppRouter = Backbone.Router.extend({
routes: {
"events/:id": "viewEvent",
}
});
var app_router = new AppRouter;
app_router.on('route:viewEvent', function (event_id) {
var _event = new Event({id:event_id});
current_view = new EventView({
model: _event,
});
});
//Will be called after route:viewEvent
app_router.on('route', function () {
if(prev_view) {
prev_view.undelegateEvents();
}
prev_view = current_view;
});
Not sure, how to make it without having current_view and prev_view out of router scope.
I am writing a backbone.js app, and I have a problem.
My collections do not fire events, can anyone spot the problem in the code bellow? I get the render-feedback, the initializer feedback.. but the append method is never called. I know that the "../app" returns a list with tro json items. And I can even see that these are being created in the collection.
Why do my event not get called?
window.TablesInspectorView = Backbone.View.extend({
tagName: "div",
initialize: function () {
console.log('Initializing window.TablesInspectorView');
// setup the tables
this.data = new Backbone.Collection();
this.data.url = "../app";
this.data.fetch();
// some event binds..
this.data.on("change", this.render , this);
this.data.on("add" , this.append_item, this);
},
render: function(){
console.log("render");
_.each(this.data.models, this.append_item);
},
append_item: function(item) {
console.log("appended");
}
});
According to my knowledge , the backbone fetch() is an asynchronous event and when it completes the reset event is triggered ,
When the models belonging to the collection (this.data) are modified , the change event is triggered, so im guessing you have not got that part correct.
so i would do something like this :
window.TablesInspectorView = Backbone.View.extend({
tagName: "div",
initialize: function () {
console.log('Initializing window.TablesInspectorView');
// setup the tables
this.data = new Backbone.Collection();
this.data.url = "../app";
this.data.fetch();
// some event binds..
this.data.on("reset", this.render , this); //change here
this.data.on("add" , this.append_item, this); // i dont see a this.data.add() in you code so assuming this was never called ?
},
render: function(){
console.log("render");
_.each(this.data.models, this.append_item);
},
append_item: function(item) {
console.log("appended");
}
});
I'm working on my first app using bbjs, after 10 tutorials and endless sources I am trying to come up with my code design.
I ask what is the best practice with views and templates. Also there is an events problem I am struggling with.
As I understand, the view is to be responsible for one element and its contents (and other sub-views).
For the code to be manageable, testable, etc.. the element/template is to be passed to the view on creation.
In my app Imho the view should hold the templates, because the visible element has many "states" and a different template for each state.
When the state changes, I guess its best to create a new view, but, is it possible for the view to update itself with new element?
App.Box = Backbone.Model.extend({
defaults: function() {
return {
media: "http://placehold.it/200x100",
text: "empty...",
type: "type1"
};
}
});
App.BoxView = Backbone.View.extend({
template: {},
templates: {
"type1": template('appboxtype1'),
"type2": template('appboxtype2')
},
events: {
'click .button': 'delete'
},
initialize: function(options) {
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'destroy', this.remove);
this.render();
},
render: function() {
this.template = this.templates[ this.model.get("type") ];
// first method
this.$el.replaceWith( $($.parseHTML(this.template(this))) );
this.$el.attr("id", this.model.cid);
// second method
var $t_el = this.$el;
this.setElement( $($.parseHTML(this.template(this))) );
this.$el.attr("id", this.model.cid);
$t_el.replaceWith( this.$el );
this.delegateEvents();
//$('#'+this.model.cid).replaceWith( $(g.test.trim()) );
//! on the second render the events are no longer bind, deligateEvents doesn't help
return this;
},
// get values
text: function() { return this.model.get('text'); },
media: function() { return this.model.get('media'); },
delete: function() {
this.model.destroy();
}
});
Thanx! :)
Instead of trying to replace the view's root element ($el), just replace its content.
this.$el.html(this.template(this));
Events should still work then.
try this
render: function() {
html = '<div>your new html</div>';
var el = $(html);
this.$el.replaceWith(el);
this.setElement(el);
return this;
}
$.replaceWith will only replace the element in the DOM. But the this.$el still holds a reference to the now displaced old element. You need to call this.setElement(..) to update the this.$el field. Calling setElement will also undelegateEvents and delegateEvents events for you.
I came up with this solution: http://jsfiddle.net/Antonimo/vrQzF/4/
if anyone has a better idea its always welcome!
basically, in view:
var t_$el = this.$el;
this.$el = $($.parseHTML(this.template(this)));
this.$el.attr("id", this.cid);
if (t_$el.parent().length !== 0) { // if in dom
this.undelegateEvents();
t_$el.each(function(index, el){ // clean up
if( index !== 0 ){ $(el).remove(); }
});
t_$el.first().replaceWith(this.$el);
this.delegateEvents();
}
I have a view which doesn't seem to want to render as the model's change event is not firing.
here's my model:
var LanguagePanelModel = Backbone.Model.extend({
name: "langpanel",
url: "/setlocale",
initialize: function(){
console.log("langselect initing")
}
})
here's my view:
var LanguagePanelView = Backbone.View.extend({
tagName: "div",
className: "langselect",
render: function(){
this.el.innerHTML = this.model.get("content");
console.log("render",this.model.get(0))
return this;
},
initialize : function(options) {
console.log("initializing",this.model)
_.bindAll(this, "render");
this.model.bind('change', this.render);
this.model.fetch(this.model.url);
}
});
here's how I instantiate them:
if(some stuff here)
{
lsm = new LanguagePanelModel();
lsv = new LanguagePanelView({model:lsm});
}
I get logs for the init but not for the render of the view?
Any ideas?
I guess it's about setting the attributes of the model - name is not a standard attribute and the way you've defined it, it seems to be accessible directly by using model.name and backbone doesn't allow that AFAIK. Here are the changes that work :) You can see the associated fiddle with it too :)
$(document).ready(function(){
var LanguagePanelModel = Backbone.Model.extend({
//adding custom attributes to defaults (with default values)
defaults: {
name: "langpanel",
content: "Some test content" //just 'cause there wasn't anything to fetch from the server
},
url: "/setlocale",
initialize: function(){
console.log("langselect initing"); //does get logged
}
});
var LanguagePanelView = Backbone.View.extend({
el: $('#somediv'), //added here directly so that content can be seen in the actual div
initialize : function(options) {
console.log("initializing",this.model);
_.bindAll(this, "render");
this.render(); //calling directly since 'change' won't be triggered
this.model.bind('change', this.render);
//this.model.fetch(this.model.url);
},
render: function(){
var c = this.model.get("content");
alert(c);
$(this.el).html(c); //for UI visibility
console.log("render",this.model.get(0)); //does get logged :)
return this;
}
});
var lpm = new LanguagePanelModel();
var lpv = new LanguagePanelView({model:lpm});
}); //end ready
UPDATE:
You don't need to manually trigger the change event - think of it as bad practice. Here's what the backbone documentation says (note: fetch also triggers change!)
Fetch
model.fetch([options])
Resets the model's state from the server.
Useful if the model has never been populated with data, or if you'd
like to ensure that you have the latest server state. A "change" event
will be triggered if the server's state differs from the current
attributes. Accepts success and error callbacks in the options hash,
which are passed (model, response) as arguments.
So, if the value fetched from the server is different from the defaults the change event will be fired so you needn't do it yourself. If you really wish to have such an event then you can use the trigger approach but custom name it since it's specific to your application. You are basically trying to overload the event so to speak. Totally fine, but just a thought.
Change
model.change()
Manually trigger the "change" event. If you've been
passing {silent: true} to the set function in order to aggregate rapid
changes to a model, you'll want to call model.change() when you're all
finished.
The change event is to be manually triggered only if you've been suppressing the event by passing silent:true as an argument to the set method of the model.
You may also want to look at 'has changed' and other events from the backbone doc.
EDIT Forgot to add the updated fiddle for the above example - you can see that the alert box pops up twice when the model is changed by explicitly calling set - the same would happen on fetching too. And hence the comment on the fact that you "may not" need to trigger 'change' manually as you are doing :)
The issue was resolved my adding
var LanguagePanelModel = Backbone.Model.extend({
//adding custom attributes to defaults (with default values)
defaults: {
name: "langpanel",
content: "no content",
rawdata: "no data"
},
events:{
//"refresh" : "parse"
},
url: "/setlocale",
initialize: function(){
log("langselect initing");
//this.fetch()
},
parse: function(response) {
this.rawdata = response;
// ... do some stuff
this.trigger('change',this) //<-- this is what was necessary
}
})
You don't need attributes to be predefined unlike PhD suggested. You need to pass the context to 'bind' - this.model.bind('change', this.render, this);
See working fiddle at http://jsfiddle.net/7LzTt/ or code below:
$(document).ready(function(){
var LanguagePanelModel = Backbone.Model.extend({
url: "/setlocale",
initialize: function(){
console.log("langselect initing");
}
});
var LanguagePanelView = Backbone.View.extend({
el: $('#somediv'),
initialize : function(options) {
console.log("initializing",this.model);
// _.bindAll(this, "render");
//this.render();
this.model.bind('change', this.render, this);
//this.model.fetch(this.model.url);
},
render: function(){
var c = this.model.get("content");
alert(c);
$(this.el).html(c);
console.log("render",this.model.get(0));
return this;
}
});
var lpm = new LanguagePanelModel();
var lpv = new LanguagePanelView({model:lpm});
lpm.set({content:"hello"});
}); //end ready
In my web application, I have a user list in a table on the left, and a user detail pane on the right. When the admin clicks a user in the table, its details should be displayed on the right.
I have a UserListView and UserRowView on the left, and a UserDetailView on the right. Things kind of work, but I have a weird behavior. If I click some users on the left, then click delete on one of them, I get successive javascript confirm boxes for all users that have been displayed.
It looks like event bindings of all previously displayed views have not been removed, which seems to be normal. I should not do a new UserDetailView every time on UserRowView? Should I maintain a view and change its reference model? Should I keep track of the current view and remove it before creating a new one? I'm kind of lost and any idea will be welcome. Thank you !
Here is the code of the left view (row display, click event, right view creation)
window.UserRowView = Backbone.View.extend({
tagName : "tr",
events : {
"click" : "click",
},
render : function() {
$(this.el).html(ich.bbViewUserTr(this.model.toJSON()));
return this;
},
click : function() {
var view = new UserDetailView({model:this.model})
view.render()
}
})
And the code for right view (delete button)
window.UserDetailView = Backbone.View.extend({
el : $("#bbBoxUserDetail"),
events : {
"click .delete" : "deleteUser"
},
initialize : function() {
this.model.bind('destroy', function(){this.el.hide()}, this);
},
render : function() {
this.el.html(ich.bbViewUserDetail(this.model.toJSON()));
this.el.show();
},
deleteUser : function() {
if (confirm("Really delete user " + this.model.get("login") + "?"))
this.model.destroy();
return false;
}
})
I always destroy and create views because as my single page app gets bigger and bigger, keeping unused live views in memory just so that I can re-use them would become difficult to maintain.
Here's a simplified version of a technique that I use to clean-up my Views to avoid memory leaks.
I first create a BaseView that all of my views inherit from. The basic idea is that my View will keep a reference to all of the events to which it's subscribed to, so that when it's time to dispose the View, all of those bindings will automatically be unbound. Here's an example implementation of my BaseView:
var BaseView = function (options) {
this.bindings = [];
Backbone.View.apply(this, [options]);
};
_.extend(BaseView.prototype, Backbone.View.prototype, {
bindTo: function (model, ev, callback) {
model.bind(ev, callback, this);
this.bindings.push({ model: model, ev: ev, callback: callback });
},
unbindFromAll: function () {
_.each(this.bindings, function (binding) {
binding.model.unbind(binding.ev, binding.callback);
});
this.bindings = [];
},
dispose: function () {
this.unbindFromAll(); // Will unbind all events this view has bound to
this.unbind(); // This will unbind all listeners to events from
// this view. This is probably not necessary
// because this view will be garbage collected.
this.remove(); // Uses the default Backbone.View.remove() method which
// removes this.el from the DOM and removes DOM events.
}
});
BaseView.extend = Backbone.View.extend;
Whenever a View needs to bind to an event on a model or collection, I would use the bindTo method. For example:
var SampleView = BaseView.extend({
initialize: function(){
this.bindTo(this.model, 'change', this.render);
this.bindTo(this.collection, 'reset', this.doSomething);
}
});
Whenever I remove a view, I just call the dispose method which will clean everything up automatically:
var sampleView = new SampleView({model: some_model, collection: some_collection});
sampleView.dispose();
I shared this technique with the folks who are writing the "Backbone.js on Rails" ebook and I believe this is the technique that they've adopted for the book.
Update: 2014-03-24
As of Backone 0.9.9, listenTo and stopListening were added to Events using the same bindTo and unbindFromAll techniques shown above. Also, View.remove calls stopListening automatically, so binding and unbinding is as easy as this now:
var SampleView = BaseView.extend({
initialize: function(){
this.listenTo(this.model, 'change', this.render);
}
});
var sampleView = new SampleView({model: some_model});
sampleView.remove();
I blogged about this recently, and showed several things that I do in my apps to handle these scenarios:
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
This is a common condition. If you create a new view every time, all old views will still be bound to all of the events. One thing you can do is create a function on your view called detatch:
detatch: function() {
$(this.el).unbind();
this.model.unbind();
Then, before you create the new view, make sure to call detatch on the old view.
Of course, as you mentioned, you can always create one "detail" view and never change it. You can bind to the "change" event on the model (from the view) to re-render yourself. Add this to your initializer:
this.model.bind('change', this.render)
Doing that will cause the details pane to re-render EVERY time a change is made to the model. You can get finer granularity by watching for a single property: "change:propName".
Of course, doing this requires a common model that the item View has reference to as well as the higher level list view and the details view.
Hope this helps!
To fix events binding multiple times,
$("#my_app_container").unbind()
//Instantiate your views here
Using the above line before instantiating the new Views from route, solved the issue I had with zombie views.
I think most people start with Backbone will create the view as in your code:
var view = new UserDetailView({model:this.model});
This code creates zombie view, because we might constantly create new view without cleanup existing view. However it's not convenient to call view.dispose() for all Backbone Views in your app (especially if we create views in for loop)
I think the best timing to put cleanup code is before creating new view. My solution is to create a helper to do this cleanup:
window.VM = window.VM || {};
VM.views = VM.views || {};
VM.createView = function(name, callback) {
if (typeof VM.views[name] !== 'undefined') {
// Cleanup view
// Remove all of the view's delegated events
VM.views[name].undelegateEvents();
// Remove view from the DOM
VM.views[name].remove();
// Removes all callbacks on view
VM.views[name].off();
if (typeof VM.views[name].close === 'function') {
VM.views[name].close();
}
}
VM.views[name] = callback();
return VM.views[name];
}
VM.reuseView = function(name, callback) {
if (typeof VM.views[name] !== 'undefined') {
return VM.views[name];
}
VM.views[name] = callback();
return VM.views[name];
}
Using VM to create your view will help cleanup any existing view without having to call view.dispose(). You can do a small modification to your code from
var view = new UserDetailView({model:this.model});
to
var view = VM.createView("unique_view_name", function() {
return new UserDetailView({model:this.model});
});
So it is up to you if you want to reuse view instead of constantly creating it, as long as the view is clean, you don't need to worry. Just change createView to reuseView:
var view = VM.reuseView("unique_view_name", function() {
return new UserDetailView({model:this.model});
});
Detailed code and attribution is posted at https://github.com/thomasdao/Backbone-View-Manager
One alternative is to bind, as opposed to creating a series of new views and then unbinding those views. You'd accomplish this doing something like:
window.User = Backbone.Model.extend({
});
window.MyViewModel = Backbone.Model.extend({
});
window.myView = Backbone.View.extend({
initialize: function(){
this.model.on('change', this.alert, this);
},
alert: function(){
alert("changed");
}
});
You'd set the model of myView to myViewModel, which would be set to a User model. This way, if you set myViewModel to another user (i.e., changing its attributes) then it could trigger a render function in the view with the new attributes.
One problem is that this breaks the link to the original model. You could get around this by either using a collection object, or by setting the user model as an attribute of the viewmodel. Then, this would be accessible in the view as myview.model.get("model").
Use this method for clearing the child views and current views from memory.
//FIRST EXTEND THE BACKBONE VIEW....
//Extending the backbone view...
Backbone.View.prototype.destroy_view = function()
{
//for doing something before closing.....
if (this.beforeClose) {
this.beforeClose();
}
//For destroying the related child views...
if (this.destroyChild)
{
this.destroyChild();
}
this.undelegateEvents();
$(this.el).removeData().unbind();
//Remove view from DOM
this.remove();
Backbone.View.prototype.remove.call(this);
}
//Function for destroying the child views...
Backbone.View.prototype.destroyChild = function(){
console.info("Closing the child views...");
//Remember to push the child views of a parent view using this.childViews
if(this.childViews){
var len = this.childViews.length;
for(var i=0; i<len; i++){
this.childViews[i].destroy_view();
}
}//End of if statement
} //End of destroyChild function
//Now extending the Router ..
var Test_Routers = Backbone.Router.extend({
//Always call this function before calling a route call function...
closePreviousViews: function() {
console.log("Closing the pervious in memory views...");
if (this.currentView)
this.currentView.destroy_view();
},
routes:{
"test" : "testRoute"
},
testRoute: function(){
//Always call this method before calling the route..
this.closePreviousViews();
.....
}
//Now calling the views...
$(document).ready(function(e) {
var Router = new Test_Routers();
Backbone.history.start({root: "/"});
});
//Now showing how to push child views in parent views and setting of current views...
var Test_View = Backbone.View.extend({
initialize:function(){
//Now setting the current view..
Router.currentView = this;
//If your views contains child views then first initialize...
this.childViews = [];
//Now push any child views you create in this parent view.
//It will automatically get deleted
//this.childViews.push(childView);
}
});