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.
Related
I am using a collection view to render my array of model views. I have added a method that removes a single model view from the existing collection view, and attempts to re-render it in a new el: element.
I use collection.get(this.model) to save the model to a variable, I add that variable to my new collection which is the model of a new collection view associated with a new DOM element, and I re-use the same collection view render method. When I console.log() the new collection, I see the model that I picked from the old collection, but it's not rendering on the page.
<script>
(function($){
//---------SINGLE ENTRY MODEL----------
var Entry = Backbone.Model.extend({
defaults: function(){
return{
word: '',
definition: ''
}
}
});
//------------ENTRY MODEL COLLECTION------------
var EntryList = Backbone.Collection.extend({
model: Entry
});
//-----INSTANCIATE COLLECTION----
var dictionary = new EntryList();
var saved = new EntryList();
//-----SINGLE ENTRY VIEW------
var EntryView = Backbone.View.extend({
model: new Entry(),
tagName:'div',
events:{
'click .edit': 'edit',
'click .delete': 'delete',
'keypress .definition': 'updateOnEnter',
'click .save': 'save'
},
delete: function(ev){
ev.preventDefault;
dictionary.remove(this.model);
},
edit: function(ev){
ev.preventDefault;
this.$('.definition').attr('contenteditable', true).focus();
},
//method that adds existing model to new collection
save: function(ev){
ev.preventDefault;
var savedEntry = dictionary.get(this.model);
dictionary.remove(this.model);
saved.add(savedEntry);
console.log(savedEntry.toJSON());
},
close: function(){
var definition = this.$('.definition').text();
this.model.set('definition', definition);
this.$('.definition').attr('contenteditable', false).blur();
},
updateOnEnter: function(ev){
if(ev.which == 13){
this.close();
}
},
initialize: function(){
this.template = _.template($("#dictionary_template").html());
},
render: function(){
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
//--------------DICTIONARY VIEW------------
var DictionaryView = Backbone.View.extend({
model: dictionary,
el: $('#entries'),
initialize: function(){
this.model.on('add', this.render, this);
this.model.on('remove', this.render, this);
},
render: function(){
var self = this;
self.$el.html('');
_.each(this.model.toArray(), function(entry, i){
self.$el.append((new EntryView({model: entry})).render().$el);
});
return this;
}
});
//---------SAVED ENTRY VIEW-----------
var SavedView = Backbone.View.extend({
model: saved,
el: $('#saved'),
initialize: function(){
this.model.on('save', this.savedRender, this);
},
//method that renders new collection view with different el:
savedRender: function(){
var self = this;
self.$el.html('');
_.each(this.model.toArray(), function(saved, i){
self.$el.append((new EntryView({model: savedEntry})).render().$el);
});
return this;
}
});
//-------BINDING DATA ENTRY TO NEW MODEL VIEW-------
$(document).ready(function(){
$('#new-entry').submit(function(ev){
var entry = new Entry({word: $('#word').val(), definition: $('#definition').val() });
dictionary.add(entry);
dictionary.comparator = 'word';
console.log(dictionary.toJSON());
$('.form-group').children('input').val('');
return false;
});
var appView = new DictionaryView();
});
//--------------ROUTER----------------
var Router = Backbone.Router.extend({
routes:{
'':'home'
}
});
var router = new Router();
router.on('route:home', function(){
console.log('router home');
});
Backbone.history.start();
})(jQuery);
</script>
There are a number of problems here.
First, you do not have an instance of SavedView. The var SavedView = Backbone.View.extend(...); statement is just defining the SavedView class. In order to have a living instance of this class, you must initialize one with the new operator. You will need a line like the following somewhere in your code (a good place would be at the end of the jQuery ready handler):
var saved_view = new SavedView();
Next, we will investigate the save method of the EntryView class. The var savedEntry = dictionary.get(this.model); statement is completely unnecessary because we know that dictionary.get(this.model) will return this.model - which we obviously already have an instance of. So we can remove the clutter from this method and be left with the following:
ev.preventDefault;
saved.add(this.model);
dictionary.remove(this.model);
However, we are still not at our destination. If we turn our attention to the SavedView class definition, we see that it is binding its render method to the 'save' event on its collection, the saved object. Its not the 'save' event we should be binding to, but rather the 'add' event - as that is what will be triggered when we add models to saved:
this.model.on('add', this.savedRender, this);
If we test our code now we should get scolded with a reference error on savedEntry within SavedView.savedRender. It looks like this is a typo and what was intended was `saved'. (You will notice below that in addition to correcting the typo, I have also removed a set of parentheses from this expression that served no function save for making the code less readable):
self.$el.append(new EntryView({ model: saved }).render().$el);
EDIT:
In response to your follow-up question about the saved variable inside the SavedView.savedRender method:
The saved object in this case is a single Entry model. The reason for your confusion is that we are re-using the variable name "saved". Within the _.each callback we have defined the first parameter to be called "saved"; this "saved" is local to the callback and is not related to the EntryList collection defined previously. Within our callback, saved is an element of the saved collection (yikes!) - which is a lesson in why variable naming is important.
As I proceeded to change the name of "saved" in the savedRender method, I noticed a few other refactorings that were screaming to be made. I have listed my refactorings below:
A purpose of using Backbone (http://backbonejs.org/) is to give us access to convenient helpers for objects (models) and arrays (collections). Backbone collections have an each method we can make use of instead of passing our collection to Underscore's (http://underscorejs.org/) each.
As stated above, saved is a terrible name for our each callback parameter because it conflicts conceptually with the name of the collection. Because saved is a collection of Entry models, "entry" is a much more suitable name.
Backbone allows us to pass the context to our each callback that will be our this within that callback. This allows us to skip the step of caching our this in the self variable.
My refactored savedRender becomes:
savedRender: function () {
this.$el.empty();
this.model.each(function (entry) {
this.$el.append(new EntryView({ model: entry }).render().$el);
}, this);
return this;
}
I'm currently using Backbone + RequireJS.
In my application, I display a tree widget that is constructed with the same Model with nested Collections.
That is to say:
FooCollection
define(['backbone', 'models/foo'], function(Backbone, FooModel) {
var FooCollection = Backbone.Collection.extend({
model: FooModel
});
return FooCollection;
});
FooModel
define(['backbone', 'underscore'], function(Backbone, _) {
var FooModel = Backbone.Model.extend({
initialize : function() {
_.bindAll(this, 'adoptOne', 'adoptAll');
var self = this;
// Need to do it this way or RequireJS won't find it
require(['collections/foos'], function(FooCollection) {
self.foos = new FooCollection();
self.on('change:foos', function() {
self.foos.reset(self.get('foos'));
});
self.foos.on('reset', self.adoptAll);
self.foos.on('add', self.adoptOne);
self.foos.reset(self.get('foos');
});
},
adoptAll : function() {
this.foos.each(this.adoptOne);
},
adoptOne : function(foo) {
foo.parent = this;
}
});
return FooModel;
});
The above works. I don't get any errors and everything is constructed as expected.
However...
// In a view
this.foos = new FooCollection();
this.foos.fetch({
success : function(foos) {
var treeView = new TreeView();
treeView.render(foos); // Doesn't work!!
}
});
The above doesn't work because of a sync problem: the TreeView gets rendered before the nested collections have finished creating (either because it takes longer to run the code or because it takes time to load 'collections/foos'.
Either way, I can fix it with this:
setTimeout(function() {
treeView.render(foos);
}, 100);
But that, of course, it's just a hack. In a production environment it could take more than 100 miliseconds and the code wouldn't work.
So, I guess that what I should do is to trigger some sort of event that my view listens to. However, my question to y'all is the following: when do I know that the entire collection of foos have been constructed and where do I attach the listener?
Thanks in advance!!
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.
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.
I'm working on an app that has an ItemListView that contains a number of ItemView elements. In my ItemListView, I'm using the jQuery .each() method to loop through the collection of items and render them as list elements.
I've got all the pieces in place except for the actual attaching of the li elements to the containing ul. The sticking point is getting access to the ItemListView.appendItem method from inside of my .each() loop. I've tried using this.appendItem and self.appendItem, but inside the loop this is the item and self is the window object.
Here's what I have right now:
ItemListView = Backbone.View.extend({
el: '#item-rows',
initialize: function () {
this.collection = new Items();
this.render();
},
render: function () {
$.each(this.collection.models, function (i, item) {
var itemview = new ItemView( { model: item });
this.appendItem(itemview); // this refers to the item, so appendItem is undefined
});
},
appendItem: function (itemView) {
$(this.el).append(itemView.render().el);
}
});
var itemlistview = new ItemListView;
I'm pretty sure that the context issue is the only problem, as I've examined the other pieces of the this by outputting them to the console and they look fine.
What am I missing?
A more Backbone-y way to do this would be to use the collection's each, provided by underscore.js:
render: function() {
this.collection.each( function( item, index ) {
var itemView = new ItemView( { model:item } );
this.appendItem( itemView );
}, this );
return this;
}
Notes:
notice the second param to each which take a reference to bind the function to
the each takes the element first and the index second
render should generally return this for chaining purposes (as mentioned in the docs), I don't think your appendItem function will work as you expect without this part
Yeah, it's a pretty simple fix. You just gotta refer to the this in the outer context.
render: function () {
var somereftothis = this;
$.each(this.collection.models, function (i, item) {
var itemview = new ItemView( { model: item });
somereftothis.appendItem(itemview); // this refers to the item, so appendItem is undefined
});
},