Create a view without a defined el property with events - Backbone - javascript

I've been trying to learn Backbone, and I'm developing an app now. But I have a problem with a view's events: App.views.ChannelView should have a click event, but it is not firing.
Here's the code:
http://pastebin.com/GgvVHvtj
Everything get rendered fine, but events won't fire. Setting the el property in the view will work, but I can't use it, and I've seen on Backbone's todo tutorial that it is possible.
How do I make events fire without a defined el property?

You must define the el element to be an existing element in your DOM. If you do not define it, fine, it will default to a div, but when you render the view, the html generated must be appended/prepended whatever, you get the point, to an existing DOM element.
Events are scoped to the view, so something's wrong with your scope. From the code you provided I can't reproduce the problem, so if you might, please provide a live example on jsfiddle/jsbin etc in order to fully understand the issue.
Demo ( in order to demonstrate the view render )
var App = {
collections: {},
models: {},
views: {},
};
App.models.Channel = Backbone.Model.extend({
defaults: {
name: '#jucaSaoBoizinhos'
}
});
App.views.ChannelView = Backbone.View.extend({
el:$('#PlaceHolder'),
events: {
"click .channel": "myhandler"
},
render: function() {
this.$el.html('<div class="channel"><button>' + this.model.get('name') + '</button></div>');
return this;
},
myhandler: function(e) {
alert(e);
console.log(this.model.get('name'));
},
});
var chView = new App.views.ChannelView({model: new App.models.Channel()});
//console.log(chView.render().el) //prints div#PlaceHolder
//without the el specified in the view it would print a div container
//but i would have to render the view into an existing DOM element
//like this
//$('#PlaceHolder').html(chView.render().el)
chView.render()

Can you try doing a
events: {
"all": "log"
}
log: function(e) {
console.log e;
}
That should log out every event that's getting fired. I find it super helpful when troubleshooting.

backbone view events can work without dom element specified. If you can't use any element at the view createion (initialization) moment, then you can use it's 'setElement' method, to attach your view to specified dom element. Here is description.
Be the way your view render method will not work also without specified 'el'.

Related

marionette.js vs Backbone updating DOM

I have noticed there is a different, the way DOM been updated by Marionette comparing with Backbone. I have created two simple fiddles. One using Backbone and other one based on marionette. Both examples has a helper method call processDom and that method simply throw an error after iterating 50 times.
However in backbone example elements been appended to DOM till die() method fires. But in marionette based example DOM has not been updated at all. It would be great if someone can explain how this works. I wonder marionette internally using a virtual dom kind of a technique.
Render method in marionette example
render: function () {
var viewHtml = this.template({
'contentPlacement': 'Sydney'
});
this.$el.html(viewHtml);
this.processDom(this.$el);
this.postRender();
}
Render method in backbone example
render: function () {
var template = Handlebars.compile($('#sample-template').html());
var viewHtml = template({
'contentPlacement': 'Sydney'
});
this.$el.html(viewHtml);
this.processDom(this.$el);
this.postRender();
},
Links to fiddle examples
http://jsfiddle.net/samitha/v5L7c2t5/7/
http://jsfiddle.net/samitha/pc2cvmLs/7/
In general for post processing purposes you could use onRender of Marionette.ItemView. It's not a good practice to rewrite Marionette.ItemView.render.
Rendering of views inside the regions for Marionette is handled a bit different as in Backbone case.
When you rendering Backbone.View - your element will attach itself to the DOM's $('#search-container'), and in render method it will operate with already attached element so you can see the changes.
When you rendering Marionette.ItemView with Marionette.Region.show method, Marionette.Region already attached to the DOM and it need to render appropriate view (in your case ItemView) and only after that step it will attach it to the DOM and will set the ItemView as currentView.
You can see from source of Marionette.Region.show that it attaches view after render is called. In your case render will throw error and it will never be attached to the DOM.
To understand it deeper lets look at the 2 methods which are responsible for attaching/creating views in BackboneView. Marionette uses the same method for Marionette.ItemView.
_ensureElement: function() {
if (!this.el) { // case when el property not specified in view
var attrs = _.extend({}, _.result(this, 'attributes'));
if (this.id) attrs.id = _.result(this, 'id');
if (this.className) attrs['class'] = _.result(this, 'className');
// creating new jQuery element(tagNam is div by default) with attributes specified
var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
this.setElement($el, false);
} else { // case when el property exists
this.setElement(_.result(this, 'el'), false);
}
}
And
setElement: function(element, delegate) {
if (this.$el) this.undelegateEvents();
// here is all magic
// if element is instance of Backbone.$, which means that el property not specified
// or it's already jquery element like in your example( el: $('#search_container')) with Backbone.
// this.$el will be just a jQuery element or already attached to the DOM jQuery element
// in other case it will try to attach it to the DOM.
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
this.el = this.$el[0];
if (delegate !== false) this.delegateEvents();
return this;
},
As you can see from comments all magic is in a few lines of code, and that's why I love it.

