Imagine the following simple model and view:
app.Messages = Backbone.Model.extend({
defaults: function() {
return {
message: "an empty message…",
messageActive: false
};
}
});
app.MessageView = Backbone.View.extend({
tagName: "li",
template: _.template($("#template").html()),
events: {
"click": "open"
},
initialize: function() {
this.listenTo(this.model, "change", this.render);
this.listenTo(this.model, "message:hide", this.hide);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
open: function() {},
hide: function() {
this.model.set("messageActive", false);
this.$el.removeClass("show-message");
}
});
The model is part of a collection and everything is held together by a parent view (simplified for this question):
app.AppView = Backbone.View.extend({
el: "#messages",
initialize: function() {
this.listenTo(app.messages, "message:show", this.foo);
},
foo: function(model) {
var open = app.messages.where({messageActive: true});
open.forEach(function(e, i) {
if (model != e) {
e.trigger("message:hide");
}
});
},
Only one message of the collection can be active at any given time. This means that as soon as a closed message is clicked, any open message will be closed. The question now concerns the functionality of app.MessageView.open()
. It could imho either use jQuery.siblings()
to hide the message of all its siblings and show its own:
open: function() {
this.$el
.addClass("show-message")
.siblings(".show-message")
.removeClass("show-message");
}
Personally I think this solution quite elegant, I only dislike that it kind of needs to know that it has siblings. My other implementation would use a custom event that gets intercepted by the parent view (app.AppView.foo()
) that then calls app.MessageView.close()
on every open message:
open: function() {
this.model.set("messageActive", true).trigger("message:show", this.model);
this.$el.addClass("show-message");
}
This feels a little more "backbone" to me, but seems a little over-engineered. ;-)
I could event think of a third possibilty where the parent view simply keeps track of the last opened model itself and triggers any close event that is intercepted by the models view (seems manageable but not very backbone-like to me ;-) ). I've already covered quite some blog posts and questions here on SO, but came to no real satisfactory conclusion. Maybe someone can shed some light this specific question.
EDIT: Completely missed the click event on the messageview so revamped my answer.
You could trigger a click event on the model whenever the view is clicked, then wait for the parentview to trigger show and hide events on the model.
app.MessageView = Backbone.View.extend({
tagName: "li",
template: _.template($("#template").html()),
events: {
"click": "isClicked"
},
initialize: function() {
this.listenTo(this.model, "change", this.render);
this.listenTo(this.model, "message:show", this.open);
this.listenTo(this.model, "message:hide", this.hide);
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
isClicked: function(){
this.model.trigger("message:clicked");
},
open: function() {
this.model.set("messageActive", true);
this.$el.addClass("show-message");
},
hide: function() {
this.model.set("messageActive", false);
this.$el.removeClass("show-message");
}
});
Your parentview is now responsible of sending the right events to the models whenever one of it's childviews is clicked.
app.AppView = Backbone.View.extend({
el: "#messages",
initialize: function() {
this.listenTo(app.messages, "message:clicked", this.foo);
},
foo: function(model) {
var open = app.messages.where({messageActive: true});
open.forEach(function(e, i) {
if (model != e) {
e.trigger("message:hide");
}
});
// Because we retrieved the currently active models before sending this event,
// there will be no racing condition:
model.trigger("message:show");
},
Perhaps not the shortest way to achieve this sorta thing. But it is completely event driven and customizable.