While Stephen is right, he only focuses on the most probable bug and leaves you dealing with everything else. I'll try to expand on this in my answer.
Model URL with ID in a query string
The API's URL is quite complex and it's cumbersome to copy-paste it every time you need it. It's best to get the URL handling in one place and one way to achieve this is with a simple service.
// The API service to use everywhere you need the API specific data.
app.API = {
protocol: 'https',
domain: 'api.xxxxxx.com',
root: '/v12_1/',
params: {
appId: 'xxxx',
appKey: 'yyyy',
},
/**
* Get the full API url, with your optional path.
* @param {String} path (optional) to add to the url.
* @return {String} full API url with protocol, domain, root.
*/
url: function(path) {
path = path || '';
if (path.slice(-1) !== '/') path += '/';
return this.protocol + "://" + this.domain + this.root + path;
},
/**
* Adds the query string to the url, merged with the default API parameters.
* @param {String} url (optional) before the query string
* @param {Object} params to transform into a query string
* @return {String} e.g.: "your-url?param=value&otherparam=123"
*/
applyParams: function(url, params) {
return (url || "") + "?" + $.param(_.extend({}, this.params, params));
},
};
Fill it with the API information.
Then, you can create a base model and collection (or replace the default Backbone behavior).
app.BaseModel = Backbone.Model.extend({
setId: function(id, options) {
return this.set(this.idAttribute, id, options);
},
url: function() {
var base =
_.result(this, 'urlRoot') ||
_.result(this.collection, 'url') ||
urlError();
var id = this.get(this.idAttribute);
return app.API.applyParams(base, this.isNew() || { id: encodeURIComponent(id) });
},
});
app.BaseCollection = Backbone.Collection.extend({
model: app.BaseModel,
sync: function(method, collection, options) {
var url = options.url || _.result(model, 'url') || urlError();
options.url = aop.API.applyParams(url);
return app.BaseCollection.__super__.sync.apply(this, arguments);
}
});
Then using it is as simple as this:
app.MyModel = app.BaseModel.extend({
urlRoot: app.API.url('item'),
})
app.Collection = app.BaseCollection.extend({
model: app.MyModel,
url: app.API.url('collection-items'),
});
The below test outputs:
var app = app || {};
(function() {
app.API = {
protocol: 'https',
domain: 'api.xxxxxx.com',
root: '/v12_1/',
params: {
appId: 'xxxx',
appKey: 'yyyy',
},
/**
* Get the full API url, with your optional path.
* @param {String} path (optional) to add to the url.
* @return {String} full API url with protocol, domain, root.
*/
url: function(path) {
path = path || '';
if (path.slice(-1) !== '/') path += '/';
return this.protocol + "://" + this.domain + this.root + path;
},
/**
* Adds the query string to the url, merged with the default API parameters.
* @param {String} url (optional) before the query string
* @param {Object} params to transform into a query string
* @return {String} e.g.: "your-url?param=value&otherparam=123"
*/
applyParams: function(url, params) {
return (url || "") + "?" + $.param(_.extend({}, this.params, params));
},
};
app.BaseModel = Backbone.Model.extend({
setId: function(id, options) {
return this.set(this.idAttribute, id, options);
},
url: function() {
var base =
_.result(this, 'urlRoot') ||
_.result(this.collection, 'url') ||
urlError();
var id = this.get(this.idAttribute);
return app.API.applyParams(base, this.isNew() || {
id: encodeURIComponent(id)
});
},
});
app.BaseCollection = Backbone.Collection.extend({
model: app.BaseModel,
sync: function(method, collection, options) {
var url = options.url || _.result(model, 'url') || urlError();
options.url = aop.API.applyParams(url);
return app.BaseCollection.__super__.sync.apply(this, arguments);
}
});
app.MyModel = app.BaseModel.extend({
urlRoot: app.API.url('item'),
})
app.Collection = app.BaseCollection.extend({
model: app.MyModel,
url: app.API.url('collection-items'),
});
var model = new app.MyModel();
console.log("New model url:", model.url());
model.setId("53444d0d7ba4ca15456f5690");
console.log("Existing model url:", model.url());
var collection = new app.Collection();
console.log("collection url:", _.result(collection, 'url'));
var modelUrlThroughCollection = new app.BaseModel({
id: "test1234"
});
collection.add(modelUrlThroughCollection);
console.log("model via collection:", modelUrlThroughCollection.url());
})();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone-min.js"></script>
New model url: https://api.xxxxxx.com/v12_1/item/?appId=xxxx&appKey=yyyy
Existing model url: https://api.xxxxxx.com/v12_1/item/?appId=xxxx&appKey=yyyy&id=53444d0d7ba4ca15456f5690
collection url: https://api.xxxxxx.com/v12_1/collection-items/
model via collection: https://api.xxxxxx.com/v12_1/collection-items/?appId=xxxx&appKey=yyyy&id=test1234
How to populate models with an external API?
Backbone.js gives structure to web applications by providing models
with key-value binding and custom events, collections with a rich API
of enumerable functions, views with declarative event handling, and
connects it all to your existing API over a RESTful JSON interface.
If the API you're using adheres to REST principles, there's probably an endpoint which returns an array of objects. This is where the collection should fetch its data.
app.Collection = app.BaseCollection.extend({
model: app.MyModel,
url: app.API.url('collection-items'),
});
var collection = new app.Collection();
// GET request to
// https://api.xxxxxx.com/v12_1/collection-items/?appId=xxxx&appKey=yyyy
collection.fetch();
And it should receive something like:
[
{ id: "24b6463n5", /* ... */ },
{ id: "345333bbv", /* ... */ },
{ id: "3g6g346g4", /* ... */ },
/* ... */
]
If you want to add an existing model (referenced with an ID) to a collection:
var model = new app.MyModel({
// giving an id to a model will make call to fetch possible
id: "53444d0d7ba4ca15456f5690"
});
// GET request to
// https://api.xxxxxx.com/v12_1/item/?appId=xxxx&appKey=yyyy&id=53444d0d7ba4ca15456f5690
model.fetch();
collection.add(model);
The response should be a single object:
{ id: "53444d0d7ba4ca15456f5690", /* ... */ }
If you want to create a new model:
var model = new app.MyModel({ test: "data", /* notice no id passed */ });
// POST request to
// https://api.xxxxxx.com/v12_1/item/?appId=xxxx&appKey=yyyy
model.save();
// or, equivalent using a collection:
collection.create({ test: "data", /* notice no id passed */ });
Avoid .on
/.bind
in favor of .listenTo
Passing the context on an event binding is important with Backbone as most parts are classes versus jQuery callbacks which are usually anonymous functions working on local variables. In addition to this, you should use Backbone's listenTo
instead of on
.
Backbone js .listenTo
vs .on
listenTo
is the newer and better option because these listeners will
be automatically removed for you during stopListening
which is called
when a view gets removed (via remove()
). Prior to listenTo
there was a
really insidious problem with phantom views hanging around forever
(leaking memory and causing misbehavior)...
Avoid manually binding events with jQuery
In the views, you should use the events
property to automatically delegates the DOM events to the view's callbacks. It's still jQuery in the background, but cleaner, already integrated into Backbone and the context is automatically passed, so there's no need to use the var self = this
trick.
app.MyModel2View = Backbone.View.extend({
events: {
"click .add-myModel": "onAddModelClick",
},
onAddModelClick: function() {
this.model.myModels.add({});
},
// ...some code...
});
Creating a new model and fetching it makes no sense from the Backbone design unless you pass an id to the model. Just calling add
on the collection with an empty object will create a default model.