Do we need different events for each layout? - backbone.js

My rendered method is being called before _initializeLayout:
var _initializeLayout = function() {
console.log('initializeLayout...');
Controller.layout = new Layout();
Controller.layout.on("show", function() {
vent.trigger("layout:rendered");
});
vent.trigger('app:show', Controller.layout);
};
I use the layout in the on rendered method:
// controller attach a sub view/ search View
vent.on("layout:rendered", function() {
console.log('layout:rendered =>StartController');
// render views for the existing HTML in the template, and attach it to the layout (i.e. don't double render)
var inspectorStartView = new InspectorStartView();
Controller.layout.inspector.attachView(inspectorStartView);
var playerStartView = new PlayerStartView();
Controller.layout.player.attachView(playerStartView);
});
When I try it, my on rendered callback is called before _initializeLayout(). I have it calling _initializeLayout in the router/controller method:
Controller.go_inspector_control_center = function(term) {
_initializeLayout();
//vent.trigger("search:term", term);
};
I just ran it again and found that an event was being triggered from a different controller's _initializeLayout() method:
// private
var _initializeLayout = function() {
console.log('initialize Start Layout...');
Controller.layout = new Layout();
Controller.layout.on("show", function() {
**vent.trigger("layout:rendered"); // <--**
});
vent.trigger('app:show', Controller.layout);
};
It appears that the events need to have unique names. I'll try that. If anyone knows please chime in.
Andrew

You just can't take us Java guys anywhere. The answer (in Javascript) is that I do need to name my events uniquely. Makes sense when you think about it.
Although most event systems I've worked with X11/Java you don't name your event you use what they give you.
Here is a stackoverflow question on event naming.
I hope my public learning is helping others. ;-)

Related

Bug while creating object in View

I'm working on a backbone.js project which is mainly to learn backbone framework itself.
However I'm stuck at this problem which i can't figure out but might have an idea about the problem...
I've got an Create View looking like this...
define(['backbone', 'underscore', 'jade!templates/addAccount', 'models/accountmodel', 'common/serializeObject'],
function(Backbone, underscore, template, AccountModel, SerializeObject){
return Backbone.View.extend({
//Templates
template: template,
//Constructor
initialize: function(){
this.accCollection = this.options.accCollection;
},
//Events
events: {
'submit .add-account-form': 'saveAccount'
},
//Event functions
saveAccount: function(ev){
ev.preventDefault();
//Using common/serializeObject function to get a JSON data object from form
var myObj = $(ev.currentTarget).serializeObject();
console.log("Saving!");
this.accCollection.create(new AccountModel(myObj), {
success: function(){
myObj = null;
this.close();
Backbone.history.navigate('accounts', {trigger:true});
},
error: function(){
//show 500?
}
});
},
//Display functions
render: function(){
$('.currentPage').html("<h3>Accounts <span class='glyphicon glyphicon-chevron-right'> </span> New Account</h3>");
//Render it in jade template
this.$el.html(this.template());
return this;
}
});
});
The problem is that for every single time I visit the create page and go to another and visit it again. It remebers it, it seems. And when i finally create a new account I get that many times I've visited total number of accounts...
So console.log("Saving!"); in saveAccount function is called x times visited page...
Do I have to close/delete current view when leaving it or what is this?
EDIT
Here's a part of the route where i init my view..
"account/new" : function(){
var accCollection = new AccountCollection();
this.nav(new CreateAccountView({el:'.content', accCollection:accCollection}));
console.log("new account");
},
/Regards
You have zombie views. Every time you do this:
new CreateAccountView({el:'.content', accCollection:accCollection})
you're attaching an event listener to .content but nothing seems to be detaching it. The usual approach is to call remove on a view to remove it from the DOM and tell it to clean up after itself. The default remove does things you don't want it to:
remove: function() {
this.$el.remove();
this.stopListening();
return this;
}
You don't want that this.$el.remove() call since your view is not responsible for creating its own el, you probably want:
remove: function() {
this.$el.empty(); // Remove the content we added.
this.undelegateEvents(); // Unbind your event handler.
this.stopListening();
return this;
}
Then your router can keep track of the currently open view and remove it before throwing up another one with things like this:
if(this.currentView)
this.currentView.remove();
this.currentView = new CreateAccountView({ ... });
this.nav(this.currentView);
While I'm here, your code will break as soon as you upgrade your Backbone. As of version 1.1:
Backbone Views no longer automatically attach options passed to the constructor as this.options, but you can do it yourself if you prefer.
So your initialize:
initialize: function(){
this.accCollection = this.options.accCollection;
},
won't work in 1.1+. However, some options are automatically copied:
constructor / initialize new View([options])
There are several special options that, if passed, will be attached directly to the view: model, collection, el, id, className, tagName, attributes and events.
so you could toss out your initialize, refer to this.collection instead of this.accCollection inside the view, and instantiate the view using:
new CreateAccountView({el: '.content', collection: accCollection})

