Trouble getting Backbone.js view event to fire - javascript

It appears as though the following code is getting inside initialize but my event doesn't appear to be firing.
What am I missing here?
var index = (function ($, window, document) {
var methods = {};
methods = {
init: function () {
},
getView: Backbone.View.extend({
el: $('.settings'),
events: {
'click .settings': 'addUl'
},
initialize: function () {
console.log('init');
},
render: function () {
},
addUl: function () {
console.log('addUI');
this.el.append("<ul> <li>hello world </li> </ul>");
}
})
};
return methods; } (jQuery, window, document));
var stuff = new index.getView();
Link to the jsbin

Remove the space in 'click .settings'

Actually remove .settings entirely.
'click .settings' is registering a click handler for a descendant of this.el that matches '.settings'.
In your example you want to register an event on this.el directly so you don't need the descendant selector.

The problem is that it is your view element ($el) that has the settings class and not a child.
click .settings tells backbone to bind a "click" event on the $el for any children that have .settings. However, because, it is $el which has the class settings the binding never match.
This is why when you remove .settings it works, because you say "any 'click' on $el"
The reason the documentation says click .blah is because it assumes that the html element(s) with the class='blah' are children of the $el element.
Hope this help.

Related

Trigger view event with jQuery

I have a Backbone View with simple events:
Backbone.View.extend({
events: {
"change #id": "idChanged"
},
idChanged: function () {}
initialize: function () {
/* construct HTML */
$("#id").trigger("change");
}
});
However this does not fire the idChanged event. When I change #id with the browser it does fire. How can I trigger the Backbone View event?
a couple of things in your code.
1 I don't think you defined your events correctly.
It should be a hash, or a function that returns a hash, like so:
events: {
"change #id": "idChanged"
}
2 a few typos like "function" and missing comma
then, to make the events work, the defined #id element must be inside the view's el. If the element is outside of the view, it's not gonna work.
also, you cannot trigger that in initialize, because before that function is executed, the view is not fully initialized yet. :)
here's a working example:
http://jsfiddle.net/3KmzQ/
That's because the events hash will be bound to the view when it gets rendered, which happens after the initialize code gets run. Try calling the desired callback directly:
Backbone.View.extend({
events: function () {
"change #id": "idChanged"
},
idChanged: function () {}
initialize: function () {
/* construct HTML */
this.idChanged();
}
});
You used "extend".
Same code should apply to Backbone.view.Object( {....} )
Specify the object that you would like to fire events at.
Backbone.View.Ojbect(
{
events: function () {
"change #id": "idChanged"
},
idChanged: funciton () {}
initialize: function () {
/* construct HTML */
$("#id").trigger("change");
}
}
);
That is, try not to extend.

How does this click handler get assigned to a DOM element?

