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).
Related
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.
I have some code which defines a backbone view being loaded as soon as a web page is loaded. The JavaScript is possibly executed before the DOM is fully loaded. If the dom element which becomes $el is not available when the code that defines the view is run is this a problem?
Programmatically I have something like this:
var view = Backbone.View.extend({
el: jQuery("#test")
events: {
}
render: function() {
this.$el.html();
}
//other view code
});
return view;
//some time passes
//with the view rendered above I now call :
view.render()
the problem is that when the render method is called this.$el is undefined. This is because when the first block of code was executed #test had not been loaded into the DOM. So; when the function Backbone.View.extend is called the 'el' element has to be loaded?
The context of this is a backbone application loaded via AMD. The first block of code is in a module. The module is 'required' before the DOM is loaded. Is this a common problem? How is it normally dealt with?
Thanks
el should not be a jQuery object but only a selector, $el will be the jQuery object for that selector. Also the id you specify as the el has to be found at initialization. So yes, the element has to be in the dom before you make it the el for the view.
If you're not creating / populating the view before making it a Backbone view you could do something like this to have backbone create the html tags etc. for you:
var view = Backbone.View.extend({
tagName: 'div',
id: 'test',
events: {
},
render: function() {
this.$el.html();
},
//other view code
});
You could then use templates to populate the view in your render function.
Short answer: Yes, the view element has to be loaded into the DOM when you initialize the view.
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');
});
I'm having a very perplexing issue which I've never seen before. I wonder if I'm missing something very obvious.
I have an template like so (which I compile with Underscore):
<a href="#advanced-search" data-toggle="modal">
Advanced search
</a>
<div id="advanced-search">
<div class="modal-header">header</div>
<div class="modal-body">
<form id="advanced-search-form">
// This has form elements
</form>
</div>
<div class="modal-footer">footer</div>
</div>
The problem is that whenever I re-render the Backbone View which is using the template, the form element is not rendered! The initial render works fine.
I've tried separating the form element from the advanced-search-form id by nesting a <div id="advanced-search-form"> within the form element and removing the id from the form element. The result is that the nested div is rendered while the form still is not.
I need the form element in order to use serializeArray, which only works on form elements.
I suppose a workaround would be to write a function which can serialize input elements within any element, but this is not ideal and I would really like to discover why this strange thing is happening.
Thanks for looking!
EDIT: the render code
Right now I am not re-rendering based on any collection or model events. I am manually triggering the re-render via a click event on a link in the parent view. The following is the basic structure of my two views (CoffeeScript):
AdvancedSearchView = Backbone.View.extend
template: _.template AdvancedSearchTpl #available via requirejs
render: ->
#$el.html #template()
ParentView = Backbone.View.extend
initialize: ->
_.bindAll #
$('body #refresh').livequery 'click', #refreshAdvancedSearch
render: ->
if !#advancedSearchElem #cache the view to save state
#advancedSearchView = new AdvancedSearchView
#advancedSearchElem = #advancedSearchView.render().el
$('#content').html #advancedSearchElem
refreshAdvancedSearch: ->
#advancedSearchElem = #advancedSearchView.render().el #from here, the template is rendered without the form element
#render()
parentView = new ParentView
parentView.render()
Later on I realized I was rendering a <form> inside another form tag, and I believe Chrome automatically fixed the invalid html by removing the inner form tag. This was probably my issue, as console.logging the html output of the template included the inner form tag.
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.