Nested views : binding events with proper ui - javascript

I have app where one type of view is nested in same type. Something like this http://take.ms/ylAeq
How can I catch event on view button and fired event on proper view?
I have situation when I have ~10 fired events becuase backbone binds event to all inner buttons, not only button of proper view.
I have one idea - I make in template id="node_body_<%= id %>" so each button id based on it's view's model id; but how can I pass it to events object? events :{ "click #node_body_" + id : 'method' } doesn't work.

The issue is event bubbling. The events on your inner view's bubble upto the el of it's parent view's, which is listening for events on same selector, in turn triggering it's handler.
This can be fixed by stopping the propagation of event in the respective view's handler:
Backbone.View.extend({
events: {
'click .thing': 'do_thing'
},
do_thing: function(event) {
event.stopPropagation(); // prevents event reaching parent view
}
});

Most properties of Backbone views can be functions instead of literal values. For example, these two things have the same result:
Backbone.View.extend({
events: {
'click .thing': 'do_thing'
},
do_thing: function() { ... }
});
and
Backbone.View.extend({
events: function() {
return {
'click .thing': 'do_thing'
};
},
do_thing: function() { ... }
});
So if your events depend on properties of the view, then you'd want to use a function instead of an object literal:
events: function() {
var events = { };
var event_spec = 'click #nody_body' + this.id;
events[event_spec] = 'method';
return events;
}
That assumes that id is the property of the view that you're using in your template.

Related

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

Disable specific event in a Backbone.js view

I am working on a nested backbone view, in which if you click on, it will create a new instance of the same view. I want to disable only a specific event, not all of them; in this case, the click. I tried using undelegateEvents(), but this will disable all the functions. Any ideas on how can this be done?
Here is a piece of the code I am working on:
var View = Backbone.View.extend({
events: {
"mousedown": "start",
"mouseup": "over"
},
start: function() {
var model = this.model;
var v = new View({
model: model,
});
v.undelegateEvents(); //I just want to disable mousedown
v.render();
},
over: function() {
/*
some code here
*/
},
render: function() {
/*
some code here
*/
}
});
The idea is to ban clicking in the second instantiated view while keeping the other events. The first one will have all of its events.
Thanks
You can specify the events you want to use when you call delegateEvents:
delegateEvents delegateEvents([events])
Uses jQuery's delegate function to provide declarative callbacks for DOM events within a view. If an events hash is not passed directly, uses this.events as the source.
So you could do something like this:
var v = new View({
model: model,
});
v.undelegateEvents();
var e = _.clone(v.events);
delete e.mousedown;
v.delegateEvents(e);
v.render();
You might want to push that logic into a method on View though:
detach_mousedown: function() {
this.undelegateEvents();
this.events = _.clone(this.events);
delete this.events.mousedown;
this.delegateEvents();
}
//...
v.detach_mousedown();
You need the this.events = _.clone(this.events) trickery to avoid accidentally altering the "class's" events (i.e. this.constructor.prototype.events) when you only want to change it for just one object. You could also have a flag for the View constructor that would do similar things inside its initialize:
initialize: function() {
if(this.options.no_mousedown)
this.detach_mousedown()
//...
}
Another option would be to have a base view without the mousedown handler and then extend that to a view that does have the mousedown handler:
var B = Backbone.View.extend({
events: {
"mouseup": "over"
},
//...
});
var V = B.extend({
events: {
"mousedown": "start",
"mouseup": "over"
},
start: function() { /* ... */ }
//...
});
You'd have to duplicate the B.events inside V or mess around with a manual extend on the events as _.extend won't merge the properties, it just replaces things wholesale.
Here is a simple example that shows how to delegate or undelegate events within a Backbone view
Backbone.View.extend({
el: $("#some_element"),
// delete or attach these as necessary
events: {
mousedown: "mouse_down",
mousemove: "mouse_move",
mouseup: "mouse_up",
},
// see call below
detach_event: function(e_name) {
delete this.events[e_name]
this.delegateEvents()
},
initialize: function() {
this.detach_event("mousemove")
},
mouse_down: function(e) {
this.events.mousemove = "mouse_move"
this.delegateEvents()
},
mouse_move: function(e) {},
mouse_up: function(e) {}
})

How can I append an attribute to a JavaScript event?

At row level I catch the event and try to add an extra parameter
onRowClick: function(e){
console.log("Event in row");
e.model = "test";
console.log(e.model) // prints 'test'
}
In main view I catch the same event again
onRowClick: function(e){
console.log("Event in main view");
console.log(e.model) //prints undefined
}
Console:
>Event in row
>test
>Event in main view
>undefined
How can I append an attribute to the event?
The answer is that you don't catch the same event, but rather two (initially) identical events. Changing the first does not change the latter.
If you want to pass data between those events, you would need to store that data elsewhere (e.g. a closure, or if you don't care about the scope save it in the window object).
There are 2 ways that I know of to pass data to a jQuery event. One with with e.data, you can add any properties to e.data like this.
http://www.barneyb.com/barneyblog/2009/04/10/jquery-bind-data/
the other way is to use closures such as:
function myFunc() {
var model = 'test';
var x = {
onRowClick: function(e){
console.log("Event in row");
console.log(model) // prints 'test'
}
}
}
instead of catching the rowClick event in the main view, i suggest you catch it in the row view, and pass it through the backbone event system...
your parentview can bind to it's rows to catch a click.
there are two ways to do this,
trigger a custom event on your row's model, and let the parent bind to every model in the collection, though that seems like a hack and a performance hit.
i suggest doing it with an event aggregator:
var App = {
events: _.extend({}, Backbone.Events);
};
var myGeneralView = Backbone.Views.extend({
initialize: function() {
_.bindAll(this, "catchMyCustomEvent";
/*
and here you bind to that event on the event aggregator and
tell it to execute your custom made function when it is triggered.
You can name it any way you want, you can namespace
custom events with a prefix and a ':'.
*/
App.events.bind('rowView:rowClicked');
},
catchMyCustomEvent: function (model, e) {
alert("this is the model that was clicked: " + model.get("myproperty"));
}
// other methods you will probably have here...
});
var myRowView = Backbone.Views.extend({
tagName: "li",
className: "document-row",
events: {
"click" : "myRowClicked"
},
initialize: function() {
_.bindAll(this, "myRowClicked");
},
myRowClicked: function (e) {
/*
You pass your model and your event to the event aggregator
*/
App.events.trigger('rowView:rowClicked', this.model, e);
}
});

Trouble getting Backbone.js view event to fire

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.

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