Backbone model not instantiating in collection - javascript

I have a backbone model like so
define([
'underscore',
'backbone'
],function( _,Backbone) {
var Task = Backbone.Model.extend({
//api url
url:'',
methodToURL: {
'read': './api/tasks/index',
'create': './api/tasks/task',
'update': './api/tasks/task',
'delete': './api/tasks/task'
},
sync: function(method, model, options) {
options = options || {};
options.url = this.methodToURL[method.toLowerCase()];
Backbone.sync(method, model, options);
}
});
return Task;
});
And a collection
define(['underscore','backbone','models/task'],function( _,Backbone,Task) {
var TaskCollection = Backbone.Collection.extend({
//Model
model:Task,
//api url
url:'',
methodToURL: {
'read': './api/tasks/index',
'create': './api/tasks/task',
'update': './api/tasks/task',
'delete': './api/tasks/task'
},
sync: function(method, model, options) {
options = options || {};
options.url = this.methodToURL[method.toLowerCase()];
Backbone.sync(method, model, options);
},
//construct
initialize: function() {
this.sort_key = 'end';
this._model = new Task();
this.fetch();
},
comparator: function(a,b) {
a = a.get(this.sort_key);
b = b.get(this.sort_key);
return a > b ? 1
: a < b ? -1
: 0;
},
mark_complete: function(task_id) {
var task_status = 0;
console.log(this.model);
this.model.save({id:task_id,task_status:task_status});
},
mark_incomplete: function(task_id) {
var task_status = 1;
console.log(this.model);
this.model.save({id:task_id,task_status:task_status});
},
sort_by_status: function() {
this.sort_key = 'task_status';
this.sort();
},
sort_by_task_tag: function() {
this.sort_key = 'task_group';
this.sort();
}
});
return TaskCollection;
});
When i the mark_complete method runs the model is logged to the console, but it logs this
"function (){ parent.apply(this, arguments); } " and says "function (){ parent.apply(this, arguments); } has no method 'save'";
Am guessing the model is supposed to be instantiated so the collection can have access it to its methods, so what is wrong?

The model property is just a constructor that Collection uses when you add a model to the collection. It is intended to make your life easier when you try to input data to the collection. Instead of always calling the constructor when adding a Task model to TaskCollection, you'd just input a JavaScript object and it will do the same thing.
So this is how your code would look like when you would want to insert a model without setting the model property to your TaskCollection
taskCollection.add(new Task({
name: "Get Milk",
description: "We're out of milk. There's a sale going on at the local supermarket."
}));
// If you wanted to just input just the JSON object without calling the
// constructor, then you can't.
And this is how your code would look like if you had set the model property
taskCollection.add({
name: "Get Milk",
description: "We're out of milk. There's a sale going on at the local supermarket."
});
As you can see, you don't need to call the Task constructor; the instance of TaskCollection will call it for you.
And this is why instances of TaskCollection will only have the model property set to the actual constructor of Task, not an initialized version.

Related

Knockout.js Adding a Property to Child Elements