failing approach to dispose off the zombie views in backbonejs

so i had the same famous problem of zombie views in my backbone app. I tried this to become a superhero :P
var Router=Backbone.Router.extend({
routes:{
"":"loadDashboard",
"home":"loadDashboard",
'post-leads':"loadPostLeads"
},
initialize:function(){
window.currentView=null;
},
loadPostLeads:function(){
require(['views/post-leads'],function(leads){
if(window.currentView!=null)
{window.currentView.remove();}
window.currentView=new leads();
window.currentView.render();
})
},
loadDashboard: function(){
require(['views/dashboard'],function(dashboard){
if(window.currentView!=null)
{window.currentView.remove();}
window.currentView=new dashboard();
window.currentView.render();
})
}
});
This doesn't work. I wanted something simple and don't want to use marionette or anything similar for that sake. Whats going wrong above? Is it a sensible approach?
In principle what you do should work, but there are some things that Backbone can't clean up, because it doesn't know of them.
First, you should make sure that you are using a recent version of Backbone (0.9.9 or newer). There have been some improvements to the event binding code, which makes it easier for the View.remove method to do all the necessary cleanup.
The common gotchas are:
Listening to model events:
//don't use other.on (Backbone doesn't know how to clean up)
this.model.on('event', this.method);
//use this.listenTo (Backbone cleans up events when View.remove is called)
//requires Backbone 0.9.9
this.listenTo(this.model, 'event', this.method);
Listening to DOM events outside your view's scope:
//if you listen to events for nodes that are outside View.el
$(document).on('event', this.method);
//you have to clean them up. A good way is to override the View.remove method
remove: function() {
$(document).off('event', this.method);
Backbone.View.prototype.remove.call(this);
}
Direct references:
//you may hold a direct reference to the view:
this.childView = otherView;
//or one of its methods
this.callback = otherView.render;
//or as a captured function scope variable:
this.on('event', function() {
otherView.render();
});
Closures:
//if you create a closure over your view, or any method of your view,
//someone else may still hold a reference to your view:
method: function(arg) {
var self = this;
return function() {
self.something(x);
}
}
Avoiding the following pitfalls should help your views to get cleaned up correctly.
Edit based on comment:
Ah, you didn't mention the full problem in your question. The problem with your approach is, as I gather, is that you're trying to render the two views into the same element:
var View1 = Backbone.View.extend({el:"#container" });
var View2 = Backbone.View.extend({el:"#container" });
And when you remove View1, the View2 does not correctly render.
Instead of specifying the view el, you should render the views into an element. On your page you should have a #container element, and append the view's element into the container.
loadPostLeads: function () {
var self = this;
require(['views/post-leads'], function (leads) {
self.renderView(new leads());
})
},
loadDashboard: function () {
var self = this;
require(['views/dashboard'], function (dashboard) {
self.renderView(new dashboard());
})
},
renderView: function(view) {
if(window.currentView) {
window.currentView.remove();
}
//the view itself does not specify el, so you need to append the view into the DOM
view.render();
$("#container").html(view.el);
window.currentView = view;
}

using twiiter tooltip with backbone.js

