Implementing a memoize-style module loader in Backbone - javascript

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.

Related

Knockoutjs nested view models

I'am using Knockoutjs and have a problem getting access to root/parent models data in a submodel. The data I get via Ajax.
The Ajax success creates a new WeekViewModel and this creates a few RowViewModels. And here is my Problem, at this time week is not defined.
After the site is rendered, I can get the Infos over week.
The only solution I've found is pasting the parent and root into the submodel.
But this works not so well cause at the initialsation, the parent is a plain js Obeject. After the site is rendered, and I will paste another rowViewModel from a click event, the parent is a knockout object.
Can anyone give me some suggestions where I'va made a mistake? Or whats a way to get this fixed?!
Here's my code:
$(function() {
var week;
var weekData;
$.when(
$.get('js/dataNew.json', function (res) {
weekData = res;
}),
// another calls ...
).then(function () {
week = new WeekViewModel(weekData);
ko.applyBindings(week, $('#content').get(0));
});
function WeekViewModel(data){
var self = this;
var mapping = {
'Rows': {
create: function(options) {
return new RowViewModel(options.data, self);
}
}
};
this.allDays = allDays;
this.cats = new ko.observableDictionary();
// more code ...
ko.mapping.fromJS(data, mapping, this);
};
function RowViewModel(row, parent){
var self = this;
var mapping = {
'RowDays': {
create: function(options) {
return new DayModel(options.data, self, parent);
}
}
};
if(row){
if(!row.DisplayName) {
// need data from the root here
// parent.cats <---
}
}
// more code ...
ko.mapping.fromJS(row, mapping, this);
}
// another submodel ...
});
Update:
fiddle:
http://jsfiddle.net/LkqTU/23750/
updated fiddle with html:
http://jsfiddle.net/LkqTU/23753/

Scoping issue devising dependency injection system for Backbone.js app

Our shop just recently adopted Backbone, and even gave me the green light to try out Marionette. However the latter is not giving me the bang for the buck I'd hoped for (possibly due to my ignorance), so instead I'm trying to devise a lightweight dependency injection system where all my Backbone views get an instance of an "Application" object of my own devising.
My system is based on a (necro) answer I posted yesterday and below is code that is working for me. However I don't want to have duplicate blocks of code for each view inside the injectViewsWithApp function, but rather would like to just loop over the views. But this is where I can't figure out how to get things working inside the loop, I think it's some sort of scoping issue.
I will continue working on it so I may post updates to the code during my remaining hour before my weekend begins.
define(['backbone', './app/views/searchform', './app/views/searchresults', './app/views/FooBardetailrow'],
function(Backbone, SearchFormView, SearchResultsView, FooBarDetailRowView) {
var APP = initApp();
injectViewsWithApp();
launchApp();
function initApp() {
return {
collections : { // No need for separate files/modules if each view gets injected with the APP
extendedHaulFooBars : new (Backbone.Collection.extend({})),
stn333sts : new (Backbone.Collection.extend({})),
FooBarTypes : new (Backbone.Collection.extend({})),
FooBarSymbols : new (Backbone.Collection.extend({})),
FooBarSearchParams : new (Backbone.Collection.extend({}))
},
defaultModel : Backbone.Model.extend({}),
views : {}
};
}
function injectViewsWithApp() {
SearchFormView.instantiator = SearchFormView.extend({
initialize : function() {
this.app = APP;
SearchFormView.prototype.initialize.apply(this, arguments);
}
});
SearchResultsView.instantiator = SearchResultsView.extend({
initialize : function() {
this.app = APP;
SearchResultsView.prototype.initialize.apply(this, arguments);
}
});
FooBarDetailRowView.instantiator = FooBarDetailRowView.extend({
initialize : function() {
this.app = APP;
FooBarDetailRowView.prototype.initialize.apply(this, arguments);
}
});
}
I've simplified things and gotten the following to "work". However, I'd like to assign view.instantiator back to view, and this I cannot get to work :( Definitely will accept somebody else's answer if they can make it so that in the end I can simply call "new MyView()" and get access to APP/app -- otherwise the following is not so bad I suppose.
var APP = {foo : 'bar'};
var MyView = Backbone.View.extend({});
var views = [MyView];
for (var i=views.length; i--; ) {
var view = views[i];
view.instantiator = view.extend({
initialize : function() {
this.app = APP;
view.prototype.initialize.apply(this, arguments);
}
});
console.log(new view.instantiator().app.foo);
}
console.log(new MyView.instantiator().app.foo);
// OUTPUT: 'bar' times 2
Here's another arguably better way; inheritance is even possible, as demonstrated, and this is particularly important in my case. Thanks to this fine answer as to how to retain the value of 'app' across calls to the ParentView and ChildView modules.
define(['backbone'],
function(Backbone) {
var APP = {
foo : 'bar'
};
var MyParentView = (function() {
var app;
return function() {
return {
ctor : Backbone.View.extend({
initialize : function() {
this.app = app;
}
}),
inject : function(_app) {app = _app;}
}
}
})();
var MyChildView = (function() {
var app;
return function() {
return {
ctor : MyParentView().ctor.extend({
initialize : function() {
console.log(this.app); // OUTPUT: undefined
MyParentView().ctor.prototype.initialize.apply(this, arguments);
console.log(this.app.foo); // OUTPUT: bar
}
}),
inject : function(_app) {app = _app;}
}
}
})();
var injectableViews = [MyParentView, MyChildView];
for (var i = injectableViews.length; i--; ) {
injectableViews[i]().inject(APP);
}
new (MyChildView().ctor)();
}
);

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.

Backbone + RequireJS + Self-Relational Model

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!!

backbone.js cannot render a view

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).

Categories

Resources