My code doesn't create a new property under the child element of knockout viewmodel that is mapped by knockout.mapping.fromJS.
I have:
//model from Entity Framework
console.log(ko.mapping.toJSON(model));
var viewModel = ko.mapping.fromJS(model, mappingOption);
ko.applyBindings(viewModel);
console.log(ko.mapping.toJSON(viewModel));
The first console.log outputs:
{
"Id": 0,
"CurrentUser": {
"BoardIds": [
{
"Id": 0
}
],
"Id": 1,
"UserName": "foo",
"IsOnline": true
},
"Boards": []
}
And then the mappingOption is:
var mappingOption = {
create: function (options) {
var modelBase = ko.mapping.fromJS(options.data);
modelBase.CurrentUser.UserName = ko.observable(model.CurrentUser.UserName).extend({ rateLimit: 1000 });
//some function definitions
return modelBase;
},
'CurrentUser': {
create: function (options) {
options.data.MessageToPost = ko.observable("test");
return ko.mapping.fromJS(options.data);
}
}
};
I referred to this post to create the custom mapping, but it seemed not working as the second console.log outputs the same JSON to the first one.
Also, I tried to create nested mapping option based on this thread and another one but it didn't work too.
var mappingOption = {
create: function (options) {
//modelBase, modifing UserName and add the functions
var mappingOption2 = {
'CurrentUser': {
create: function (options) {
return (new(function () {
this.MessageToPost = ko.observable("test");
ko.mapping.fromJS(options.data, mappingOption2, this);
})());
}
}
}
return ko.mapping.fromJS(modelBase, mappingOption2);
}
};
How can I correctly add a new property to the original viewmodel?
From the mapping documentation for ko.toJS (toJS and toJSON work the same way as stated in the document)
Unmapping
If you want to convert your mapped object back to a regular JS object, use:
var unmapped = ko.mapping.toJS(viewModel);
This will create an unmapped object containing only the properties of the mapped object that were part of your original JS object
If you want the json to include properties you've added manually either use ko.toJSON instead of ko.mapping.toJSON to include everything, or use the include option when first creating your object to specify which properties to add.
var mapping = {
'include': ["propertyToInclude", "alsoIncludeThis"]
}
var viewModel = ko.mapping.fromJS(data, mapping);
EDIT: In your specific case your mapping options are conflicting with each other. You've set special instructions for the CurrentUser field but then overridden them in the create function. Here's what I think your mapping options should look like:
var mappingOption = {
'CurrentUser': {
create: function (options) {
var currentUser = ko.mapping.fromJS(options.data, {
'UserName': {
create: function(options){
return ko.observable(options.data);
}
},
'include': ["MessageToPost"]
});
currentUser.MessageToPost = ko.observable("test");
return ko.observable(currentUser).extend({ rateLimit: 1000 });
}
}
};
and here's a fiddle for a working example

Implement search effectively in Backbone.js

I am trying to perform a search on my current collection and if the results aren't retrieved i am trying to query my search api
Collection:
var Backbone = require('backbone'),
_ = require('underscore'),
Urls = require('../../libs/urls'),
services = require('../../libs/services'),
skuListModel = require('../../models/sku/SkuListModel');
var SkuListCollection= Backbone.Collection.extend({
model: skuListModel,
sync: function (method, model, options) {
options = _.defaults({}, options, {
readUrl: Urls.sku.list
});
return services.sync.call(model, method, model, options);
}
});
View
searchData: function (e) {
var self = this;
var models = this.skuCollection.filter(function (item) {
return item.get("sku_code").indexOf(e.target.value) > -1
});
console.log(models);
if (models != null) {
self.skuCollection.set(models);
}
else {
self.skuCollection.fetch({
data: {
search_string: e.target.value
}
}).then(function (response) {
console.log(response);
//self.skuCollection.add(self.skuSearchCollection.toJSON(), { silent: true });
});
}
}
My question effectively is how do i modify my current collection to store the retrieved results and if my solution seems effective.
Move your filtering logic to the collection
Use promises to unify your response : an immediately resolved deferred if you find models, the xhr object if you have to fetch the data
Customize the behavior of fetch via the set options, e.g {remove: false} to keep the existing models
These points lead to a collection definition :
var SkuListCollection = Backbone.Collection.extend({
skus: function(code) {
var self = this;
var filtered = function() {
return self.filter(function (item) {
return item.get("sku_code").indexOf(code) !== -1;
});
};
var models = filtered();
if (models.length) {
// models found : define a promise and resolve it
var dfd = $.Deferred();
dfd.resolve(models);
return dfd.promise();
} else {
// models missing: fetch and add them
return this.fetch({
remove: false,
data: {
search_string: code
}
}).then(filtered);
}
}
});
Your view would then be rewired as :
searchData: function (e) {
this.skuCollection.skus(e.target.value).then(function(models) {
// do what you have to do with the filtered models
});
}
And a demo http://jsfiddle.net/nikoshr/84342xer/1/