full sample here
I have a very simple backbone js structure.
var Step1View = Backbone.View.extend({
el:'.page',
render:function () {
var template = _.template($('#step1-template').html());
this.$el.html(template);
}
});
var step1View = new Step1View();
var Router = Backbone.Router.extend({
routes:{
"":"home"
}
});
var router = new Router;
router.on('route:home', function () {
step1View.render();
})
Backbone.history.start();
This works well however i am unable to get this simple jquery function called.
$(document).ready(function() {
$('.tip').tooltip();
});
Update
School boy error here. Jquery onload functions need to be placed in the route. I'm very new to backbone so i'm not sure if this is best practice. But the following works.
render:function () {
var that = this;
var savings = new Savings();
savings.fetch({
success:function () {
var template = _.template($('#step3-template').html(), {savings:savings.models});
that.$el.html(template);
// put your jquery good ness here
$('.tip').tooltip();
$(".step3-form").validate();
}
})
}
Looks like you found your answer! Just wanted to also share that you could scope down your jQuery a bit by doing this instead.
savings.fetch({
success:function () {
var template = _.template($('#step3-template').html(), {savings:savings.models});
that.$el.html(template);
that.$el.find('.tip').tooltip();
that.$el.find(".step3-form").validate();
}
What you have in your example works but it's also scanning the whole document every time for HTML with the class tip where you could use the element you just created to scan downward only for the tip you just created inside it. Slight optimization.
Hope this is helpful!
Looks like you found your answer! Just wanted to also share that you could scope down your jQuery a bit by doing this instead.
savings.fetch({
success:function () {
var template = _.template($('#step3-template').html(), {savings:savings.models});
that.$el.html(template);
that.$el.find('.tip').tooltip();
that.$el.find(".step3-form").validate();
}
What you have in your example works but it's also scanning the whole document every time for HTML with the class tip where you could use the element you just created to scan downward only for the tip you just created inside it. Slight optimization.
Hope this is helpful!

Backbone.js:"Maximum call stack size exceeded" error

suppose I have a model and a view ,ths view have two method:one is bind the document mousemove event and the other is unbind method,defalut I give the document mousemove event, once the model's enable value changed I will call the view's unbind method:
window.ConfigModel = Backbone.Model.extend({
defaults: {
'enable':0
},
initialize: function(){
this.bind("change:enable", function () {
var portView2 = new PortView();
portView2.viewOff();
});
},
change:function () {
this.set('enable', 9);
}
})
window.PortView = Backbone.View.extend({
viewOn: function () {
$(document).on('mousemove', function () {
console.log('move')
})
},
viewOff: function () {
$(document).off('mousemove');
}
})
then I put an input on the document to call the model changed:
$('input').click(function () {
var configModel = new ConfigModel();
configModel.change();
})
the boot script is :
var portView1 = new PortView();
portView1.viewOn();
The problem is once I call the click the input button ,the chrome would tell me an error:Maximum call stack size exceeded it seems the change be invoke many times.So what's the problem with my problem ,how can I solve this problem
Backbone models already have a change method:
change model.change()
Manually trigger the "change" event and a "change:attribute" event for each attribute that has changed. 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.
Presumably something inside Backbone is trying to call configModel.change() and getting your version of change which triggers another change() call inside Backbone which runs your change which ... until the stack blows up.
You should use a different name for your change method.
That said, your code structure is somewhat bizarre. A model listening to events on itself is well and good but a model creating a view is odd:
initialize: function() {
this.bind("change:enable", function () {
var portView2 = new PortView();
portView2.viewOff();
});
}
And instantiating a view simply to call a single method and then throw it away is strange as is creating a new model just to trigger an event.
I think you probably want to have a single ConfigModel instance as part of your application state, say app.config. Then your click handler would talk to that model:
$('input').click(function () {
app.config.enable_level_9(); // or whatever your 'change' gets renamed to
});
Then you'd have some other part of your application (not necessarily a view) that listens for changes to app.config and acts appropriately:
app.viewOn = function() {
$(document).on('mousemove', function() {
console.log('move')
});
};
app.viewOff = function() {
$(document).off('mousemove');
};
app.init = function() {
app.config = new ConfigModel();
app.viewOn();
$('input').click(function () {
app.config.enable_level_9();
});
// ...
};
And then start the application with a single app.init() call:
$(function() {
app.init();
});

Failing to pass models correctly from collection view?

I've been staring at this for a while and trying various tweaks, to no avail.
Why am I getting a "this.model is undefined" error at
$(function(){
window.Sentence = Backbone.Model.extend({
initialize: function() {
console.log(this.toJSON())
}
});
window.Text = Backbone.Collection.extend({
model : Sentence,
initialize: function(models, options){
this.url = options.url;
}
});
window.SentenceView = Backbone.View.extend({
initialize : function(){
_.bindAll(this, 'render');
this.template = _.template($('#sentence_template').html());
},
render : function(){
var rendered = this.template(this.model.toJSON());
$(this.el).html(rendered);
return this;
}
})
window.TextView = Backbone.View.extend({
el : $('#notebook') ,
initialize : function(){
_.bindAll(this, 'render');
},
render : function(){
this.collection.each(function(sentence){
if (sentence === undefined){
console.log('sentence was undefined');
};
var view = new SentenceView({model: sentence});
this.$('ol#sentences').append(view.render().el);
});
return this;
}
});
function Notebook(params){
this.text = new Text(
// models
{},
// params
{
url: params.url
}
);
this.start = function(){
this.text.fetch();
this.textView = new TextView({
collection: this.text
});
$('body').append(this.textView.render().el);
};
}
window.notebook = new Notebook(
{ 'url': 'js/mandarin.js' }
);
window.notebook.start();
})
There's an online version wher eyou can see the error in a console at:
http://lotsofwords.org/languages/chinese/notebook/
The whole repo is at:
https://github.com/amundo/notebook/
The offending line appears to be at:
https://github.com/amundo/notebook/blob/master/js/notebook.js#L31
I find this perplexing because as far as I can tell the iteration in TextView.render has the right _.each syntax, I just can't figure out why the Sentence models aren't showing up as they should.
var view = new SentenceView({model: sentence});
I'm pretty sure when you pass data to a backbone view constructor, the data is added to the Backbone.View.options property.
Change this line
var rendered = this.template(this.model.toJSON());
to this
var rendered = this.template(this.options.model.toJSON());
and see if it works
UPDATE:
From the doco:
When creating a new View, the options you pass are attached to the view as this.options, for future reference. There are several special options that, if passed, will be attached directly to the view: model, collection, el, id, className, and tagName
So, disregard the above advice - the model should by default be attached directly to the object
Things to check next when debugging:
confirm from within the render() method that this is actually the SentenceView object
confirm that you are not passing in an undefined sentence here:
var view = new SentenceView({model: sentence});
UPDATE2:
It looks like the collection is borked then:
this.textView = new TextView({
collection: this.text
});
To debug it further you'll need to examine it and work out what's going on. When I looked in firebug, the collection property didn't look right to me.
You could have a timing issue too. I thought the fetch was asynchronous, so you probably don't want to assign the collection to the TextView until you are sure it has completed.
Backbone surfaces underscore.js collection methods for you so you can do this. See if this works for you:
this.collection.each(function(sentence) {
// YOUR CODE HERE
});
I think the problem is on line 48 of notebook.js shown below:
render : function(){
_(this.collection).each(function(sentence){
var view = new SentenceView({model: sentence});
this.$('ol#sentences').append(view.render().el);
});
The problem is you are wrapping the collection and you don't have to. Change it to
this.collection.each(function(sentence){ ...
hope that fixes it
EDIT:
OK i'm going to take another crack at it now that you mentioned timing in one of your comments
take a look at where you are fetching and change it to this:
this.start = function(){
this.text.fetch({
success: _.bind( function() {
this.textView = new TextView({
collection: this.text
});
$('body').append(this.textView.render().el);
}, this)
);
};
I typed this manually so there may be mismatching parentheses. The key is that fetch is async.
Hope this fixes it
try using _.each
_.each(this.collection, function(sentence){
if (sentence === undefined){
console.log('sentence was undefined');
};
var view = new SentenceView({model: sentence});
this.$('ol#sentences').append(view.render().el);
},this);

Resources