Backbone.js: undelegateEvents not removing events - backbone.js

In the following code:
HTML
<div id="myView">
<button id="test_button">
Test Button
</button>
<ul id="output"></ul>
</div>
JavaScript
var myView = Backbone.View.extend({
initialize: function() {
// why doesn't this remove the previously delegated events?
this.undelegateEvents();
this.delegateEvents({
'click #test_button': 'buttonClicked'
});
},
// this event fires twice for one button click
buttonClicked: function() {
$("#output").append('<li>Button was clicked</li>');
}
});
$(document).ready(function(){
new myView({el: "#myView"});
// instantiate view again
new myView({el: "#myView"});
});
why does
this.undelegateEvents();
in the initialize() method of the Backbone View not remove the previously delegated events from the previous instantiation of the View?
JSFiddle example of above code: https://jsfiddle.net/billb123/o43zruea/28/

I'll try not to shout but please stop trying to bind views to existing elements. Let the view create and own its own el, then call view.remove() to kill it off before replacing it. This simple change solves so many problems with view events that you should always think twice (and twice more) if you don't do it this way.
In your case, you'd have HTML like this:
<script id="t" type="text/x-underscore">
<div id="myView">
<button id="test_button">
Test Button
</button>
</div>
</script>
<div id="container">
</div>
<ul id="output"> <!-- This is outside the container because we're going to empty and refill it -->
</ul>
And your JavaScript would look like this:
var myView = Backbone.View.extend({
events: {
'click #test_button': 'buttonClicked'
},
render: function() {
this.$el.html($('#t').html());
return this;
},
buttonClicked: function() {
$("#output").append('<li>Button was clicked</li>');
}
});
$(document).ready(function(){
var v = new myView();
$('#container').append(v.render().el);
v.remove(); // <----------------- Clean things up before adding a new one
v = new myView();
$('#container').append(v.render().el);
});
Points of interest:
Create the view then render it then put it on the page.
Call remove on the view when you're done with it.
The view goes inside the container. The caller owns the container, the view owns its el.
There are no delegateEvents or undelegateEvents calls anywhere. The presence of those almost always point to structural problems in your application IMO.
Each view is self contained: the outside world doesn't play with anything inside the view and the view keeps its hands to itself.
Updated fiddle: https://jsfiddle.net/bp8fqdgm/
But why didn't your attempted undelegateEvents do anything? undelegateEvents looks like this:
undelegateEvents: function() {
if (this.$el) this.$el.off('.delegateEvents' + this.cid);
return this;
},
The cid is unique per view instance so each view instance uses its own unique namespace for events that delegateEvents binds. That means that this:
this.undelegateEvents();
this.delegateEvents();
is saying:
Remove the events that this instance of the view has bound. These events will be found in the the '.delegateEvents' + this.cid namespace where cid is unique for each view instance.
Bind the events that this instance of the view defines (or the events in the delegateEvents call). These events will be attached using the '.delegateEvents' + this.cid namespace.
So your undelegateEvents call is removing events but not all of them, only the specific event bindings that that view instance adds are removed.
Your this.undelegateEvents() call doesn't actually accomplish anything because it is in the wrong place and called at the wrong time. If the new View caller did the undelegateEvents call:
var v = new myView({el: "#myView"});
v.undelegateEvents();
new myView({el: "#myView"});
then it would happen in the right place and at the right time. Of course this means that your router needs to keep track of the current view so that it can currentView.undelegateEvents() at the right time; but if you're doing that then you'd be better off (IMO) taking the approach I outlined at the top of the answer.

Related

Backbone view - created on mouse enter not firing events

