Backbone View Inheritance - javascript

I am trying to write a Backbone view for an object browser which is designed to be implemented in several places with different object types and slightly different operation.
I have tried simply extending the backbone view in my browser and then extending the browser in my implementation however this leaves me with some properties which are shared. This is an undesired effect as the data is appended to all implementations with every browser creation.
Could someone shed light on a way to solve this problem or perhaps an alternative solution?
Here are some code examples to give you a better idea of how it currently stands:
var BrowserView = Backbone.View;
_.extend(BrowserView.prototype, Backbone.View.prototype, {
className: 'browser',
collections: [],
events: {
},
_events:{
},
initialize: function () {
this._initialize();
},
render: function () {
this._render();
},
_initialize: function () {
this.container = $( this.make('div', {class: 'container'} ) );
this.$el.append(this.container);
if ( this.options.collections ) {
this.collections = [];
_.each(this.options.collections, this.add, this);
}
_.extend(this.events, this._events);
this.delegateEvents();
},
_render: function () {
this.container.empty();
_.each(this.collections, function (view) {
this.container.append(view.el);
view.render();
}, this);
}
});
BrowserView.extend = Backbone.View.extend;
var ContactBrowserView = BrowserView.extend({
});
EDIT
My issue is that the sub classes are sharing the collections property. Here is an example of my own solution which initialises the collections property through an inherited method. jsfiddle.net/JhZXh/3

I think I've figured out the answer to my own problem.
I believe the right way to achieve what I am looking for is to move the initialization of properties in to the initialize method provided by Backbone views. This way they are initialized
var BrowserView = Backbone.View.extend({
initialize: function () {
this.collections = [];
}
});
var FileBrowserView = BrowserView.extend({
initialize: function () {
BrowserView.prototype.initialize.apply(this);
this.collections.push({name: 'Example Collection' + Math.rand()});
}
});
var FileBrowserInstance1 = new FileBrowserView;
console.log(FileBrowserInstance1.collections);
var FileBrowserInstance2 = new FileBrowserView;
console.log(FileBrowserInstance2.collections);
http://jsfiddle.net/yssAT/2/

It's hard to see what exactly your goal is.
but this is how i see it
if you have an view object
var myView = Backbone.View.extend({
foo: "bar"
});
and you have it extend the backbone.View... then you actually have a new view object with everything of backbone.view, and the extra options you give as parameters.
if you then go and create a second view, that extends your first one
it will get everything from your first view, + it's own extras
var mySecondView = myView.extend({
foobar: "f00b#r"
});
if you would create an instance of the second view and log it's foo property it will still hold "bar" as value
var mySecondViewInstance = new mySecondView();
console.log("mySecondViewInstance.foo: ", mySecondViewInstance.foo);
console.log("mySecondViewInstance.foobar: ", mySecondViewInstance.foobar);
if i create a new instance of my first view, and change foo into "changed-foo"
the log of foo on mySecondViewInstance will still be "bar"
var myViewInstance = new myView();
myViewInstance.foo = "changed-foo";
console.log("mySecondViewInstance.foo: ", mySecondViewInstance.foo);
console.log("mySecondViewInstance.foobar: ", mySecondViewInstance.foobar);
a JS-Fiddle to play around with it can be found here:
http://jsfiddle.net/saelfaer/uNBSW/