Now that I understand Backbone a little better (I Hope) I've been going through this App with a fine tooth comb to understand how it works:
https://github.com/ccoenraets/nodecellar/tree/master/public
The latest thing that's stumped me is the EL tag in windetails.js (here: https://github.com/ccoenraets/nodecellar/blob/master/public/js/views/winedetails.js)
I'll paste the relevant code below, but my question is how does this view's EL property get assigned? As you'll notice in the view definition no EL tag is defined, nor is there an idTag or className property assigned. However I verified in firebug that this view is indeed listening on a DIV tag in the middle of the DOM (just underneath the content DIV actually). So how did it get attached there? If not for that the Click handler would not work properly but it does. All of the previous views which look like there were created in the same way have unattached EL properties.
window.WineView = Backbone.View.extend({
initialize: function () {
this.render();
},
render: function () {
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
events: {
"change" : "change",
"click .save" : "beforeSave",
"click .delete" : "deleteWine",
"drop #picture" : "dropHandler"
},
change: function (event) {
// Remove any existing alert message
utils.hideAlert();
// Apply the change to the model
var target = event.target;
var change = {};
change[target.name] = target.value;
this.model.set(change);
// Run validation rule (if any) on changed item
var check = this.model.validateItem(target.id);
if (check.isValid === false) {
utils.addValidationError(target.id, check.message);
} else {
utils.removeValidationError(target.id);
}
},
beforeSave: function () {
var self = this;
var check = this.model.validateAll();
if (check.isValid === false) {
utils.displayValidationErrors(check.messages);
return false;
}
this.saveWine();
return false;
},
saveWine: function () {
var self = this;
console.log('before save');
this.model.save(null, {
success: function (model) {
self.render();
app.navigate('wines/' + model.id, false);
utils.showAlert('Success!', 'Wine saved successfully', 'alert-success');
},
error: function () {
utils.showAlert('Error', 'An error occurred while trying to delete this item', 'alert-error');
}
});
},
deleteWine: function () {
this.model.destroy({
success: function () {
alert('Wine deleted successfully');
window.history.back();
}
});
return false;
},
dropHandler: function (event) {
event.stopPropagation();
event.preventDefault();
var e = event.originalEvent;
e.dataTransfer.dropEffect = 'copy';
this.pictureFile = e.dataTransfer.files[0];
// Read the image file from the local file system and display it in the img tag
var reader = new FileReader();
reader.onloadend = function () {
$('#picture').attr('src', reader.result);
};
reader.readAsDataURL(this.pictureFile);
}
});
EDIT
There's been a lot of talk about this pattern:
$(x).append(v.render().el)
Someone correct me if I'm wrong but as I understand it this is a Jquery call to update the DOM at the "x" tag with the contents of the "el" property from the v object (after render is called). This technique should render content into the DOM EVEN IF the "el" property has not previously been set and is an "unattached div" provided it has had valid content previously written to it from the render method.
However after the content has been written to the DOM the "el" property still remains an unattached div until it is directly assigned to the DOM.
I verified through Firebug that this Backbone app has two views which are rendered this exact way and both have unattached div el properties. Those are the wineList view and the homeView. However, the 3rd view is the WineDetail view and it does not seem to have an unattached EL property. It's EL property seems to be attached and furthermore is facilitating a click event. My question is how did this EL property get attached and assigned to the DOM?
The answer can be found by looking at the internals of Backbone.View.
Looking at the constructor:
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
this._configure(options || {});
//this function is responsible for the creation of the `this.el` property.
this._ensureElement();
this.initialize.apply(this, arguments);
this.delegateEvents();
};
Ensure that the View has a DOM element to render into. If this.el is a
string, pass it through $(), take the first matching element, and
re-assign it to el. Otherwise, create an element from the id,
className and tagName properties. http://backbonejs.org/docs/backbone.html#section-133
Now that we know where this.el comes from, have a look at the events docs to see how it's handled.
The view is instantiated in main.js
$('#content').html(new WineView({model: wine}).el);
EDIT:
None of which explains how the View Object's EL property is set and
and how the click trigger works.
I will try to explain it better:
this.el is created by a call to this._ensureElement in the Backbone.View constructor. We can also see that this.render is called from the initialize function which runs at instanciation time. We can see that in this.render, we set the content of this.el to the result of applying this.template to the model.
Now, during the initialization process of a Backbone.View, right after this.initialize is called, the events config is processed by making a call to this.delegateEvents. This is where event listeners will get attached using the given selectors. Note that most events will get attached directly to this.el and make use of event delegation, instead of attaching the events directly on the children elements.
At this point, we are left with a this.el that contains all the necessary markup and has all the event listeners setup. However, this.el is still not part of the DOM yet.
But from the code, we can see that this.el will be attached to the DOM as a children of the #content element after the instanciation of the view:
$('#content').html(new WineView({model: wine}).el);
The last three lines in this piece of code:
events: {
"change" : "change",
"click .save" : "beforeSave",
"click .delete" : "deleteWine",
"drop #picture" : "dropHandler"
},
look like this pattern (looking at the 2nd line in the events structure):
"click" = event to register a handler for
".save" = selector to use for selecting objects for the event handler
beforeSave = method to call when the event fires

