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);
}
});
Related
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.
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.
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
What is a good way for a Backbone model to fire a custom event when a specific attribute has been changed?
So far this is the best I've got:
var model = Backbone.Model.extend({
initialize: function(){
// Bind the mode's "change" event to a custom function on itself called "customChanged"
this.on('change', this.customChanged);
},
// Custom function that fires when the "change" event fires
customChanged: function(){
// Fire this custom event if the specific attribute has been changed
if( this.hasChanged("a_specific_attribute") ){
this.trigger("change_the_specific_attribute");
}
}
})
Thanks!
You can already bind to attribute-specific change events:
var model = Backbone.Model.extend({
initialize: function () {
this.on("change:foo", this.onFooChanged);
},
onFooChanged: function () {
// "foo" property has changed.
}
});
Backbone already has an event "change:attribute" that gets fired for each attribute that has changed.
var bill = new Backbone.Model({
name: "Bill Smith"
});
bill.on("change:name", function(model, name) {
alert("Changed name to " + name);
});
bill.set({name : "Bill Jones"});
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) {}
})