I am using a jQuery tool tip plug-in that requires the use of the jQuery clone function to populate an auto generated tooltip. For the project I am working on, I don't have controle over when the tooltip is initiated nor is there an available callback so I am using jQuery .on('mouseenter') to initialize my event.
Anything I put within the initialize function works, but my click event wont fire. From what I have read, if el is defined then standard events (click) should automatically be bound but that is not happening and as far as I can tell this should be working correctly.
javascript:
Lot = Backbone.View.extend({
initialize: function(){
this.wrapper = $(this.$el).find('.childDiv');
$(this.$el).css('background-color', 'blue');
console.log('init');
},
events: {
'click .showChild': 'showfunction'
},
showfunction:function(e){
this.wrapper.slideToggle('fast');
}
});
//gives the auto generated tooltip a class, otherwise it would be classless
$.balloon.defaults.classname = "balloon";
//tooltip needs content passed in, the tooltip creator recommends using clone
$('#showParent')
.balloon({contents: $('.tooltip-content').clone(), position: "bottom right" });
// this may look redundant, but I do not have access to the initialize function
$('#showParent').on('mouseenter', function() {
console.log('mouse enter');
lots = new Lot({el: $('.balloon .tooltip-content')});
});
HTML:
<button id="showParent">Hover</button>
<div id="wrapper">
<div class="parentDiv tooltip-content">
<h1> Some text to test parent</h1>
<button class="showChild">Click</button>
<div class="childDiv">
<h2> here is a child div</h2>
</div>
</div>
</div>
Here is a fiddle: http://jsfiddle.net/KFkjZ/
any insite as to why the events may not be binding is appreciated
It's because the balloon jquery plugin uses the clone and appends it to the body of the HTML when its first displayed. That breaks the event handler for your Lot view (as it means that the scope of the Backbone attached event handlers are no longer relevant).
One option which breaks the encapsulation would be to attach a document level event to handle the click in the way you want:
$(document).on('click', '.showChild', function (e) {
console.log('clicked');
$(this).slideToggle('fast');
});

Backbone events are not working

For some reason I don't know why my event in a Backbone View doesn't work.
I tried to Google for some answer but I didn't find anything that would help me.
Basically, my code is this:
Backbone:
var ViniView = Backbone.View.extend({
el: $('.container'),
events: {
"click .clickme" : "render"
},
render: function() {
alert("please, work");
}
});
new ViniView;
HTML
<div class="container">
<button class="clickme">
test
</button>
</div>
Your example works fine for me in this fiddle.
As explunit noted, though, your el should reference an element and should not be a jQuery object. $el takes care of that. According to the docs:
All views have a DOM element at all times (the el property), whether they've already been inserted into the page or not.
Check that you're correctly loading the Jquery, Underscore and Backbone scripts (in that order). Also make sure you're script is being executed once the page is ready and not, say, before your DOM has finished loading (causing your view to not attach to anything).

Backbone.js per attribute rendering (multiple small views vs multiple templates per view )