Backbone.js event handler not working

I was trying the backbone.js examples given here and then tried writing some code on my own.
For some reason the event handler I have attached to event 'click p' is not working. Why is the 'highlight' function not executing when a paragraph tag is clicked?
var ItemView = Backbone.View.extend({
tagName : 'p',
events: {
'click p': 'highlight'
},
initialize: function(){
console.log("An object of ItemView was created");
_.bindAll(this, 'render', 'highlight');
this.render();
},
render: function(){
this.$el.text(this.model.get('content'));
$('body').append(this.$el);
return this;
},
highlight: function(){
console.log('clicked');
}
});
This event it targeting any <p> element inside your root element. It is not targeting your root element, even if your root element is a <p> element.
Try:
events: {
'click': 'highlight'
}
To target the root element.
Hmmm... in your render, you replace all the body content with this.$el. I'm thinking you've effectively "unhooked" the events hash when you replace all the content. So after you do the append, call this.delegateEvents() like so:
render: function(){
this.$el.text(this.model.get('content'));
$('body').append(this.$el);
this.delegateEvents();
return this;
}
This should allow you to click on your paragraph tags.

View Events not firing on created elements?

Trying to create a todo example app to mess around with backbone. I cannot figure out why the click event for the checkbox of a task is not firing. Here is my code for the TaskCollection, TaskView, and TaskListView:
$(document).ready(function() {
Task = Backbone.Model.extend({});
TaskCollection = Backbone.Collection.extend({
model: 'Task'
});
TaskView = Backbone.View.extend({
tagName: "li",
className: "task",
template: $("#task-template").html(),
initialize: function(options) {
if(options.model) {
this.model = options.model
}
this.model.bind('change',this.render,this);
this.render();
},
events: {
"click .task-complete" : "toggleComplete"
},
render: function(){
model_data = this.model.toJSON();
return $(_.template(this.template, model_data));
},
toggleComplete: function() {
//not calling this function
console.log("toggling task completeness");
}
});
TaskListView = Backbone.View.extend({
el: $("#task-list"),
task_views: [],
initialize: function(options) {
task_collection.bind('add',this.addTask,this);
},
addTask: function(task){
task_li = new TaskView({'model' : task});
this.el.append(task_li.render());
this.task_views.push(task_li);
},
});
});
Template for the task:
<script type='text/template' id='task-template'>
<li class="task">
<input type='checkbox' title='mark complete' class='task-check' />
<span class='task-name'><%= name %></span>
</li>
</script>
I can't seem to figure out why the toggleComplete event will not fire for the tasks. how can I fix this?
The problem here is that the backbone events only set to the element of the view (this.el) when you create a new view. But in your case the element isn't used. So you have the tagName:li attribute in your view, which let backbone create a new li element, but you doesn't use it. All you return is a new list element created from your template but not the element backbone is creating, which you can access by this.el
So you have to add your events manually to your element created by your template using jQuery or add your template as innerHtml to your element:
(this.el.html($(_.template(this.template, model_data)))
Try changing the lines where you set your listeners using .bind() to use .live(). The important difference is .live() should be used when you want to bind listeners to elements that will be created after page load.
The newest version of jQuery does away with this bit of ugliness and simplifies the methods used to set event listeners.
Your event is binding to a class of .task-complete but the class on your checkbox is .task-check
Try modifying your render function to call delegateEvents() like so:
render: function(){
model_data = this.model.toJSON();
this.el = $(_.template(this.template, model_data));
this.delegateEvents();
return this.el;
},
You'd really be better off changing your template to not include the li and then return this.el instead of replacing it, but if you want the events to work you need to have this.el be the root element one way or another; delegateEvents() re-attaches the event stuff, so when you change this.el that should fix the issue.
#Andreas Köberle answers it correctly. You need to assign something to this.elto make events work.
I changed your template and your TaskView#render() function.
This JSFiddle has the changes applied.
New render function:
render: function(){
var model_data = this.model.toJSON();
var rendered_data = _.template(this.template, model_data);
$(this.el).html(rendered_data);
return this;
}
It is recommended that the render() returns this.
One line in your TaskListView#addTask function changes from this.el.append(task_li.render()); to this.el.append(task_li.render().el);.
Template change
Since we are using this.el in the render() function, we have to remove the <li> tag from the template.
<script type='text/template' id='task-template'>
<input type='checkbox' title='mark complete' class='task-complete' />
<span class='task-name'><%= name %></span>
</script>

Backbone.js View can't unbind events properly

I have some Backbone.js code that bind a click event to a button,
and I want to unbind it after clicked, the code sample as below:
var AppView = Backbone.View.extend({
el:$("#app-view"),
initialize:function(){
_.bindAll(this,"cancel");
},
events:{
"click .button":"cancel"
},
cancel:function(){
console.log("do something...");
this.$(".button").unbind("click");
}
});
var view = new AppView();
However the unbind is not working, I tried several different way and end up binding event in initialize function with jQuery but not in Backbone.events model.
Anyone know why the unbind is not working?
The reason it doesn't work is that Backbonejs doesn't bind the event on the DOM Element .button itself. It delegates the event like this:
$(this.el).delegate('.button', 'click', yourCallback);
(docs: http://api.jquery.com/delegate)
You have to undelegate the event like this:
$(this.el).undelegate('.button', 'click');
(docs: http://api.jquery.com/undelegate)
So your code should look like:
var AppView = Backbone.View.extend({
el:$("#app-view"),
initialize:function(){
_.bindAll(this,"cancel");
},
events:{
"click .button":"cancel"
},
cancel:function(){
console.log("do something...");
$(this.el).undelegate('.button', 'click');
}
});
var view = new AppView();
Another (maybe better) way to solve this is to create a state attribute like this.isCancelable now everytime the cancel function is called you check if this.isCancelable is set to true, if yes you proceed your action and set this.isCancelable to false.
Another button could reactivate the cancel button by setting this.isCancelable to true without binding/unbinding the click event.
You could solve this another way
var AppView = Backbone.View.extend({
el:$("#app-view"),
initialize:function(){
_.bindAll(this,"cancel");
},
events:{
"click .button":"do"
},
do:_.once(function(){
console.log("do something...");
})
});
var view = new AppView();
underscore.js once function ensures that the wrapped function
can only be called once.
There is an even easier way, assuming you want to undelegate all events:
this.undelegateEvents();
I like bradgonesurfing answer. However I came across a problem using the _.once approach when multiple instances of the View are created. Namely that _.once would restrict the function to be called only once for all objects of that type i.e. the restriction was at the class level rather than instance level.
I handled the problem this way:
App.Views.MyListItem = Backbone.View.extend({
events: {
'click a.delete' : 'onDelete'
},
initialize: function() {
_.bindAll(this);
this.deleteMe = _.once(this.triggerDelete);
},
// can only be called once
triggerDelete: function() {
console.log("triggerDelete");
// do stuff
},
onDelete:(function (e) {
e.preventDefault();
this.deleteMe();
})
});
Hopefully this will help someone
you can simply use object.off, the code below is work for me
initialize:function () {
_.bindAll(this, 'render', 'mouseover', 'mouseout', 'delete', 'dropout' , 'unbind_mouseover', 'bind_mouseover');
.......
},
events: {
'mouseover': 'mouseover',
'unbind_mouseover': 'unbind_mouseover',
'bind_mouseover': 'bind_mouseover',
.....
},
mouseover: function(){
$(this.el).addClass('hover');
this.$('.popout').show();
},
unbind_mouseover: function(){
console.log('unbind_mouseover');
$(this.el).off('mouseover');
},
bind_mouseover: function(){
........
},

Categories