Backbone Collection fetch() method override previous models on repeat call

I'm using backbone.js Collection fetch method to send request to return data based on offset and limit. First collection.fetch() method returns 5 models because LIMIT 0 and LIMIT 5 , and while calling again Backbone.Collection.prototype.fetch.call(this, options) its returns same data(previous models) but server response with next OFFSET=5 and LIMIT=5, i,e next set of objects.But i want to append in collection object whenever its call fetch method.
define([
'underscore',
'backbone',
'utils',
'core/base/models-and-collections',
'core/constants'
], function(_, Backbone, Utils, CollectionUtils, Constants) {
var Collection = Backbone.Collection.extend({
initialize: function(models, options) {
this.options = options;
_.bindAll(this, 'parse', 'url', 'pageInfo', 'nextPage');
typeof(options) != 'undefined' || (options = {});
typeof(this.limit) != 'undefined' || (this.limit = 5);
typeof(this.offset) != 'undefined' || (this.offset = 0);
typeof(this.size) != 'undefined' || (this.size = 5);
console.log("photo-collection-intitalize");
},
url: function() {
console.log("photo-collection-url-hit");
// if (this.size === this.limit) {
return Utils.keywordFormat("/api/student/{studentId}?limit={limit}&offset={offset}", {
studentId: this.options.studentId,
limit: this.limit,
offset: this.offset
});
},
nextFetch: function(options) {
console.log("next fetch method");
typeof(options) != 'undefined' || (options = {});
var self = this;
var success = options.success;
options.success = function(resp) {
if(success) {
success(self, resp);
console.info("-collection response on success");
console.info(resp);
}
};
return Backbone.Collection.prototype.fetch.call(this, options);
},
pageInfo: function(){
},
nextPage: function(){
},
parse: function(resp) {
console.info(resp);
if(!resp.items) {
console.error("items not specified", resp);
}
console.log("resp:limit "+resp.limit+ "resp: offset "+ resp.offset);
this.size= resp.limit;
return resp.items;
},
});
return Collection;
});
this.collection= new Collection();
this.collection.once("sync",this.GetFirstSetOfData,this);
this.collection.fetch();
GetFirstSetOfData: function(collection){
//set view
}
//set next data offset
this.collection.offset=this.collection.offset+this.collection.limit;
//call again fetch method
var newCollection=this.collection.nextFetch();
//new Collection is same data as previous this.collection have but server response next set of data
// i,e row 5-9
I would do something like this, when fetch is successful it triggers a 'sync' event. So each time it is successful you can increment the offset counter:
offsetStep: 5,
initialize: function() {
this.on('sync', this.incrementOffset);
}
incrementOffset: function() {
this.offset += this.offsetStep;
}
Then instead of having nextFetch method you just need to pass {add: true} when fetching - this will merge in new models from the server as long as they have unique ids:
myCollection.fetch({ add: true });

Backbone collection sortBy

