Backbone js dynamic events with variables - javascript

Lets say I got this view:
var HomeView = Backbone.View.extend({
el: '#application',
initialize: function() {
this.template = template; // Comes from requireJS (not relevant)
this.$elements = {};
},
render: function() {
this.$el.html(this.template);
this.$elements = {
signIn: {
email: $('#sign-in-email'),
password: $('#sign-in-password')
}
};
// Demonstration.
this.$elements.signIn.email.myPluginInit();
this.$elements.signIn.password.myPluginInit();
//
// NOTE: How to handle the events?
//
}
});
I have the this.$elements object, which will contain all the objects of my DOM there, how can I put events on them because with this solution they are variable. This is what I used to do (see backbone.org).
var HomeView = Backbone.View.extend({
events: {
'click #sign-in-email': 'clickedSignInEmail',
'focus #sign-in-password': 'focusSignInPassword'
}
});

Using delegateEvents provides a number of advantages over manually
using jQuery to bind events to child elements during render. All
attached callbacks are bound to the view before being handed off to
jQuery, so when the callbacks are invoked, this continues to refer to
the view object. When delegateEvents is run again, perhaps with a
different events hash, all callbacks are removed and delegated afresh
— useful for views which need to behave differently when in different
modes.
Example code:
initialiaze: function () {
// …
this.events = this.events || {};
// dynamically build event key
var eventKey = 'click ' + '#sign-in-email';
this.events[eventKey] = 'clickedSignInEmail';
this.delegateEvents();
// …
}

How about using the normal jQuery event handling syntax?
this.$elements.signIn.email.click(this.clickedSignInEmail);
this.$elements.signIn.password.focus(this.focusSignInPassword);
You can also use the Backbone.View.delegateEvents method, but that requires you to construct the events hash from your selectors.

Related

Backbone collection add event not firing

