I am new to backbone.js and I am trying to learn it. In the code below I want my collection called "JokesCollection" to only accept adding models of the class "Joke". How do I do achieve this? When setting "Collection" attribute "model" to a certain model, isn´t the collection supposed to only accept that model class and ensure homogeneity? Don´t seam so. When I assign attribute "model" in the "JokesCollection" class to "Joke" it still accepts adding models of class "Persson" witch is not what I want. I only want it to accept adding models of class "Joke".
Joke = Backbone.Model.extend ({
initialize: function(){
console.log("Joke was created");
},
defaults: {
joke : "",
date : "0",
}
});
JokesCollection = Backbone.Collection.extend({
initialize: function(){
console.log("JokesCollection was created");
},
model: Joke // <=== Isn´t this supposed to ensure that the collection only accepts models of class "Joke"?
});
Person = Backbone.Model.extend ({
initialize: function(){
console.log("Person was created");
},
defaults: {
username: "default",
password: "default",
email: "default"
}
});
var person1 = new Person({username:"masterMind"});
var joke1 = new Joke({joke:"Girls are cute and funny hahahaha"});
jokesCollection = new JokesCollection();
jokesCollection.add(joke1);
jokesCollection.add(person1); // This adds a model of class "Person" to the collection. Witch is not what I want. It is not supposed to work! I want "jokesCollection" to only accept models of class "Joke".
console.log(jokesCollection.length); // length gets increased by 1 after adding "person1" to "jokesCollection". Again, it is no supposed to work from my point of view. I want "jokesCollection" to only accept models of class "Joke".
console.log(jokesCollection);
From official docs:
model collection.model
Override this property to specify the model class that the collection
contains. If defined, you can pass raw attributes objects (and arrays)
to add, create, and reset, and the attributes will be converted into a
model of the proper type.
Looks like will have to re-write add method something like this :
add: function(models, options) {
var modelClass = this.model;
isProperIns = this.models.every.(function(model){
return model instanceof modelClass;
});
if (!isProperIns) {
throw new Error("Some of models has unacceptable type")
}
return this.set(models, _.extend({merge: false}, options, addOptions));
}
The purpose of a Collection's model property is not to limit which models the Collection can accept. Rather, that property defines the Model class which the Collection will use when it needs to create a new Model. For instance,when you pass an object literal of Model attributes (as opposed to an instantiated Model) to JokesCollection.add, or when you fetch models in to a JokesCollection, Backbone will use Joke as the Model to instantiate those new additions to the Collection.
There are two ways to ensure your JokesCollection is only populated with instances of Joke. The first way is to never add Model instances to the JokesCollection directly, and instead either:
A) Bring new Jokes in from the server by calling fetch on a JokesCollection
B) add only "raw" Model attributes to the JokesCollection; don't add instantiated Models
However, if you're concerned about a developer accidentally adding a non-Joke Model to the Collection, your other option (as first suggested by #Evgeniy) is to overwrite your JokesCollection's add method. Unlike #Evgeniy's answer though I would not recommend re-writing Backbone's internals. Instead, I would use a simple overwrite that just calls the base Backbone method if possible:
add: function(models, options) {
if (models instanceof Joke) {
// Use the normal Backbone.Collection add method
return Backbone.Collection.prototype.add.call(this, models, options);
}
var allModelsAreJokes = _(models).all(function(model) {
return model instanceof Joke;
));
if (allModelsAreJokes) {
// Use the normal Backbone.Collection add method
return Backbone.Collection.prototype.add.call(this, models, options);
}
// Handle the case where non-Jokes are passed in; either:
// A) convert whatever was passed in to be a Joke:
// var rawModels = _(models).isArray() ? _(models).invoke('toJSON') : model.toJSON();
// return Backbone.Collection.prototype.add.call(this, rawModels, options);
// B) just don't add anything
}
Related
Usually I find my self needing to write an object with a specific functionality that it is a set of models.
Finally I extend a collection and add more functions that works with its model.
I think is better show you an example:
My app has a set of permissions, related with the user and/or the version of the platform.
var Permissions = Backbone.Collection.extend({
model: Permission,
hasAccess: function (moduleCode) {
....
},
allowAccess: function (moduleCode) {
....
},
...
With that methods I change the format of a property of a permission (the model). (My permissions are a concatenation of code with an string that identify the type of permission.)
A workmate tells me that it is wrong. In .NET he creates a class and he adds a private list and makes the changes to it. He does not inherit the list and changes it.
He would make a model and inside it he would add a collection property
this.set("permissionsCollection", new Backbone.Collection.extend({model: Permission}))
[Comment: I don't understand why he creates properties of everything, I think in his case it is not needed.] -> But this is another question
I think in a different way. I know the Collection has internally that list. I have a great potencial in Backbone.Collections, why do I have to use a model that it is not necessary? If I don't need that encapsulation... I think that it is not necessary, he is overprogramming in my opinnion.
Am I wrong? Did I not know how to use BackboneJS Collections?
Thank you in advance.
At the beginning I had something called helper with similar methods:
findAttr: function (model, name) {
var attrs = model.get('app_attrs');
if (attrs !== undefined) {
return this.findByAttrName(attrs, name);
}
},
findByAttrName: function (array, name) {
return _.find(array, function(a) {
if (a.attrName === name) {
return a;
}
});
}
The view code was more awkward:
render: function () {
var attr = helper.findAttr(this.model, 'user');
...
return this;
}
The only logical solution was to move these methods into the model (this.model in the above case). After refactoring I've got:
render: function () {
var attr = this.model.findAttr('user');
...
return this;
}
which is of course more readable than the previous solution.
I have a complex model served from my back end, which has a bunch of regular attributes, some nested models, and a couple of collections.
My page has two tables, one for invalid items, and one for valid items. The items in question are from one of the nested collections. Let's call it baseModel.documentCollection, implementing DocumentsCollection.
I don't want any filtration code in my Marionette.CompositeViews, so what I've done is the following (note, duplicated for the 'valid' case):
var invalidDocsCollection = new DocumentsCollection(
baseModel.documentCollection.filter(function(item) {
return !item.isValidItem();
})
);
var invalidTableView = new BookIn.PendingBookInRequestItemsCollectionView({
collection: app.collections.invalidDocsCollection
});
layout.invalidDocsRegion.show(invalidTableView);
This is fine for actually populating two tables independently, from one base collection. But I'm not getting the whole event pipeline down to the base collection, obviously. This means when a document's validity is changed, there's no neat way of it shifting to the other collection, therefore the other view.
What I'm after is a nice way of having a base collection that I can have filter collections sit on top of. Any suggestions?
I fleshed out my previous attempt and have come up with an extension to Backbone.Collection that does what I need.
collections.FilteredCollection = Backbone.Collection.extend({
initialize: function(items, options) {
if (_.isUndefined(options.baseCollection))
throw "No base collection to watch";
if (!_.isFunction(options.filterFunc)) {
throw "No filter to apply";
}
_.extend(this, options);
this.listenTo(this.baseCollection, 'all', this.reraise);
},
reraise: function (event) {
this.reset(this.baseCollection.filter(this.filterFunc), { silent: true });
var args = [].slice.call(arguments, 1);
this.trigger(event, args);
}
});
The one small issue I have with this is that I have to manually apply filterFunc to the baseCollection, then pass that in as the items parameter when instantiating a FilteredCollection, but that's something I can live with.
The below code is what I'm using to instantiate. Note that there's another almost-exact copy which is for the collection of ONLY VALID items, but any filters can be applied.
var allDocs = theModel.get('Documents');
var invalidOptions = {
baseCollection: allDocs,
filterFunc: function(item) {
return !item.isValidItem();
}
};
var invalidDocs = allDocs.filter(invalidOptions.filterFunc);
var invalidDocsCollection = new collections.FilteredCollection(
invalidDocs, invalidOptions
);
If I have a model named "Book" and a collection named "Library" defined as below:
Book
app.Book = Backbone.Model.extend({
defaults: {
title: 'No title',
author: 'Unknown'
}
});
Library
app.Library = Backbone.Collection.extend({
model: app.Book,
url: '/api/books'
});
When I call BookInstance.save() how does it come up with the URL to use? Does it derive it from the collection?
In Backbone.model there are 2 options: url and urlRoot. What is the purpose and difference between these?
In Backbone.collection, there is a single parameter url. Is this always the GET request for the RESTFUL api?
Basically, there are 3 possibilities to construct a model's url:
If the model object exists in a collection then its url method will return an address composed of the collection.url and model.id: [collection.url]/[id].
If you don't want to use a model inside the collection, then model.urlRoot's value can be used instead of the collection.url fragment, resulting in the following pattern: [urlRoot]/[id].
Finally, if you're NOT planning to persist more that one model of a given type to the server or will be defining URLs for each model upon their creation, you can directly assign a value to model.url.
Collections send only GET requests — to get an array of models' JSON data. For saving, removing, and updating, the individual model's save() (POST/PUT/PATCH) and destroy() (DELETE) methods are used.
Here's the source code of Backbone.Model.url, which should help you:
url: function() {
var base =
_.result(this, 'urlRoot') ||
_.result(this.collection, 'url') ||
urlError();
if (this.isNew()) return base;
var id = this.get(this.idAttribute);
return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id);
}
In model
urlRoot is used for the Model.
url is used for the instance of the Model.
So if urlRoot exists
then book.fetch() will fetch the data given id, for example
var Book = Backbone.Model.extend({urlRoot: 'books' });
var book = new Book({id: 1});
book.fetch(); // will get /books/1
var Book = Backbone.Model.extend({});
var book = new Book({url: 'books/1'});
book.fetch(); // will get /books/1
var Books = Backbone.Collection.extend({model: Book});
var books = new Books({ /*....*/ });
books.fetch(); // will get /books/
You can refer the backbone Model urlRoot source code here
I hope it makes sense to you, good luck.
I have a collection of Animals.
App.Collections.Animals extends Backbone.Collection
model: App.Animal
url: '/animals/' #returns json
And these animal classes:
App.Models.Animal extends Backbone.Model
App.Models.Monkey extends App.Models.Animal
defaults:{type:'Monkey'}
App.Models.Cat extends App.Models.Animal
defaults:{type:'Cat'}
App.Models.Dog extends App.Models.Animal
defaults:{type:'Dog'}
When collection is filled with JSON (records contain the type attribute) I want models to be instantiated as sub-classed models (Monkey,Cat,Dog) and not as Animal. How can you achieve this?
From Backbone documentation:
A collection can also contain polymorphic models by overriding this
property with a function that returns a model.
var Library = Backbone.Collection.extend({
model: function(attrs, options) {
if (condition) {
return new PublicDocument(attrs, options);
} else {
return new PrivateDocument(attrs, options);
}
}
});
The solution is straightforward (pardon the JS, I don't know CoffeeScript):
var SmartZoo = Backbone.Collection.extend({
model: function (attrs, options) {
// This code assumes that the object looks something like '{ type: "Cat", ... }'.
switch (attrs.type) {
case 'Cat':
return new Cat(attrs, options);
case 'Dog':
return new Dog(attrs, options);
default: // Unknown subclass
return new Animal(attrs, options);
}
}
});
You have to:
Include an attribute in your model from which you can infer the type of Backbone model to create. In this example, my objects contain an attribute called "type" whose value is the full name of the Backbone type that represents it. Be sure to set it in the defaults or initialize of your Model so that you can also add real model instances to the collection.
Define the models property of your collection as a function. The first parameter to this function will be the raw JS object (if that's what you passed in), or the attributes object of a Backbone model. Either way, you can access your type field as a property of this object.
Execute your logic to infer the proper model from your type field.
Return an instance of the correct model from the models function.
Here is a JSFiddle that shows this polymorphic collection in action: http://jsfiddle.net/FiddlerOnTheTarmac/uR2Sa/
Override backbone collection's _prepareModel. The collection new uses subclasses when defined otherwise uses the default model.
class App.Collections.Animals extends Backbone.Collection
model: App.Models.Animal
_prepareModel: (attrs, options) ->
if attrs instanceof Backbone.Model
attrs.collection = #
return attrs
options || (options = {})
options.collection = #
model_class = APP.Models[attrs.ntype] or this.model
model = new model_class(attrs, options)
if (!model._validate(attrs, options))
false
else
model
My Code:
I am new to Backbone.js and trying to build an app with Backbone.js and PHP. When I am trying to call add in the router, I am getting error:
Uncaught TypeError: Object [object Object] has no method 'set'.
Please help me to find my mistake.
Thanks.
// Models
window.Users = Backbone.Model.extend({
urlRoot:"./bb-api/users",
defaults:{
"id":null,
"name":"",
"email":"",
"designation":""
}
});
window.UsersCollection = Backbone.Collection.extend({
model:Users,
url:"./bb-api/users"
});
// Views
window.AddUserView = Backbone.View.extend({
template:_.template($('#new-user-tpl').html()),
initialize:function(){
this.model.bind("click", this.render, this);
},
render:function(){
$(this.el).html(this.template(this.model.toJSON()));
return this;
},
events:{
"click .add":"saveUser"
},
saveUser:function(){ alert('saveUser');
this.model.set({
name:$("#name").val(),
email:$("#email").val(),
designation:$("#designation").val()
});
if(this.model.isNew()){
this.model.create(this.model);
}
return false;
}
});
// Router
var AppRouter = Backbone.Router.extend({
routes:{
"":"welcome",
"users":"list",
"users/:id":"userDetails",
"add":"addUser"
},
addUser:function(){
this.addUserModel = new UsersCollection();
this.addUserView = new AddUserView({model:this.addUserModel});
$('#content').html(this.addUserView.render().el);
}
});
var app = new AppRouter();
Backbone.history.start();
As suggested in the comments, the problem starts here here:
this.addUserModel = new UsersCollection();
this.addUserView = new AddUserView({model:this.addUserModel});
and finishes here:
saveUser:function(){ alert('saveUser');
this.model.set({
By passing a collection in place of a model you create confusion, and as a result later in the saveUser function you try to call a Backbone.Model method (set) on a Backbone.Collection instance.
Note: As of version 1.0.0 Backbone.Collection now has a set method. In previous versions, such as the one used by the question's author, that method was instead called update.
There are several steps you can take to clarify this code. For starters, I would rename your model and collection classes so that it's clear that the model is the singular form and the collection is the plural form:
window.Users => window.User
window.UsersCollection => window.Users
Next, I would create a new User model, instead of a Users collection, and pass that to your view:
this.addUserModel = new User();
this.addUserView = new AddUserView({model:this.addUserModel});
Finally, I'd remove these lines:
if(this.model.isNew()){
this.model.create(this.model);
}
For one thing, the model will always be new (as you just created it before passing it in), but more importantly you don't need to call the Collection's create method because that method creates a new model, when you already have one created. Perhaps what you should add instead is :
this.model.save();
if your intent is to save the model to your server.
Since you already specified a urlRoot for the model, that should be all you need to create a new model, pass it to your view, have your view fill in its attributes based on DOM elements, and finally save that model's attributes to your server.
I think you are facing problem with object scope. When event fired it send to event object to that function. Just try this it may work
Declare global variable with the current view inside the initialize
initialize : function(){ self = this; }
then change this to self,
saveUser:function(){ alert('saveUser');
self.model.set({
name:$("#name").val(),
email:$("#email").val(),
designation:$("#designation").val()
});
if(self.model.isNew()){
self.model.create(this.model);
}
return false;
}