I make my first backbone app and get some problems with collection sorting.
After using this
var SortedFriends = MyFriends.sortBy(function(friend) {
return friend.get("uid");
});
console.log(SortedFriends) show that SortedFriends contains sorted models, but when i try to use collection functions like 'SortedFriends.each' or 'SortedFriends.at' it make error:
TypeError: SortedFriends.each is not a function.
Code:
var Friend = Backbone.Model.extend({});
var Friends = Backbone.Collection.extend({
model: Friend,
});
var MyFriends = new Friends();
MyFriends.reset(<?=$friends?>);
var FriendView = Backbone.View.extend({
initialize: function(){
model:Friend
},
tagName: "tr",
template: _.template($('#item-template').html()),
className: "document-row",
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
var SortedFriends = MyFriends.sortBy(function(friend) {
return friend.get("uid");
});
var addOne = function(element){
var view = new FriendView({model: element});
$("#friends").append(view.render().el);
}
console.log(JSON.stringify(SortedFriends));
SortedFriends.each(function(friend){
var view = new FriendView({model: friend});
$("#friends").append(view.render().el);
});
If youre using backbone collections then youre probably better off using the comparator rather than collection methods
http://backbonejs.org/#Collection-comparator
When youre ready to sort your collection:
MyFriends.comparator = function(friend){
return friend.get("uid");
});
MyFriends.sort();
OR if you want to keep the order of the unsorted collection then you will need to clone it first
http://backbonejs.org/#Collection-clone
var SortedFriends = MyFriends.clone();
SortedFriends.comparator = function(friend){
return friend.get("uid");
});
SortedFriends.sort();
I'm not sure if it's a bug or a feature of Backbone's adaptation of sortBy, but apparently it returns an array, not an Underscore collection.
One workaround is to wrap the whole thing in _( ... ), which tells Underscore to wrap the array back into a collection:
var SortedFriends = _(MyFriends.sortBy(function(friend) {
return friend.get("uid");
}));
Edit
Most of the Underscore methods in Backbone seem to be chainable (replace sortBy with reject, for example, and it runs). Looking at the Backbone source where they wire up the Underscore proxies, it seems that sortBy is treated differently. I can't understand why they do it this way ...
var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf',
'isEmpty', 'chain'];
_.each(methods, function(method) {
Collection.prototype[method] = function() {
var args = slice.call(arguments);
args.unshift(this.models);
return _[method].apply(_, args);
};
});
var attributeMethods = ['groupBy', 'countBy', 'sortBy'];
_.each(attributeMethods, function(method) {
Collection.prototype[method] = function(value, context) {
var iterator = _.isFunction(value) ? value : function(model) {
return model.get(value);
};
return _[method](this.models, iterator, context);
};

Why are my Backbone Models nested strangely within a Collection, requiring drilling down to access methods/properties?

I've got a Collection and a Model, both using attributes/options to augment them with additional capabilities. Here's the Model (LoadRouteGroup):
return Backbone.Model.extend({
initialize: function () {
console.log(this);
},
fetchf: function () {
console.log("FETCH");
}
});
And the Collection (LoadRouteGroups):
return Backbone.Collection.extend({
constructUrl: function(options) {
if (options.groupingType === "facility") {
// TODO: new endpoint: /api/v1/loadroutes?grouping=facility
this.url = clawConfig.endpoints.webApiRootUrl + "/api/loads/facilities";
}
else {
this.url = clawConfig.endpoints.webApiRootUrl + "/api/v1/loadroutes";
}
},
initialize: function (models, options) {
options || (options = {});
this.constructUrl(options);
console.log(this);
}
});
They're instantiated as such:
var loadRouteGroup = new LoadRouteGroup({
entityType: "facility"
});
// WORKS
loadRouteGroup.fetchf();
// assign groupingType option to collection to denote which URL to use
var loadRouteGroups = new LoadRouteGroups({
model: loadRouteGroup
}, {
groupingType: "facility"
});
var firstGroup = loadRouteGroups.at(0);
// DOESN'T WORK
firstGroup.fetchf();
// WORKS
firstGroup.attributes.model.fetchf();
I would expect that call to firstGroup.fetchf() to work... but it doesn't. Instead, I have to weirdly drill down and use firstGroup.attributes.model.fetchf() in order to access the method.
What's going on here? This would seem straightforward to me, but I can't for the life of me figure out what's wrong with the relationship between my Collection and Model.
The collection definition should include the model type:
return Backbone.Collection.extend({
// ....
model: LoadRouteGroup
});
When initializing the collection, pass in an array of models:
var loadRouteGroup = new LoadRouteGroup({
entityType: "facility"
});
var loadRouteGroups = new LoadRouteGroups([loadRouteGroup], {
groupingType: "facility"
});
Specify the model when you extend the collection instead of when you instantiate.

Categories

Resources