attaching backbone.js views to existing elements vs. inserting el into the DOM

I am implementing my first actual non-tutorial Backbone app, and have 2-ish questions about an aspect of using using backbone.js that isn't settling very well with me, which relates to injecting a view's rendered el into the DOM vs. using an existing element for el. I suspect I will provide you all with a few "teachable moments" here, and appreciate the help.
Most Backbone View examples I see in the web specify tagName, id and/or className, when creating a View and thereby create an el that is unattached from the DOM. They typically look something like:
App.MyView = Backbone.View.extend({
tagName: 'li',
initialize: function () {
...
},
render: function () {
$(this.el).html(<render an html template>);
return this;
}
});
But the tutorials don't always get around to explaining how they recommend getting the rendered el into the DOM. I've seen it a few different ways. So, my first question is: where is the appropriate place to call a view's render method and insert its el into the DOM? (not neccessarily one and the same place). I've seen it done in a router, in the view's initialize or render functions, or just in a root level document ready function. ( $(function () ) . I can imagine that any of these work, but is there a right way to do it?
Second, I am starting with some HTML markup/wireframe, and converting html portions to js templates corresponding to backbone views. Rather than let the view render an unattached element and providing an anchor point in the html to stick it in, I feel like its more natural, when there is only going to be one element for a view and it won't be going away, to use an existing, emptied wrapper element (often a div or span) as the el itself. That way I don't have to worry about finding the place in the document to insert my unattached el, which would potentially end up looking like this (note the extra layering):
<div id="insert_the_el_in_here"> <!-- this is all that's in the original HTML doc -->
<div id="the_el"> <!-- i used to be a backbone generated, unattached el but have been rendered and inserted -->
<!-- we're finally getting to some useful stuff in here -->
</div>
</div>
So part of my second question is, for a basically static view, is there anything wrong with using an existing element from the page's HTML directly as my view's el? This way I know its already in the DOM, in the right place, and that calling render will immediately render the view on the page. I would acheive this by passing the already exixting element to my view's constsructor as 'el'. That way, it seems to me, i don't have to worry about sticking it into the DOM (making question 1 sort of moot), and calling render will immediately update the DOM. E.g.
<form>
<div someOtherStuff>
</div>
<span id="myView">
</span>
</form>
<script type="text/template" id = "myViewContents"> . . . </script>
<script type="application/javascript">
window.MyView = Backbone.View.extend( {
initialize: function () {
this.template = _.template($('#myViewContents').html());
this.render();
},
render: function () {
$(this.el).html(this.template());
return this;
}
});
$(function () {
window.myView = new MyView({ el: $('#myView').get(0) });
});
</script>
Is this an OK way to do it for static views on the page? i.e., there is only one of these views, and it will not go away in any circumstance. Or is there a better way? I realize that there may be different ways to do things (i.e., in a router, in a parent view, on page load, etc.) based on how I am using a view, but right now I am looking at the initial page load use case.
Thanks
There's absolutely nothing wrong with the idea of attaching a view to an existing DOM node.
You can even just put the el as a property on your view.
window.MyView = Backbone.View.extend( {
el: '#myView',
initialize: function () {
this.template = _.template($('#myViewContents').html());
this.render();
},
render: function () {
this.$el.html(this.template()); // this.$el is a jQuery wrapped el var
return this;
}
});
$(function () {
window.myView = new MyView();
});
What I recommend is, do what works... The beauty of Backbone is that it is flexible and will meet your needs.
As far as common patterns are concerned, generally I find myself having a main view to keep track of over views, then maybe a list view and individual item views.
Another common pattern as far as initialization is concerned is to have some sort of App object to manage stuff...
var App = (function ($, Backbone, global) {
var init = function () {
global.myView = new myView();
};
return {
init: init
};
}(jQuery, Backbone, window));
$(function () {
App.init();
});
Like I said before though, there's really no WRONG way of doing things, just do what works.
:)
Feel free to hit me up on twitter #jcreamer898 if you need any more help, also check out #derickbailey, he's kind of a BB guru.
Have fun!
You can also send an HTML DOM Element object into the view as the 'el' property of the options.
window.MyView = Backbone.View.extend( {
initialize: function () {
this.template = _.template($('#myViewContents').html());
this.render();
},
render: function () {
this.$el.html(this.template()); // this.$el is a jQuery wrapped el var
return this;
}
});
$(function () {
window.myView = new MyView({
el: document.getElementById('myView')
});
});
Use delegate events method:
initialize: function() {
this.delegateEvents();
}
To understand why: http://backbonejs.org/docs/backbone.html#section-138 near "Set callbacks, where"
Looks also like these days you can youse setElement.

