Initialize Backbone Models based on another Backbone Model - javascript

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;
}
};

Related

Backbone: Best way to handle variable common to all models

I'm currently developing my first Backbone single page app project and I'm facing an issue.
Basically I have a menu (html select input element) implemented as a View. Its value is used to control pretty much every other data requests since it specifies which kind of data to show in the other Views.
Right now I handle the DOM event and trigger a global event so that every model can catch it and keep track internally of the new value. That's because that value is then needed when requesting new data. But this doesn't look like a good solution because A) I end up writing the same function (event handler) in every model and B) I get several models with the same variable.
var Metrics = Backbone.Collection.extend({
url: "dummy-metrics.json",
model: MetricsItem,
initialize: function () {
this.metric = undefined;
},
setMetric: function (metric) {
this.metric = metric;
globalEvents.trigger("metric:change", this.get(metric));
}
});
var GlobalComplexity = Backbone.Collection.extend({
url: function () {
var url = "http://asd/global.json?metric=" + this.metric;
return url;
}, //"dummy-global.json",
model: GlobalComplexyItem,
initialize: function () {
this.metric = undefined;
this.listenTo(globalEvents, "metric:change", this.updateMetric);
},
updateMetric: function (metric) {
this.metric = metric.get("id");
this.fetch({ reset: true });
}
});
All my other Collections are structured like GlobalComplexity.
What's the cleanest way to solve this problem?
Thank you very much.
Define a global parametersManager. Export an instance (singleton) then require it when you need it.
On "globalupdate" you update the parametersManager then trigger "update" for all your model/collections so they'll look what are the current parameters in the parametersManager.

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.

Standard way of returning backbone collection in requirejs

I've seen different examples in different articles about how to return a Backbone collection (or View, for that matter) from a RequireJS define. For example:
define(['models/person'], function( person ) {
var personCollection = Backbone.Collection.extend({
model: person,
url: "api/person"
});
// do this?
return new personCollection();
// or this?
//return personCollection;
});
Is there a memory advantage to either approach? Is there standard design pattern that dictates which should be used?
The same question would apply to views, as I've seen them done both ways too.
I would do the second way, because then you would receive reference to "blueprint" not the object itself. In most cases one should want to initialize object creation itself.
Also, even if you want only single instance of collection created I would advise using factory method like this:
define(['models/person'], function( person ) {
var personCollection = Backbone.Collection.extend({...}),
singleCollection,
export = {
getInstance = function (options) {
if (!singleCollection) {
singleCollection = new personCollection(options);
}
return singleCollection;
}
}
return export;
});
And then you could call it like this:
require('models/person', function (person) {
var personCollection = person.getInstance(options);
}

Backbone View Inheritance

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

Categories