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!!
Related
I'm having a problem where render is being called autimatically in my Marionette CompositeView which is correct, the problem is that I'm fetching collection data in the initialize and want this to be present when the render happens. At the moment I'm running this.render() inside the done method of the fetch which re-renders but this causes problems as now I have 2 views per model. Can anyone recommend how I can properly prevent this initial render or prevent the duplicate views. 1 entry will output view1 and view2.
JS CompositeView
initialize: function() {
var self = this;
this.teamsCollection = new TeamsCollection();
this.teamsCollection.fetch().done(function() {
self.render();
});
},
First of all, I don't believe there is a way to stop rendering outright, but you have a bunch ways around that.
Option 1: fetch data first, then create your view and pass data into it when it's done.
//before view is rendered, this is outside of your view code.
var teamsCollection = new TeamsCollection();
teamsCollection.fetch().done(function(results) {
var options = {res: results};
var myView = new CompositeView(options);
myView.setElement( /* your element here */ ).render();
});
Option 2:
// don't use render method, use your own
initialize: function() {
var self = this;
this.teamsCollection = new TeamsCollection();
this.teamsCollection.fetch().done(function() {
self.doRender();
});
},
render: function(){}, // do nothing
doRender: function(){
// override render here rather than using default
}
Option 3: (if using template)
// if you have a template, then you can simply pass in a blank one on initialization
// then when the fetch is complete, replace the template and call render again
initialize: function() {
var self = this;
this.template = "<div></div"; // or anything else really
this.teamsCollection = new TeamsCollection();
this.teamsCollection.fetch().done(function() {
self.template = /* my template */;
self.render();
});
},
In reality I need more info. How is the view created? is it a region? is it added dynamically on the fly? Do you use templates? Can you provide any more code?
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
});
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.
Below I have the code for one of my modules. This is kind of spagetti-ish code, but all I want to accomplish is having a model, a collection, and render a view (using underscore templates) connecting the data from the collection to the views. I'm failing miserably. The problem I'm getting is that trying to run the last call down there to testfeed.render() tells me that render is not a function, yet it is clearly defined. I'm able to fetch that data and seemingly add it to the collection from the api. What am I doing wrong here?
// Create a new module.
var Tagfeed = app.module();
// Default model.
Tagfeed.Model = Backbone.Model.extend({
defaults : {
name : '',
image : ''
},
initialize : function(){
console.log('tagfeed model is initialized');
this.on("change", function(){
console.log("An attribute has been changed");
});
}
});
var feedCollection = Backbone.Collection.extend({
model: Tagfeed.Model,
initialize : function () {
console.log('feedcollection is initialized');
},
fetch: function () {
var thisCollection = this;
Api_get('/api/test', function(data){
$.each(data.data, function(){
thisCollection.add(this);
});
return thisCollection;
})
}
});
var test = new Tagfeed.Model({name:'test'});
var newFeedCollection = new feedCollection();
newFeedCollection.fetch();
console.log(newFeedCollection.at(0));
var testfeed = Backbone.View.extend({
el: $('#main'),
collection : newFeedCollection,
render: function( event ){
var compiled_template = _.template( $("#tag-template").html() );
this.$el.html( compiled_template(this.model.toJSON()) );
return this; //recommended as this enables calls to be chained.
}
});
testfeed.render();
EDIT * updated code from #mu is short suggestions
// Create a new module.
var Tagfeed = app.module();
// Default model.
var tagModel = Backbone.Model.extend({
defaults : {
name : '',
image : '',
pins : 0,
repins : 0,
impressions : 0
},
initialize : function(){
console.log('tagfeed model is initialized');
this.on("change", function(){
console.log("An attribute has been changed");
});
}
});
var feedCollection = Backbone.Collection.extend({
model: tagModel,
initialize : function () {
console.log('feedcollection is initialized');
},
fetch: function () {
var thisCollection = this;
Api_get('/reporting/adlift/pin_details', function(data){
thisCollection.add(data.data);
return data.data;
})
}
});
var test = new tagModel({name:'test'});
var newFeedCollection = new feedCollection();
newFeedCollection.fetch();
console.log(newFeedCollection.at(0));
var TestFeed = Backbone.View.extend({
el: $('#main'),
render: function( event ){
console.log('here');
var compiled_template = _.template( $("#tag-template").html(), this.collection.toJSON());
this.el.html( compiled_template );
return this; //recommended as this enables calls to be chained.
},
initialize: function() {
console.log('initialize view');
this.collection.on('reset', this.render, this);
}
});
//Tagfeed.testfeed.prototype.render();
var testfeed = new TestFeed({ collection: newFeedCollection });
testfeed.render();
and now when i run testfeed.render() I don't see any error, nor do i see that console.log in the render function. thoughts?
Your problem is right here:
var testfeed = Backbone.View.extend({ /*...*/ });
testfeed.render();
That makes your testfeed a view "class", you have to create a new instance with new before you can render it:
var TestFeed = Backbone.View.extend({ /*...*/ });
var testfeed = new TestFeed();
testfeed.render();
You're also doing this inside the "class":
collection : newFeedCollection
That will attach newFeedCollection to each instance of that view and that might cause some surprising behavior. The usual way of getting a collection into a view is pass it to the constructor:
var TestFeed = Backbone.View.extend({ /* As usual but not collection in here... */ });
var testfeed = new TestFeed({ collection: newFeedCollection });
testfeed.render();
The view constructor will automatically set the view's this.collection to the collection you pass when building the view.
Another thing to consider is that this:
newFeedCollection.fetch();
is usually an AJAX call so you might not have anything in your collection when you try to render it. I would do two things to deal with this:
Your view's render should be able to deal with an empty collection. This mostly depends on your template being smart enough to be sensible when the collection is empty.
Bind render to the collection's "reset" event in the view's initialize:
initialize: function() {
this.collection.on('reset', this.render, this);
}
Another problem you'll have is that your view's render is trying to render this.model:
this.$el.html( compiled_template(this.model.toJSON()) );
when your view is based on a collection; you want to change that to:
this.$el.html(compiled_template({ tags: this.collection.toJSON() }));
You'll need the tags in there so that the template has a name to refer to when looking at the collection data.
Also, you should be able to replace this:
$.each(data.data, function(){
thisCollection.add(this);
});
with just this:
thisCollection.add(data.data);
There's no need to add them one by one, Collection#add is perfectly happy with an array of models.
And here's a demo with (hopefully) everything sorted out:
http://jsfiddle.net/ambiguous/WXddy/
I had to fake the fetch internals but everything else should be there.
testfeed is not an instance - it's a constructor function.
var instance = new testfeed();
instance.render();
would probably work (what with you defining el during View definition - making it a prototype property, IIRC).
Began restructuring my Backbone app referencing this article by Bocoup: http://weblog.bocoup.com/organizing-your-backbone-js-application-with-modules
I'm having trouble initializing the views as defined in the module.
See this jsfiddle: http://jsfiddle.net/nicksergeant/8L6JX/
My application.js:
// Memoizing technique from http://weblog.bocoup.com/organizing-your-backbone-js-application-with-modules
var sidepros = {
// Create this closure to contain the cached modules
module: function() {
// Internal module cache.
var modules = {};
// Create a new module reference scaffold or load an
// existing module.
return function(name) {
// If this module has already been created, return it.
if (modules[name]) {
return modules[name];
}
// Create a module and save it under this name
return modules[name] = { Views: {} };
};
}()
};
// Using the jQuery ready event is excellent for ensuring all
// code has been downloaded and evaluated and is ready to be
// initialized. Treat this as your single entry point into the
// application.
jQuery(function($) {
if ($('body').hasClass('apply')) {
sidepros.app = new sidepros.module('apply').Views.AppView();
}
});
The module, apply.js:
(function(Apply) {
App = sidepros.app;
Apply.FieldModel = Backbone.Model.extend({
group: null
});
FieldView = Backbone.View.extend({
initialize: function() {
this.model = new FieldModel({
group: $(this.el).parents('div.group').attr('id')
});
this.model.view = this;
this.$tooltip = $('div.tooltip', $('#' + this.model.get('group')));
},
events: {
'focus': 'focused',
'blur' : 'blurred',
'keyup': 'updateTooltip'
},
focused: function() {
App.$tooltips.hide();
this.$tooltip.show();
},
blurred: function() {
App.$tooltips.hide();
},
updateTooltip: function() {
if (this.model.get('group') == 'name') {
short_name = $.trim(App.$first_name.val() + ' ' + App.$last_name.val().charAt(0));
if (short_name !== '') {
short_name = ': ' + short_name;
}
App.$name_preview.text($.trim(short_name));
}
}
});
AppView = Backbone.View.extend({
el: '#app',
initialize: function(opts) {
$('input, select, textarea', this.el).each(this.addField);
this.$first_name = $('input#id_first_name', this.el);
this.$last_name = $('input#id_last_name', this.el);
this.$name_preview = $('strong#name-preview', this.el);
this.$tooltips = $('div.tooltip', this.el);
},
addField: function() {
model = new FieldView({ el: this });
}
});
Apply.Views = {
'AppView': AppView,
'FieldView': FieldView
};
})(sidepros.module('apply'));
When attempting to init the AppView like so:
sidepros.app = new sidepros.module('apply').Views.AppView();
I get the error:
Uncaught TypeError: Object #<Object> has no method '_configure'
You are getting that error because Javascript is getting confused about the context of your constructor function. If you step into your AppView constructor, the context is Apply.Views, which means the new operator hasn't been called yet.
To get rid of that error, you need to do one of the following:
var appView = sidepros.module('apply').Views.AppView;
sidepros.app = new appView();
OR
sidepros.app = new (sidepros.module('apply').Views.AppView)();
Beyond that, I am not exactly sure what you are trying to do. There are no input, select or textarea nodes in your jsFiddle, so I can't say for sure what your next problem is.
In addition, this line model = new FieldView({ el: this }): feels really odd to me. Why are you setting your model to your view in the addField function?
I think a new jsFiddle is necessary to debug further.