Inherit from Backbone.View doesn't work, or is quite complex.
You should create a common object, which every of your view will inherit from, ie :
var ViewInterface = {
events : { /* ... */ },
initialize : function (options) { /* ... */ },
otherFunction : function (options) { /* ... */ },
}
each of your view would extend from this object :
var BrowserView = Backbone.View.extend(_.extend(ViewInterface, {
anotherFunction : function (options) { /* ... */ },
})
var AnotherView = Backbone.View.extend(_.extend(ViewInterface, {
yetAnotherFunction : function (options) { /* ... */ },
})

This gist shows a better alternative:
https://gist.github.com/2287018

Related

Initialize Backbone Models based on another Backbone Model

I have a config.json that I am going to load into my app as a Backbone Model like:
var Config = Backbone.Model.extend({
defaults: {
base: ''
},
url: 'config.json'
});
Other models should be dependent on some data contained in Config like:
var ModelA = Backbone.Collection.extend({
initialize: function(){
//this.url should be set to Config.base + '/someEndpoint';
}
});
In above example, ModelA's url property is dependent on Config's base property's value.
How do I go about setting this up properly in a Backbone app?
As I see it, your basic questions are:
How will we get an instance of the configuration model?
How will we use the configuration model to set the dependent model's url?
How can we make sure we don't use the url function on the dependent model too early?
There are a lot of ways to handle this, but I'm going to suggest some specifics so that I can just provide guidance and code and "get it done," so to speak.
I think the best way to handle the first problem is to make that configuration model a singleton. I'm going to provide code from backbone-singleton GitHub page below, but I don't want the answer to be vertically long until I'm done with the explanation, so read on...
var MakeBackboneSingleton = function (BackboneClass, options) { ... }
Next, we make a singleton AppConfiguration as well as a deferred property taking advantage of jQuery. The result of fetch will provide always(callback), done(callback), etc.
var AppConfiguration = MakeBackboneSingleton(Backbone.Model.extend({
defaults: {
base: null
},
initialize: function() {
this.deferred = this.fetch();
},
url: function() {
return 'config.json'
}
}));
Now, time to define the dependent model DependentModel which looks like yours. It will call AppConfiguration() to get the instance.
Note that because of MakeBackboneSingleton the follow is all true:
var instance1 = AppConfiguration();
var instance2 = new AppConfiguration();
instance1 === instance2; // true
instance1 === AppConfiguration() // true
The model will automatically fetch when provided an id but only after we have completed the AppConfiguration's fetch. Note that you can use always, then, done, etc.
var DependentModel = Backbone.Model.extend({
initialize: function() {
AppConfiguration().deferred.then(function() {
if (this.id)
this.fetch();
});
},
url: function() {
return AppConfiguration().get('base') + '/someEndpoint';
}
});
Now finally, putting it all together, you can instantiate some models.
var newModel = new DependentModel(); // no id => no fetch
var existingModel = new DependentModel({id: 15}); // id => fetch AFTER we have an AppConfiguration
The second one will auto-fetch as long as the AppConfiguration's fetch was successful.
Here's MakeBackboneSingleton for you (again from the GitHub repository):
var MakeBackboneSingleton = function (BackboneClass, options) {
options || (options = {});
// Helper to check for arguments. Throws an error if passed in.
var checkArguments = function (args) {
if (args.length) {
throw new Error('cannot pass arguments into an already instantiated singleton');
}
};
// Wrapper around the class. Allows us to call new without generating an error.
var WrappedClass = function() {
if (!BackboneClass.instance) {
// Proxy class that allows us to pass through all arguments on singleton instantiation.
var F = function (args) {
return BackboneClass.apply(this, args);
};
// Extend the given Backbone class with a function that sets the instance for future use.
BackboneClass = BackboneClass.extend({
__setInstance: function () {
BackboneClass.instance = this;
}
});
// Connect the proxy class to its counterpart class.
F.prototype = BackboneClass.prototype;
// Instantiate the proxy, passing through any arguments, then store the instance.
(new F(arguments.length ? arguments : options.arguments)).__setInstance();
}
else {
// Make sure we're not trying to instantiate it with arguments again.
checkArguments(arguments);
}
return BackboneClass.instance;
};
// Immediately instantiate the class.
if (options.instantiate) {
var instance = WrappedClass.apply(WrappedClass, options.arguments);
// Return the instantiated class wrapped in a function so we can call it with new without generating an error.
return function () {
checkArguments(arguments);
return instance;
};
}
else {
return WrappedClass;
}
};

Backbone architecture and view management

I am struggling with when to destroy backbone views. I know I need to destroy the view somewhere, but I am not sure where.
I have the following code in router.js
routes: {
"names/search": "nameSearch",
"companies/search": "companySearch"
},
initialize: function(){
Backbone.history.start();
this.navigate("#/", true);
}
nameSearch: function () {
require(["app/views/RecordSearch"], function (RecordSearchView) {
var obj = {};
obj.Status = [utils.xlate("On Assignment"), utils.xlate("Candidate")];
var view = new RecordSearchView({ model: obj, el: $(".content") }, { "modelName": "Candidate" });
view.delegateEvents();
});
},
companySearch: function () {
require(["app/views/RecordSearch"], function (RecordSearchView) {
var view = new RecordSearchView({ model: {}, el: $(".content") }, { "modelName": "Company" });
view.delegateEvents();
});
}
And then in RecordSearchView.js I have the following function that is called when a user clicks the search button
doSearch: function () {
require(["app/utils/SearchHelper", "app/models/" + modelName, "app/views/SearchResults"], function (SearchHelper, Model, SearchResultsView) {
var obj = $("#searchForm").serializeArray();
var params = SearchHelper.getQuery(obj);
params["page"] = 1;
params["resultsPerPage"] = 25;
var collection = new Model[modelName + "Collection"]({}, { searchParams: params });
params["Fields"] = collection.getSearchFields();
collection.getPage(params["page"], function (data) {
require(["app/views/SearchResults"], function (SearchResultsView) {
App.Router.navigate(modelName + "/search/results");
var view = new SearchResultsView({ collection: data, el: $(".content") });
view.delegateEvents();
});
});
return false;
});
And SearchResults.js
return BaseView.extend({
init: function () {
this.render();
},
render: function () {
var data = this.collection.convertToSearchResults();
this.$el.html(template(data));
return this;
}
});
The problem is the second time I perform any search (calling the doSearch function from RecordSearch.js). As soon as I perform the second search, the data shown is that belonging to the previous search I performed. (For example I do a name search and it works, then do a company search but the screen shows company search results but then is quickly replaced with name search results).
My questions are
I suspect I need to call some cleanup code on the view before it is re-used. Where is the proper place within a backbone application to run this.
Is there anything wrong with the way I load SearchResults view from within RecordSearch view? SearchResults does not have a path on my router, but it is basically a form post, so I assume it shouldn't?
Any help is appreciated.
This problem is quite common and is known as Zombie Views. Derick Bailey explains this issue very well here: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
However unfortunately you can't simply solve it without changing the way you are loading your views.
Because you are loading them inside RequireJS modules that will keep it in the local var scope, you are losing the reference to the views once the route has been fully processed.
In order to solve this problem, you would need to keep the reference of the current view somewhere, and then properly dispose it before calling another view, something like this:
showView: function(view) {
this.currentView && this.currentView.remove();
this.currentView = view;
this.currentView.render();
$('#content').html(this.currentView.el);
}
More about this solution here: http://tiagorg.com/talk-backbone-tricks-or-treats-html5devconf/#/6
I personally suggest you adopting a solution that will take care of this for you, like Marionette.js
It will handle this and quite many other issues, by providing the missing gaps of every Backbone-based architecture.

Clarification - Dealing with additonal information in Backbone.js

I'm fairly new to Backbone and I'm working on a project that is probably not ideal for Backbone really, but I'm trying anyway! I'm not working with a RESTful API, so all of my data is fetched on page load and that is it, it is then not updated, at all. The data is sent through as a single lump of JSON.
I have set up a new model CookingItem which gets created for every item in the JSON and is then added to the CookingItemList collection, this is fine. However, I need to store information about the data set somewhere so I have created another 'Properties model' that stores this information (state information, start items etc). This properties model will also have several methods that manipulate the data.. these are then called from the view (is this OK?).
This is working fine, but I just want to be sure what I'm doing is considered a good way of doing things. I'm sure there may be a better way to handle the Properties model?
// Models
var CookingItem = Backbone.Model.extend(),
CookingProperties = Backbone.Model.extend({
defaults:{
startItem: 'anything',
currentItems:[]
},
setStartItem:function (val) {
// Code Here to edit this models properties
},
addRemoveItem:function (val) {
// Code Here to edit this models properties
}
}),
// Collection
CookingItemList = Backbone.Collection.extend({
model:CookingItem,
url:function () {
return "json.js";
}
}),
// View
CookingView = Backbone.View.extend({
el:'.page',
events:{
// Buttons in the UI
'click .link1':function () {
this.cookingProperties.addRemoveItem('item name');
},
'click .link2':function () {
this.cookingProperties.addRemoveItem('item name');
}
},
initialize:function () {
// Scope
var _this = this;
// Instantiate
this.cookingItemList = new CookingItemList(); // PlayListItems Collection.
this.cookingProperties = new CookingProperties(); // Properties Model.
// Bindings
this.cookingProperties.on('change:startItem', function () {
_this.customMethod1();
_this.customMethod2();
});
// Render the view
this.render();
},
render:function () {
var _this = this;
this.cookingItemList.fetch({
success:function () {
_this.cookingProperties.setStartItem('item');
_this.customMethod1();
}
});
return this;
},
customMethod1:function () {
// Do something
},
customMethod2:function () {
// Do something
}
}),
// Start
cookingView = new CookingView({
collection:CookingItemList
});

Backbone.js, cannot set context on a callback

Ok, so I am working on a method to override the fetch method on a model. I want to be able to pass it a list of URL's and have it do a fetch on each one, apply some processing to the results, then update its own attributes when they have all completed. Here's the basic design:
A Parent "wrapper" Model called AllVenues has a custom fetch function which reads a list of URL's it is given when it is instantiated
For each URL, it creates a Child Model and calls fetch on it specifying that URL as well as a success callback.
The AllVenues instance also has a property progress which it needs to update inside the success callback, so that it will know when all Child fetch's are complete.
And that's the part I'm having problems with. When the Child Model fetch completes, the success callback has no context of the Parent Model which originally called it. I've kind of hacked it because I have access to the Module and have stored the Parent Model in a variable, but this doesn't seem right to me. The Parent Model executed the Child's fetch so it should be able to pass the context along somehow. I don't want to hardcode the reference in there.
TL;DR
Here's my jsFiddle illustrating the problem. The interesting part starts on line 13. http://jsfiddle.net/tonicboy/64XpZ/5/
The full code:
// Define the app and a region to show content
// -------------------------------------------
var App = new Marionette.Application();
App.addRegions({
"mainRegion": "#main"
});
App.module("SampleModule", function (Mod, App, Backbone, Marionette, $, _) {
var MainView = Marionette.ItemView.extend({
template: "#sample-template"
});
var AllVenues = Backbone.Model.extend({
progress: 0,
join: function (model) {
this.progress++;
// do some processing of each model
if (this.progress === this.urls.length) this.finish();
},
finish: function() {
// do something when all models have completed
this.progress = 0;
console.log("FINISHED!");
},
fetch: function() {
successCallback = function(model) {
console.log("Returning from the fetch for a model");
Mod.controller.model.join(model);
};
_.bind(successCallback, this);
$.each(this.urls, function(key, val) {
var venue = new Backbone.Model();
venue.url = val;
venue.fetch({
success: successCallback
});
});
}
});
var Venue = Backbone.Model.extend({
toJSON: function () {
return _.clone(this.attributes.response);
}
});
var Controller = Marionette.Controller.extend({
initialize: function (options) {
this.region = options.region;
this.model = options.model;
this.listenTo(this.model, 'change', this.renderRegion);
},
show: function () {
this.model.fetch();
},
renderRegion: function () {
var view = new MainView({
model: this.model
});
this.region.show(view);
}
});
Mod.addInitializer(function () {
var allVenues = new AllVenues();
allVenues.urls = [
'https://api.foursquare.com/v2/venues/4a27485af964a52071911fe3?oauth_token=EWTYUCTSZDBOVTYZQ3Z01E54HMDYEPZMWOC0AKLVFRBIEXV4&v=20130811',
'https://api.foursquare.com/v2/venues/4afc4d3bf964a520512122e3?oauth_token=EWTYUCTSZDBOVTYZQ3Z01E54HMDYEPZMWOC0AKLVFRBIEXV4&v=20130811',
'https://api.foursquare.com/v2/venues/49cfde17f964a520d85a1fe3?oauth_token=EWTYUCTSZDBOVTYZQ3Z01E54HMDYEPZMWOC0AKLVFRBIEXV4&v=20130811'
];
Mod.controller = new Controller({
region: App.mainRegion,
model: allVenues
});
Mod.controller.show();
});
});
App.start();
I think you're misunderstanding how _.bind works. _.bind returns the bound function, it doesn't modify it in place. In truth, the documentation could be a bit clearer on this.
So this:
_.bind(successCallback, this);
is pointless as you're ignoring the bound function that _.bind is returning. I think you want to say this:
var successCallback = _.bind(function(model) {
console.log("Returning from the fetch for a model");
Mod.controller.model.join(model);
}, this);
Also note that I added a missing var, presumably you don't want successCallback to be global.

Automatic _.bindAll() in backbone.js

Is there a way to automatically do an _.bindAll for a backbone.js object?
I was talking to someone a while ago and they said that there was, but I have no idea where to start looking.
Example:
var TheView = Backbone.View.extend({
initialize: function() {
// HOW CAN I AVOID HAVING TO DO THIS?---->
_.bindAll(this,'render','on_element_01_click', 'on_element_02_click');
},
events: {
'click #element_01': 'on_element_01_click',
'click #element_02': 'on_element_02_click',
},
render: function(){
return this;
},
on_element_01_click: function(){
},
on_element_02_click: function(){
}
}
Do this instead:
_.bindAll(this);
Will bind ALL functions in this view.
I've since learned of a easier technique if you want to build bindAll in to your views (which is handy for things like AJAX callback methods that aren't auto-bound the way event handlers are). Basically you just override the constructor to perform the auto-binding.
var BoundModel = Backbone.Model.extend({
constructor: function() {
Backbone.Model.apply(this, arguments);
if (this.boundMethods) {
_(this).bindAll.apply(this, this.boundMethods);
}
}
})
var SubclassOfBoundModel = Backbone.Model.extend({
boundMethods: ['handleFetchResponse'],
initialize: function () {
this.model.on('sync', this.handleFetchResponse);
}
handleFetchResponse: function() {
// this function is bound to the model instance
}
})
Of course if you just wanted to bind all your methods you could leave out the "boundMethods" part and just have:
constructor: function() {
Backbone.Model.apply(this, arguments);
_(this).bindAll();
}
I tried doing this myself and I was able to get it working with something like this:
function bindOnExtend(clazz) {
var originalExtend = clazz.extend;
clazz.extend = function() {
var newSubClass = originalExtend.apply(this, arguments);
var originalInitialize = newSubClass.prototype.initialize;
newSubClass.prototype.initialize = function() {
// The constructor will get broken by bindAll; preserve it so _super keeps working
var realConstructor = this.constructor;
_.bindAll(this);
this.constructor = realConstructor;
originalInitialize.apply(this, arguments);
};
return bindOnExtend(newSubClass);
};
return clazz;
}
var BoundModel = Backbone.Model.extend();
bindOnExtend(BoundModel);
var BoundView = Backbone.View.extend();
bindOnExtend(BoundView);
However, I wouldn't recommend it. Doing that will make closures for every single method on every single model/view/whatever you instantiate. Not only does that add a slight increase in overall memory usage, it also opens up the possibility of memory leaks if you're not careful. Furthermore, it makes your stacktraces several lines longer, as they have to wind through bindOnExtend.
In my experience, having to do "_.bindAll(this, ..." is worth the trouble because:
1) it makes my code more clear/obvious to anyone coming after me
2) it encourages me to qualify my bindAll, instead of just using the 1-arg form
3) I hate wading through long stacktraces
But, if you want it the above code should work.

Categories