Why is the click event triggered several times in my Backbone app?

I'm making a Backbone.js app and it includes an index view and several subviews based on id. All of the views have been bound with mousedown and mouseup events. But every time I go from a subview to the index view and then go to any of subviews again, the mousedown and mouseup events in the current subview will be triggered one more time, which means when I click the subview, there will be several consecutive mousedown events triggered followed by several consecutive mouseup events triggered.
After looking through my code, I finally found that it's the router that causes this problem. Part of my code is as follows:
routes: {
"": "index",
"category/:id": "hashcategory"
},
initialize: function(options){
this._categories = new CategoriesCollection();
this._index = new CategoriesView({collection: this._categories});
},
index: function(){
this._categories.fetch();
},
hashcategory: function(id){
this._todos = new TodosCollection();
this._subtodolist = new TodosView({ collection: this._todos,
id: id
});
this._todos.fetch();
}
As you can see, I create the index collection and view in the initialize method of the router, but I create the subview collection and view in the corresponding route function of the router. And I tried to put the index collection and view in the index function and the click event in index view will behave the same way as subviews. So I think that's why the mousedown and mouseup will be triggered several times.
But the problem is, I have to use the id as one of the parameters sent to subview. So I can't create subview in the initialize method. What's more, I've already seen someone else's projects based on Backbone and some of them also create sub collection and view in the corresponding route function, but their app runs perfectly. So I don't know what is the root of my problem. Could someone give me some idea on this?
Sounds like you're having a delegate problem because:
all sub views all use the a same <div> element
Backbone views bind to events using jQuery's delegate on their el. If you have a view using a <div> as its el and then you use that same <div> for another view by replacing the contained HTML, then you'll end up with both views attached to that <div> through two different delegate calls. If you swap views again, you'll have three views receiving events through three delegates.
For example, suppose we have this HTML:
<div id="view-goes-here"></div>
and these views:
var V0 = Backbone.View.extend({
events: { 'click button': 'do_things' },
render: function() { this.$el.html('<button>V0 Button</button>'); return this },
do_things: function() { console.log('V0 clicked') }
});
var V1 = Backbone.View.extend({
events: { 'click button': 'do_things' },
render: function() { this.$el.html('<button>V1 Button</button>'); return this },
do_things: function() { console.log(V1 clicked') }
});
and we switch between them with something like this (where which starts at 0 of course):
which = (which + 1) % 2;
var v = which == 0
? new V0({el: $('#view-goes-here') })
: new V1({el: $('#view-goes-here') });
v.render();
Then you'll have the multi-delegate problem I described above and this behavior seems to match the symptoms you're describing.
Here's a demo to make it easy to see: http://jsfiddle.net/ambiguous/AtvWJ/
A quick and easy way around this problem is to call undelegateEvents on the current view before rendering the new one:
which = (which + 1) % 2;
if(current)
current.undelegateEvents(); // This detaches the `delegate` on #view-goes-here
current = which == 0
? new V0({el: $('#view-goes-here') })
: new V1({el: $('#view-goes-here') });
current.render();
Demo: http://jsfiddle.net/ambiguous/HazzN/
A better approach would be to give each view its own distinct el so that everything (including the delegate bindings) would go away when you replaced the HTML. You might end up with a lot of <div><div>real stuff</div></div> structures but that's not worth worrying about.

How can I "bubble up" events in a Backbone View hierarchy?

I have a backbone app with a view structure that looks like the following - note that I've removed implementations, models, collections, etc. for brevity:
NewsListView = Backbone.View.extend({
el: $('li#newspane'),
// This is what I would like to be able to do
// events: { 'filtered': 'reset' }
initialize: function() {
_.bindAll(this);
},
render: function() {
},
reset: function(){
}
});
FilterView = Backbone.View.extend({
el: $('li.filter'),
initialize: function() {
},
render: function() {
},
toggleFilter: function() {
}
});
AllView = Backbone.View.extend({
initialize: function() {
this.newsListView = new NewsListView();
this.filterView = new FilterView();
}
});
Essentially, whenever the FilterView's toggleFilter() function is called, I would like to fire off an event called filtered or something like that that is then caught by the NewsListView, which then calls its reset() function. Without passing a reference of a NewsListView object to my FilterView, I'm not sure how to send it an event. Any ideas?
You're on the right track. It sounds like what you need is a global event dispatcher. There a decent article and example here: http://www.michikono.com/2012/01/11/adding-a-centralized-event-dispatcher-on-backbone-js/
You might be able to do this using the already available functionality of jquery events and the backbone events property.
For example, instead of doing this from inside your subview:
this.trigger("yourevent", this);
do this instead:
this.$el.trigger("yourevent", this);
Then in any view that is a parent, grandparent, etc of your child view, listen for the event on that view's $el by defining a property on that view's events object:
events:{
"yourevent":"yourhandler"
}
and define the handler on that view as well:
yourhandler:function(subview) {
}
So this way, a view doesn't need to know about what descendant views exist, only the type of event it is interested in. If the view originating the event is destroyed, nothing needs to change on the ancestor view. If the ancestor view is destroyed, Backbone will detach the handlers automatically.
Caveat: I haven't actually tried this out yet, so there may be a gotcha in there somewhere.
You should check out the Backbone.Courier plugin as bubbling events is a perfect use case:
https://github.com/dgbeck/backbone.courier
The easiest way I've found to trigger and listen to events is to just use the Backbone object itself. It already has the events functions mixed in to it, so you can just trigger eg:
Backbone.trigger('view:eventname',{extra_thing:whatever, thing2:whatever2});
then, in any other backbone view in your app, you can listen for this event eg:
Backbone.on('view:eventname', function(passed_obj) {
console.log(passed_obj.extra_thing);
});
I'm not exactly sure what the advantage is in not using the Backbone object as your event handler, and instead creating a separate object to do it, but for quick-and-dirty work, the above works fine. HTH!
NOTE: one disadvantage to this is that every listener will "hear" every single event triggered in this way. Not sure what the big O is on that, but work being careful to not overload your views with lots of this stuff. Again: this is quick and dirty! :)
This problem can be solved using small backbone.js hack. Simply modify Backbone.Events.trigger for passing events to the this.parent
if this.parent != null
So, I came up a with a solution - create an object that extends Backbone.Events, and pass it as a parameter to multiple views. This almost feels like message passing between actors, or something. Anyway - I'm posting this as an answer in case anybody else needs a quick solution, but I'm not going to accept the answer. This feels hacky. I'd still like to see a better solution.
NewsListView = Backbone.View.extend({
el: $('li#newspane'),
// Too bad this doesn't work, it'd be really convenient
// events: { 'filtered': 'reset' }
initialize: function() {
_.bindAll(this);
// but at least this does
this.options.eventProxy.bind('filtered', this.reset);
},
render: function() {},
reset: function() {}
});
FilterView = Backbone.View.extend({
el: $('li.filter'),
initialize: function() {},
render: function() {},
toggleFilter: function() {
this.options.eventProxy.trigger('filtered');
}
});
AllView = Backbone.View.extend({
initialize: function() {
var eventProxy = {};
_.extend(eventProxy, Backbone.Events);
this.newsListView = new NewsListView({eventProxy: eventProxy});
this.filterView = new FilterView({eventProxy: eventProxy});
}
});

Backbone refresh view events

In my view I don't declare this.el because I create it dinamically, but in this way the events don't fire.
This is the code:
View 1:
App.Views_1 = Backbone.View.extend({
el: '#content',
initialize: function() {
_.bindAll(this, 'render', 'renderSingle');
},
render: function() {
this.model.each(this.renderSingle);
},
renderSingle: function(model) {
this.tmpView = new App.Views_2({model: model});
$(this.el).append( this.tmpView.render().el );
}
});
View 2:
App.Views_2 = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'render');
},
render: function() {
this.el = $('#template').tmpl(this.model.attributes); // jQuery template
return this;
},
events: {
'click .button' : 'test'
},
test: function() {
alert('Fire');
}
});
});
When I click on ".button" nothing happens.
Thanks;
At the end of your render() method, you can tell backbone to rebind events using delegateEvents(). If you don't pass in any arguments, it will use your events hash.
render: function() {
this.el = $('#template').tmpl(this.model.attributes); // jQuery template
this.delegateEvents();
return this;
}
As of Backbone.js v0.9.0 (Jan. 30, 2012), there is the setElement method to switching a views element and manages the event delegation.
render: function() {
this.setElement($('#template').tmpl(this.model.attributes));
return this;
}
Backbone.View setElement: http://backbonejs.org/#View-setElement
setElementview.setElement(element)
If you'd like to apply a Backbone view to a different DOM element, use
setElement, which will also create the cached $el reference and move
the view's delegated events from the old element to the new one.
Dynamically creating your views in this fashion has it's pros and cons, though:
Pros:
All of your application's HTML markup would be generated in templates, because the Views root elements are all replaced by the markup returned from the rendering. This is actually kind of nice... no more looking for HTML inside of JS.
Nice separation of concerns. Templates generate 100% of HTML markup. Views only display states of that markup and respond to various events.
Having render be responsible for the creation of the entire view (including it's root element) is in line with the way that ReactJS renders components, so this could be a beneficial step in the process of migrating from Backbone.Views to ReactJS components.
Cons: - these are mostly negligible
This wouldn't be a painless transition to make on an existing code base. Views would need to be updated and all templates would need to have the View's root elements included in the markup.
Templates used by multiple views could get a little hairy - Would the root element be identical in all use cases?
Prior to render being called, the view's root element is useless. Any modifications to it will be thrown away.
This would include parent views setting classes/data on child view elements prior to rendering. It is also bad practice to do this, but it happens -- those modifications will be lost once render overrides the element.
Unless you override the Backbone.View constructor, every view will unnecessarily delegate it's events and set attributes to a root element that is replaced during rendering.
If one of your templates resolves to a list of elements rather than a single parent element containing children, you're going have a bad time. setElement will grab the first element from that list and throw away the rest.
Here's an example of that problem, though: http://jsfiddle.net/CoryDanielson/Lj3r85ew/
This problem could be mitigated via a build task that parses the templates and ensures they resolve to a single element, or by overriding setElement and ensuring that the incoming element.length === 1.

Categories