Delegating events to a parent view in Backbone
Solution 1
In your parent view (extending also from Backbone.Events
), I would bind onEvent
to the DOM event. On trigger, it would fire a backbone event including some "id" attribute that your child views know (presumably some row id?).
var TuneBook = Backbone.View.extend(_.extend({}, Backbone.Events, {
events: {
"click .tune .button": "clickHandler"
},
clickHandler: function (ev) {
this.trigger('buttonClick:' + ev.some_id_attr, ev);
},
}));
Child views would then naturally subscribe to the parent views event that concerns them. Below I do it in initialize
passing the parent view as well as that special id attribute you used before in options
.
var ClosedTune = Backbone.View.extend({
initialize: function (options) {
options.parent.on('buttonClick:' + options.id, this.handler, this);
},
handler: function (ev) {
...
},
});
You can of course also set up similar subscribers on Tune
or OpenTune
.
Solution 2
Here are a couple of possibilities.
1. Centralized: store ClosedTune
objects in the TuneBook
instance
Store a reference to each
ClosedTune
intune_book.tunes
. How you populatetune_book.tunes
is up to you; since you mentioned an adder method onTuneBook
, that's what I've illustrated below.In the
TuneBook
event handler, retrieve theClosedTune
fromtune_book.tunes
by using something like theid
attribute of the event target as the key. Then call theTune
orClosedTune
handler.
var Tune = Backbone.View.extend({
className: "tune",
click_handler: function (event) {
event.preventDefault();
console.log(this.id + " clicked");
},
render: function () {
this.$el.html(
'<a href="" class="button">' + this.id + '</a>'
);
return this;
}
});
var ClosedTune = Tune.extend({});
var OpenTune = Tune.extend({
events: {
"click .button" : 'click_handler'
}
});
var TuneBook = Backbone.View.extend({
events: {
"click .tune .button" : 'click_handler'
},
click_handler: function (event) {
var tune = this.options.tunes[
$(event.target).closest(".tune").attr('id')
];
tune.click_handler( event );
},
add_tune: function (tune) {
this.options.tunes[tune.id] = tune;
this.$el.append(tune.render().el);
},
render: function () {
$("body").append(this.el);
return this;
}
});
var tune_book = new TuneBook({
tunes: {}
});
[1, 2, 3].forEach(function (number) {
tune_book.add_tune(new ClosedTune({
id: "closed-tune-" + number
}));
});
tune_book.render();
var open_tune = new OpenTune({
id: "open-tune-1"
});
$("body").append(open_tune.render().el);
2. Decentralized: associate the view object with the DOM object using jQuery.data()
When you create a
ClosedTune
, store a reference to it, e.g.this.$el.data('view_object', this)
.In the event listener, retrieve the
ClosedTune
, e.g.$(event.target).data('view_object')
.
You can use the same exact handler for ClosedTune
(in TuneBook
) and OpenTune
, if you want.
var Tune = Backbone.View.extend({
className: "tune",
initialize: function (options) {
this.$el.data('view_object', this);
},
click_handler: function (event) {
event.preventDefault();
var tune =
$(event.target).closest(".tune").data('view_object');
console.log(tune.id + " clicked");
},
render: function () {
this.$el.html(
'<a href="" class="button">' + this.id + '</a>'
);
return this;
}
});
var ClosedTune = Tune.extend({
initialize: function (options) {
this.constructor.__super__.initialize.call(this, options);
}
});
var OpenTune = Tune.extend({
events: {
"click .button" : 'click_handler'
}
});
var TuneBook = Backbone.View.extend({
events: {
"click .tune .button": Tune.prototype.click_handler
},
add_tune: function (tune) {
this.$el.append(tune.render().el);
},
render: function () {
$("body").append(this.el);
return this;
}
});
var tune_book = new TuneBook({
tunes: {}
});
[1, 2, 3].forEach(function (number) {
tune_book.add_tune(new ClosedTune({
id: "closed-tune-" + number
}));
});
tune_book.render();
var open_tune = new OpenTune({
id: "open-tune-1"
});
$("body").append(open_tune.render().el);
Response to comment
I considered option 1 but decided against it as I already have a collection of tune models in the tunebook and didn't want another object I'd need to keep in sync
I guess it depends what kind of housekeeping / syncing you feel the need to do, and why.
(e.g. in TuneModel.remove() I would need to remove the view from tunebook's list of views... would probably need events to do this, so an event only solution starts to look more attractive).
Why do you feel that you "need to remove the view from tunebook's list of views"? (I'm not suggesting you shouldn't, just asking why you want to.) Since you do, how do you think @ggozad's approach differs in that respect?
Both techniques store ClosedTune
objects in the TuneBook
instance. In @ggozad's technique it's just hidden behind an abstraction that perhaps makes it less obvious to you.
In my example they're stored in a plain JS object (tune_book.tunes
). In @ggozad's they're stored in the _callbacks
structure used by Backbone.Events
.
Adding a ClosedTune
:
1.
this.options.tunes[tune.id] = tune;
2.
this.on('buttonClick:' + tune.id, tune.handler, tune);
If you want to get rid of a ClosedTune
(say you remove it from the document with tune.remove()
and you want the view object gone completely), using @ggozad's approach will leave an orphaned reference to the ClosedTune
in tune_book._callbacks
unless you perform the same kind of housekeeping that would make sense with the approach I suggested:
1.
delete this.options.tunes[tune.id];
tune.remove();
2.
this.off("buttonClick:" + tune.id);
tune.remove();
The first line of each example is optional -- depending if you want to clean up the ClosedTune
objects or not.
Option 2 is more or less what I'm doing right now, but (for other reasons) I also store the model as a data attribute on view.$el, and I can't help feeling that there's got to be a better way than storing references all over the place.
Well, it ultimately comes down to your preference for how to structure things. If you prefer storing the view objects in a more centralized fashion, you can store them in the TuneBook
instance instead of using jQuery.data
. See #1: Centralized.
One way or another you're storing references to the ClosedTune
objects: using jQuery.data
, or in a plain object in the TuneBook
, or in _callbacks
in the TuneBook
.
If you like @ggozad's approach for reasons that you understand, go for it, but it's not magic. As it's presented here I'm not sure what advantage is supposed to be provided by the extra level of abstraction compared to the more straightforward version I present in #1. If there is some advantage, feel free to fill me in.
Related videos on Youtube
wheresrhys
Mostly a front-end developer, working with html, css and recently nursing an addiction to jQuery/javascript too. Also delving a bit into php development using the zend framework. I play in an irish folk band, go birdwatching, and enjoy listening to classic reggae.
Updated on June 04, 2022Comments
-
wheresrhys almost 2 years
My view,
TuneBook
, has several child views of typeClosedTune
. I also have separate full page views for each tune,OpenTune
. The same events are bound withinClosedTune
andOpenTune
, so I've designed my app so that they both inherit from a shared 'abstract' viewTune
.To make my app more scaleable I would like the events for each
ClosedTune
to be delegated toTuneBook
, but for maintainability I would like the same handlers (the ones stored inTune
) to be used byTuneBook
(although they'd obviously need to be wrapped in some function).The problem I have is, within
TuneBook
, finding the correctClosedTune
to call the handler on. What's a good way to architect this, or are there other good solutions for delegating events to a parent view?Note - not a duplicate of Backbone View: Inherit and extend events from parent (which is about children inheriting from a parent class, whereas I'm asking about children which are child nodes of the parent in the DOM)
-
Dave Cadwallader almost 12 yearsThis approach uses the parent view as an "event aggregator" which is a good pattern to follow. Alternatively, you can also create a separate object that's your event aggregator and pass that to your child views. Then your children don't need to traverse the DOM to find the right object on which to listen for events. For more info on event aggregators, see this article: lostechies.com/derickbailey/2012/04/03/…
-
wheresrhys almost 12 yearsI considered option 1 but decided against it as I already have a collection of tune models in the tunebook and didn't want another object I'd need to keep in sync (e.g. in TuneModel.remove() I would need to remove the view from tunebook's list of views... would probably need events to do this, so an event only solution starts to look more attractive). Option 2 is more or less what I'm doing right now, but (for other reasons) I also store the model as a data attribute on view.$el, and I can't help feeling that there's got to be a better way than storing references all over the place.
-
wheresrhys almost 12 yearsThis looks perfect. Will try it out tonight
-
wheresrhys almost 12 yearsI implemented this almost exactly, with the sole alteration that I attached
ClosedTune
's event listeners within the addOne() method ofTuneBook
to a) avoid having to pass references to parent b) enable use ofClosedTune
within other contexts -
JMM almost 12 years@wheresrhys I don't know if it was a factor in your selection, but I've corrected the stupid errors that were in my earlier code examples. I updated my answer with a response to your comment (see "Response to comment" section).
-
wheresrhys almost 12 yearsCheers for the thorough answer. It's true that it's easy to see events as a magic bullet that loosely couples objects without any downsides, and you're right that I should give more thought to declutternig the callbacks when removing objects. Nevertheless events is still my preferred option. Having the duplicate references to the views abstracted away feels cleaner and less tempting for me or other developers to misuse. As for using $.data I'm always wary of using anything that queries the DOM unless absolutely necessary as DOM interactions are relatively slow.
-
JMM almost 12 years@wheresrhys "less tempting for me or other developers to misuse" -- not sure what you mean here. "As for using $.data ..." -- I think you misunderstand what
jQuery.data()
does. You didn't specify what you're actually using whereev.some_id_attr
appears in yourclickHandler
, so I assume you're using a property of a DOM object you have access to at that point, e.g.ev.target.id
.jQuery.data()
isn't a DOM operation, it just uses a property of a DOM object (e.g.ev.target
) as a key to read data from a JS object. Callingthis.trigger()
does the same -- reads data from a JS object. -
vini over 8 yearsCan we pass data on click?