I am writing a slightly modified version of the Backbone Todos app commonly found online, using books instead. My Backbone version is 1.3.3. Like the example, for now I am just trying to display a book title added by the user in an input list. My code (partial) is given below:
app.BookList = Backbone.Collection.extend({
model: app.Book,
// For temp use, to be replaced with URL
localStorage: new Backbone.LocalStorage('BookList')
});
app.bookList = new app.BookList();
app.AppView = Backbone.View.extend({
el: '#app',
initialize: () => {
this.input = this.$('#book-name');
app.bookList.on('change', this.addOneBook, this);
app.bookList.fetch();
},
events: {
'keypress #book-name': 'createNewBookModel'
},
addOneBook: (book) => {
console.log('BOOK: ', book);
const b = new app.BookView({ model: book });
this.$('#bookList').append(b.render().el());
},
createNewBookModel: (e) => {
if (e.which === 13) {
e.preventDefault();
app.bookList.create({ title: this.input.val().trim() });
this.input.val('');
}
}
});
app.appView = new app.AppView();
My problem is the addOneBook event is not firing, after the createNewBookModel event has fired and completed. My understanding (reading this - http://backbonejs.org/#Collection-create), is that creating a model will trigger an add event on the collection. I tried using bind instead of on, but that didn't work. I also tried this.listenTo in place of on, but that threw an error, saying, this.listenTo is not defined. Please help. Thanks!
I've never made extensive use of ES6 arrow functions, but I think they may be causing you problems here. As I understand it, arrow functions do not bind the this object. While your functions are executing, your this will evaluate to undefined or maybe even the window parent object, but I think you want it to evaluate to the backbone object.
Try replacing:
initialize: () => {
// ...
},
with:
initialize: function() {
// ...
},
and similar for your other methods. I think that will clear up a lot of problems.

How to access an object/var from view1 but instantiated in view2 in Backbone js?

I am trying to access an object(this.historyComboBox) declared in a view(StatusView)'s render function and trying to access the same object from another view(HistoryView)'s extendedEvents function.
I have tried to use this.historyComboBox to access it but unable to hold any reference. Got really puzzled. If anyone has got some different idea I am ready to try it out!
Note: StatusView gets initialized prior to HistoryView.
Following is the code snippet.
StatusView = Backbone.View.extend({
init: function() {
//some code
},
render: function() {
this.historyComoBox = new sys.ComboBox();
}
}
HistoryView = Backbone.View.extend({
template: _.template(historyTemplate),
init: function() {
//some code
},
extendedEvents: {
'click #refreshButton': function() {
//want to access historyComoBox; not accessible with 'this.historyComoBox'
}
}
}
To get a property of a StatusView instance, you need a reference to that instance. So, if you have something like this:
var statusView = new StatusView();
Then from within the methods of HistoryView, you can do this:
statusView.historyComboBox;
However, while you can do it this way, I wouldn't access the StatusView instance directly like this. A better way would be to pass it to the HistoryView instance as a parameter, which you would receive in the initialize method. This keeps the views loosely coupled,
var HistoryView = Backbone.View.extend({
initialize: function (options) {
this.statusView = options.statusView;
},
events: {
'click #refreshButton': function () {
// use this.statusView;
}
}
});
(I notice you're using the names init and extendedEvents. You don't mention that you're using a third-party library with Backbone or something else that might change those, so I'll just mention that Backbone expects these to be initialize and events respectively.)

Remove and unbind subviews in Backbone

I am using Backbone 1.1.2 and I found that some weird behaviour of my app was probably due to zombieviews. I read the "Run! Zombies!" article from Derick Bailey but found out later that this was written for an older version of Backbone (0.9 if I am correct).
Then I found that for the newer Backbone version it was enough to do a .remove() on views to kill them properly (because the events bound with ListenTo would automatically be removed by a call to StopListening).
In my app I have a global view that at some point creates two subviews. When clicking a reset button (within the global view) these views should be rerendered (but probably first removed/unbound to prevent zombieviews).
So what I did was appending the subviews to a list that was accessible by the global view. In the initialize function:
this._views = []; // empty list
and when rendering the subviews I added them to the list
v = new app.gameView();
this._views.push(v);
Just before rerendering the subviews I call a function cleanUp that loops through the list of subviews and does a .remove() followed by an .unbind() for each subview:
_.each(this._views, function(view){
this.remove();
this.unbind();
});
this._views = []; // empty the list for next use
My question is twofold:
Is calling .remove and .unbind enough to prevent zombieviews?
Is adding the subviews to a list within the global view the proper way of doing this?
Any thoughts are appreciated!
In my experience, simply calling remove() and unbind()/off() is enough to prevent "zombies" hanging around. The only thing I would add is that if the parent view (the one that contains the subviews inside of this._views) is being referenced from another part of your application, then you have to make sure that you remove those references by simply assigning those variables to null.
It is totally fine to have a this._views array inside of the parent to save its subviews in. However, as your application grows you might want to create some sort of Subview Manager and a Core View that all other views inherit from.
Here is what I've done in the past; I hope it helps:
CoreView:
// Probably all views should inherit from CoreView.
define([
'jquery',
'backbone',
'subviews'
], function($, Backbone, Subviews) {
var CoreView = Backbone.View.extend({
$main: $('#main'),
// Create an empty `subviews` property on all views.
constructor: function() {
this.subviews = new Subviews(this);
// Since we're overriding `constructor` here,
// we need to delegate to Backbone
Backbone.View.prototype.constructor.apply(this, arguments);
},
// Views should be attached to the DOM only with the `attach` method to have the right events thrown.
// Attach the view's element only if it's not already in the DOM.
attach: function() {
if (!this.isAttached()) {
this.$main.append(this.el);
this.trigger('dom:attach');
}
return this;
},
isAttached: function() {
return $.contains(document.body, this.el);
},
// Remove each view's subviews and clean up various properties before
// calling Backbone's remove() method.
remove: function() {
if (this.subviews.size()) {
this.subviews.removeAll();
}
// Remove the DOM element (jQuery makes sure to clean up DOM element's data)
Backbone.View.prototype.remove.apply(this, arguments);
// Fire a helpful `dom:detach` event when the view is removed from the DOM.
this.trigger('dom:detach');
this.off();
return this;
}
});
return CoreView;
});
Subview Manager (not complete):
// Simple Subview Manager
define([
'jquery',
'backbone'
], function($, Backbone) {
function Subviews(view) {
this.self = view; // this view
this._entries = {}; // its subviews
}
Subviews.prototype = {
constructor: Subviews,
add: function(name, viewInstance) { ... },
has: function(name) { return !!this._entries[name]; },
get: function(name) { return this._entries[name] && this._entries[name]; },
// By default the subview manager tries to replace an element with
// a `data-subview` attribute with the actual subview element.
attach: function(name) {
// In the parent view's template you would have: `<div data-subview="child1"></div>`
var $subViewOutput = this.self.$('[data-subview="'+name+'"]');
if (this._entries[name] && $subViewOutput.length) {
$subViewOutput.replaceWith(this._entries[name].render().el);
}
},
// When removing a subview we also have to remove it from
// this view's `subviews` property.
remove: function(name) {
if (this._entries && this._entries[name]) {
this._entries[name].remove();
// Cleanup
this._entries[name] = null;
this._entries = _.omit(this._entries, name);
}
},
removeAll: function() {
if (this.size()) {
_.each(this._entries, function(view) {
view.remove(); // it will call remove() in CoreView first
});
}
this._entries = {};
this.self = null;
},
size: function() {
return _.size(this._entries);
}
};
return Subviews;
});
Ordinary View:
define([
'jquery',
'backbone',
'templates',
'views/coreView',
'views/childView'
],
function($, Backbone, templates, CoreView, ChildView) {
var Widget = CoreView.extend({
tagName: 'section',
id: 'widget123',
template: templates.widget123,
initialize: function() {
this.subviews.add('child1', new ChildView());
this.on('dom:attach', function() {
// When the parent is inserted into the DOM also insert its child1
this.subviews.attach('child1');
});
},
render: function() {
this.$el.html(this.template());
return this;
}
});
var instance = new Widget();
instance.render().attach(); // attach() comes from CoreView
});

Backbone Not Firing Events

So here is an example of my app in jsfiddle: http://jsfiddle.net/GWXpn/1/
The problem is click event isn't being fired at all. I am not getting any JS errors in the console.
First, I wanted to display an unordered list with couple if items, each item should be clickable. This is what I did:
var FooModel = Backbone.Model.extend({});
var ListView = Backbone.View.extend({
tagName: 'ul', // name of (orphan) root tag in this.el
initialize: function() {
_.bindAll(this, 'render'); // every function that uses 'this' as the current object should be in here
},
render: function() {
for (var i = 0; i < 5; i++) {
var view = new SingleView({
model: new FooModel()
});
$(this.el).append(view.render().el);
}
return this; // for chainable calls, like .render().el
}
});
var SingleView = Backbone.View.extend({
tagName: 'li', // name of (orphan) root tag in this.el
initialize: function() {
_.bindAll(this, 'render', 'click'); // every function that uses 'this' as the current object should be in here
},
events: {
"click": "click"
},
click: function(ev) {
console.log("aaa");
alert(333);
},
render: function() {
$(this.el).append("aaa");
return this; // for chainable calls, like .render().el
}
});
I wanted to divide my app in to multiple modules (header, body, footer) so I created an abstract model and extended my modules from it:
var AbstractModule = Backbone.Model.extend({
getContent: function () {
return "TODO";
},
render: function () {
return $('<div></div>').append(this.getContent());
}
});
var HeaderModule = AbstractModule.extend({
id: "header-module",
});
var BodyModule = AbstractModule.extend({
id: "body-module",
getContent: function () {
var listView = new ListView();
return $("<div/>").append($(listView.render().el).clone()).html();
}
});
var ModuleCollection = Backbone.Collection.extend({
model: AbstractModule,
});
Then I just created my main view and rendered all its subviews:
var AppView = Backbone.View.extend({
el: $('#hello'),
initialize: function (modules) {
this.moduleCollection = new ModuleCollection();
for (var i = 0; i < modules.length; i++) {
this.moduleCollection.add(new modules[i]);
}
},
render: function () {
var self = this;
_(this.moduleCollection.models).each(function (module) { // in case collection is not empty
$(self.el).append(module.render());
}, this);
}
});
var appView = new AppView([HeaderModule, BodyModule]);
appView.render();
Any ideas why?
You have two bugs in one line:
return $("<div/>").append($(listView.render().el).clone()).html();
First of all, clone doesn't copy the events unless you explicitly ask for them:
Normally, any event handlers bound to the original element are not copied to the clone. The optional withDataAndEvents parameter allows us to change this behavior, and to instead make copies of all of the event handlers as well, bound to the new copy of the element.
[...]
As of jQuery 1.5, withDataAndEvents can be optionally enhanced with deepWithDataAndEvents to copy the events and data for all children of the cloned element.
You're cloning the <ul> here so you'll want to set both of those flags to true.
Also, html returns a string and strings don't have events so you're doubling down on your event killing.
I don't understand why you're cloning anything at all, you should just return the el and be done with it:
return listView.render().el;
If you insist on cloning, then you'd want something like this:
return $(listView.render().el).clone(true, true);
but that's just pointless busy work.
BTW, 'title' and 'Title' are different model attributes so you'll want to say:
console.log(this.model.get("title") + " clicked");
instead of
console.log(this.model.get("Title") + " clicked");
Also, Backbone collections have a lot of Underscore methods mixed in so don't mess with a collection's models directly, where you're currently saying:
_(this.moduleCollection.models).each(...)
just say:
this.moduleCollection.each(...)
And as Loamhoof mentions, 0.3.3 is ancient history, please upgrade to newer versions of Backbone, Underscore, and jQuery. You should also read the change logs so that you can use newer features (such as this.$el instead of $(this.el), fewer _.bindAll calls, listenTo, ...).
Partially Corrected Demo (including updated libraries): http://jsfiddle.net/ambiguous/e4Pba/
I also ripped out the alert call, that's a hateful debugging technique that can cause a huge mess if you get into accidental infinite loops and such, console.log is much friendlier.

Delegating events to a parent view in Backbone

My view, TuneBook, has several child views of type ClosedTune. I also have separate full page views for each tune, OpenTune. The same events are bound within ClosedTune and OpenTune, so I've designed my app so that they both inherit from a shared 'abstract' view Tune.
To make my app more scaleable I would like the events for each ClosedTune to be delegated to TuneBook, but for maintainability I would like the same handlers (the ones stored in Tune) to be used by TuneBook (although they'd obviously need to be wrapped in some function).
The problem I have is, within TuneBook, finding the correct ClosedTune 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)
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.
Here are a couple of possibilities.
1. Centralized: store ClosedTune objects in the TuneBook instance
Store a reference to each ClosedTune in tune_book.tunes. How you populate tune_book.tunes is up to you; since you mentioned an adder method on TuneBook, that's what I've illustrated below.
In the TuneBook event handler, retrieve the ClosedTune from tune_book.tunes by using something like the id attribute of the event target as the key. Then call the Tune or ClosedTune handler.
http://jsfiddle.net/p5QMT/1/
var Tune = Backbone.View.extend({
className: "tune",
click_handler: function (event) {
event.preventDefault();
console.log(this.id + " clicked");
},
render: function () {
this.$el.html(
'' + this.id + ''
);
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.
http://jsfiddle.net/jQZNF/1/
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(
'' + this.id + ''
);
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.
Great solution I have taken from this article (#dave-cadwallader comment).
Extend an general backbone events object and store it in a reference vent:
var vent = _.extend({}, Backbone.Events);
Pass it to parent view:
var parentView = new ParentView({vent: vent});
The child view will trigger an event:
ChildView = Backbone.View.extend({
initialize: function(options){
this.vent = options.vent;
},
myHandler: function(){
this.vent.trigger("myEvent", this.model);
}
});
And the parent view is listening to the child event:
ParentView = Backbone.View.extend({
initialize: function(options){
this.vent = options.vent;
this.vent.on("myEvent", this.onMyEvent);
let childView = new ChildView({vent: this.vent});
},
onMyEvent: function(){
console.log("Child event has been ");
}
});
Disclaimer - pay attention that the vent object has to be injected to every view so you will find in this article better design patterns to make use of.

Categories

Resources