I have a model and a view. The view displays attributes of a model and allows the user to manipulate these attributes. The problem is that when an attribute is modified it re-renders the whole view which causes a lot of problems for me.
Example blur event on a text input saves the new input to an attribute and thus fires render. Which means that if the user clicked from that text input straight to a button on the same view that event will never fire as the first event that fires will be blur causing the whole view to re-render and thus losing the button click event.
I have two ideas:
Have a single view where every attribute is in a separate template. Then I bind to a particular attribute change event and in render I update only the html of the changed attribute. This seems like a hack, as there is a lot of work to force the view to update only the changed attribute. It will add a lot of unnecessary complexity to an already complex view.
Create a master view which consists of views, where each of them represents a model's attribute. This will create a lot of views, with nearly no functionality.
I seem to prefer the 2. option. What do you think? What are the best practices? Is there any better way to handle this?
I think you can do this quite easily.
Take a step back and think about where you are binding your events. It seems that you are binding them directly on top of each individual element instead of using a parent delegate.
Here's an example
Backbone.View.extend({
el: $("div.parent"),
events: function() {
this.$el.on("click", "input[type=button]", function(){});
// jquery cross browser on this
this.$el.on("blur", "input[type=text]", function(){});
},
initialize: function() {
this.model.bind("change", this.render, this);
},
render: function() {
this.$el.html('<input type="text" /><input type="button" />');
}
});
Here's what el and it's structure looks like
<div class="parent">
<input type="text" />
<input type="button" />
</div>
So this.$el points to div.parent. I can constantly rerender the contents of this.$el, and as long as the html structure dosen't change, I don't have to worry about events getting unbound. The other solution is that if I really cannot do delegation, I would just call the events method whenever I render again.
Like you said yourself, both of your options seem very complex. But sometimes additionaly complexity is a necessary evil. However, if the updated fields are something relatively simple (like binding a value to an element or an input field), I would simply update the DOM elements without creating additional View/Template abstractions on top of them.
Say you have a model:
var person = new Person({ firstName: 'John', lastName: 'Lennon', instrument:'Guitar' });
And a view which renders the following template:
<div>First Name: <span class="firstName">{{firstName}}</span></div>
<div>Last Name: <span class="lastName">{{lastName}}</span></div>
<div>Instrument: <input class="instrument" value="{{instrument}}"></input></div>
You could declare in the view which property change should update which element, and bind the model change event to a function which updates them:
var PersonView = Backbone.View.extend({
//convention: propertyName+"Changed"
//specify handler as map of selector->method or a function.
firstNameChanged: { '.firstName': 'text' },
lastNameChanged: { '.lastName': 'text' },
instrumentChanged: { '.instrument': 'val' },
otherFieldChanged: function(val) { //do something else },
initialize: function (opts) {
this.model.on('change', this.update, this);
},
//called when change event is fired
update: function(state) {
_.each(state.changed, function(val, key) {
var handler = this[key + "Changed"];
//handler specified for property?
if(handler) {
//if its a function execute it
if(_.isFunction(handler)) {
handler(val);
//if its an object assume it's a selector->method map
} else if(_.isObject(handler)) {
_.each(handler, function(prop, selector) {
this.$(selector)[prop](val);
}, this);
}
}
}, this);
}
A solution like this doesn't scale to very complex views, because you have to add classed elements to the DOM and maintain them in the View code. But for simpler cases this might work quite well.
In addition it's always good to try to compose views of multiple, smaller views, if they naturally divide into sections. That way you can avoid the need to update single fields separately.

Backbone view organization and partials

I have a rather complex Backbone application and I'm not sure how to organize the views/templates. The application is a web-based email client. I'm really having trouble understanding how to make this sidebar.
The application sidebar is very similar to what you see in Apple Mail/Outlook. It's basically a folder browser. This view exists on each page.
I have two main issues:
How do I get the collection data into the sidebar view?
There are technically three "collections" that get rendered on the sidebar - accounts => mailboxes, and labels. So some sample code would look like this:
<div class="sidebar">
<div class="sidebar-title">Mailboxes</div>
<div class="sidebar-subtitle">Gmail</div>
<div class="mailboxes" data-account-id="1">
<ul>
<li>Inbox</li>
<li>Sent</li>
...
</ul>
</div>
<div class="sidebar-subtitle">Yahoo</div>
<div class="mailboxes" data-account-id="2">
<ul>
<li>Inbox</li>
<li>Sent</li>
...
</ul>
</div>
<div class="sidebar-title">Labels</div>
<div class="sidebar-labels">
<ul>
<li>Home</li>
<li>Todo</li>
</ul>
</div>
</div>
So theoretically I need to do something like this, no?
<div class="sidebar">
<div class="sidebar-title">Mailboxes</div>
<% for account in #accounts.models: %>
<div class="sidebar-subtitle"><%= account.get('name') %></div>
<div class="mailboxes" data-account-id="<%= account.get('id') %>">
<ul>
<% for mailbox in account.mailboxes.models: %>
<li><%= mailbox.get('name') %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="sidebar-title">Labels</div>
<div class="sidebar-labels">
<ul>
<% for label in #labels: %>
<li><%= label.get('name') %></li>
<% end %>
</ul>
</div>
</div>
The problem is that I can't pass both #accounts and #labels from the router. I realize I could use #options, but that seems messy and unbackbone. This sidebar needs to exist on each page. What should that look like on the Backbone stack? I don't think it should have its own router, since it's not really a "page".
Do I break things up into smaller views?
Should each mailbox be its own view? Should each mailbox be its own view? Each label its own view? I want to listen for events, such as a new message, and update a mailboxes unread count (for example). But how do I do this? How do I nest the views and handle all the object creation/fetching efficiently? If I am going to break things into smaller views, what should my sidebar view look like?
Sorry if I'm rambling. I've been looking at this for days and can't seem to find a good solution.
Using options is totally legit and Backbone-y. So I wouldn't fear utilizing it although there are plenty of other ways to go with this.
Yeah. I think the sidebar should be initialized inside your bigger page-view which gets called by the router.
I also think your mailboxes should be its own sub-view. Sounds like regardless of whether the mail is yahoo or google, each mailbox will have the same functionality (like minimizing) so it makes a lot of sense to create that view class and reuse it multiple times.
So what should this mailbox view take? (with respect to collection)
Again, you have choices. Throw in 1 large collection that has everything, or throw in each collection that represents the mail from a type of account...
If your collections are already separate (e.g. 1 for Google mail, 1 for Yahoo mail) then it would be easy to create a subview and pass the appropriate collection into each one. Even if it was not separate, I'd probably filter the collection down to only the models it needs before passing it in as a collection. This is what I would lean toward. I'm assuming that in each mailbox view there is never a situation where that mailbox needs access to any mail model not associated with a particular account. So it doesn't make sense to pass in anything you don't need.
As for labels, I'm not sure. It depends on what and how your labels come to be I suppose. For example, do you have your own label model that is basically a label tag associated with a mail model.id? Or are labels an attribute of a mail models that you're plucking?
UPDATE with sample code - Focus on passing data through view hierarchy
So I basically outline here a very common type of way of making child views inside of parent views and how to pass different data (read: in your case collections) through the hierarchy of views.
You can do this for even more child views (like maybe each mail model has it's own representative child view as a list item inside the mailbox view etc.) by simply repeating the pattern, utilizing the [options] argument of the View constructor and passing stuff through and accepting it on the inside.
In many parts of this code, I take a long winded approach so as to make it more transparent exactly which data is getting passed where. I slightly altered your mailbox template just to illustrate a point as to how you might add more child-views but you can certainly change it back. What you do with your MailboxView is up to you and your code (depending on your goals) should reflect that.
So without further ado, here is some sample code to chew on and think about. I whipped this together so there might be errors. I didn't actually see if it would execute.
Normally in a view, we'd probably define the view.el as the most outer element in the template and only include the inner parts but to keep with the templates you provided and to decrease the number of possible confusing extras in the code, I've left the templates as they are for the most part.
// Your templates
<script id="sidebar" type="text/template">
<div class="sidebar">
<div class="sidebar-title">Mailboxes</div>
// Mailbox here
// Mailbox here
// Labels here
</div>
</script>
// I altered this template slightly just to illustrate a point on appending the model
// view data into your mailbox view. You might not want this but it's just for
// demonstration purposes
<script id="mailbox" type="text/template">
<div class="sidebar-subtitle">Gmail</div>
<div class="mailboxes" data-account-id="1">
<ul class="inbox"></ul> // Changed this
<ul class="sent"></ul>
</div>
</script>
<script id="labelBox" type="text/template">
<div class="sidebar-title">Labels</div>
<div class="sidebar-labels">
<ul>
<li>Home</li>
<li>Todo</li>
</ul>
</div>
</div>
</script>
Your application BIG view, main view that encompasses all.
AppView = Backbone.View.extend({
initialize: function() {
// You might not instantiate your collections here but it is a good starting
// point for this demo. We'll be passing these through children view and
// utilizing them in the appropriate views.
// Let's assume these collections we instantiate already have models inside them.
// Your 3 Collections
this.gmail = new MailCollection();
this.yahoo = new MailCollection();
this.labels = new LabelCollection();
},
render: function() {
this.$el.html();
// We pass each collection into the SidebarView [options] hash. We can "pick-up"
// these collections in the sidebar view via this.options.xxx
// Render the sidebar
var sidebar = new SidebarView({
'gmail':this.gmail,
'yahoo':this.yahoo,
'labels':this.labels
});
// Render the sidebar and place it in the AppView... anywhere really
// but for simplicity I just append it to the AppView $el
this.$el.append(sidebar.render().el);
return this;
}
});
Your sidebar view:
SidebarView = Backbone.View.extend({
template: _.template($('#sidebar').html()),
initialize: function() {
// We passed the 3 collections into Sidebar view and can access
// them through the options. We could have instantiated them here
// but I passed them into the side to illustrate that using
// options for this kind of thing is a-okay.
this.gmail = this.options.gmail,
this.yahoo = this.options.yahoo,
this.labels = this.options.labels
// This is an array of sub-view mailboxes so when we close this view,
// it's easy to loop through the subviews and close both mailboxes
this.mailboxes = [];
},
render: function() {
// We render the sidebarView using the template
this.$el.html(this.template());
// We generate the sub-view mailbox views (gmail and yahoo)
var gmailView = new MailboxView({
'collection': this.gmail // We pass in only the gmail collection
});
var yahooView = new MailboxView({
'collection': this.yahoo // Pass in the yahoo mail collection
});
var labelView = new LabelboxView({
'collection': this.labels // Pass in the labels collection
});
// We push the views into our array
this.mailboxes.push(gmailView);
this.mailboxes.push(yahooView);
this.mailboxes.push(labelView);
// Render each view and attach it to this sidebar view
this.$el.append(gmailView.render().el);
this.$el.append(yahooView.render().el);
this.$el.append(labelView.render().el);
return this;
},
onClose: function() {
// Sample code of how we close out child views. When this parent view closes,
// it automatically cleans up the child views.
_.each(this.mailboxes, function(view) {
view.close(); // A special function that I use to close views
});
}
});
See Zombie View Cleanup for more details on the onClose() and close() methods I use. Particularly helpful once you start creating lots of views / sub-view relationships in your apps.
Your Mailbox View:
MailboxView = Backbone.View.extend({
template: _.template($('#mailbox').html()),
initialize: function() {
// When we pass something in as 'collection' backbone automatically
// attaches it as a property of the view so you can use the collection
// as this.collection rather than this.options.collection for
// convenience
},
render: function() {
this.$el.html(this.template());
this.loadMail();
return this;
},
loadMail: function() {
// Some code that loops through your collection and adds the mail to an
// appropriate DOM element. Would make sense to make Inbox a ul and each
// mail an li so I altered your template to demonstrate this
// Let's assume a mail model has the attr 'author' and 'body' in this
// simple example
// This will loop through your collection and use a mail template to
// populate your list with the appropriate data.
this.collection.each(function(mail) {
this.$('ul').append(_.template('<li><%=author%><%=body%></li>', mail.toJSON()))
});
// Alternatively, you could (and probably should) make each mail model
// represented by ANOTHER sub-view, a mail subview that has all the
// functionality that mail views usually have.
// To accomplish this you just repeat the prior demonstrated cycle
// of creating child view inside child view, passing in the appropriate
// data that you need, creating the view, and attaching it to the parent
// where you would like.
}
});
Your LabelsView:
LabelboxView = Backbone.View.extend({
template: _.template($('#labelBox').html()),
initialize: function() {
// I think you get the gist from above, you'd do the same thing as each
// mailbox view except deal with the unique aspects of labels, whatever
// these may be (and do)
},
render: function() {
this.$el.html(this.template());
return this;
}
});

Backbone.js turning off wrap by div in render

I have model Post and collection Posts. And want to make form with list of all post in <select id="multi" multiple="multiple">. So i have to make a PostView render inside my #multi with just this template:
<option value=""><%= title %></option>
But finally I get it wrapped with div. Is there any solution for not wrapping this template with <div>?
If you don't define an el (or tagName) for the view (in the class or during instantiation) the view will be placed inside a div tag. http://documentcloud.github.com/backbone/#View-el
var PostView = Backbone.View.extend({
tagName: 'option'
});
UPDATE
Starting v0.9.0, Backbone has view.setElement(element) to get this done.
var PostView = Backbone.View.extend({
initialize: function() {
var template = _.template('<option value=""><%= title %></option>');
var html = template({title: 'post'});
this.setElement(html);
}
});
If you don't want to have the view wrap your HTML, you'll have to do a few things:
Replace this.el entirely
Call delegateEvents on the new el
render: function(){
var html = "some foo";
this.el = html;
this.delegateEvents(this.events);
}
Since Backbone generates a div or other tag (based on your tagName setting for the view), you have to replace it entirely. That's easy to do. When you do that, though, you lose your declared events because Backbone uses jQuery's delegate under the hood to wire them up. To re-enable your declared events, call delegateEvents and pass in your events declarations.
The result is that your view.el will be the <option> tag that you want, and nothing more.
In version 0.9.0, Backbone introduced view.setElement(element) to handle this operation.

Resources