How do you stop a remove in backbone.js - backbone.js

I have collection of sprites and a collection of frames (each frame has an instance of a sprite in it) and I don't want to remove a sprite from the sprites collection if it is used in the frames collection.
I have looked into listening for the remove event and should be able to detect if it is in the frames collection, but without using an exception doubt that I could prevent the removal of the sprite.
What event should I be looking for, or should I be looking for something a little more complex?

Backbone source for remove method:
remove: function(models, options) {
models = _.isArray(models) ? models.slice() : [models];
options || (options = {});
var i, l, index, model;
for (i = 0, l = models.length; i < l; i++) {
model = this.get(models[i]);
if (!model) continue;
delete this._byId[model.id];
delete this._byId[model.cid];
index = this.indexOf(model);
this.models.splice(index, 1);
this.length--;
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
}
this._removeReference(model);
}
return this;
},
So, there isn't 'out-of-the-box' way do prevent deletion of element.
To achieve this, you can, extends Backbone.Collection and override remove method:
var SpriteCollection = Backbone.Collection.extend({
remove: function(attrs, options) {
//Some your checks
return Backbone.Collection.prototype.remove.call(this, options);
}
});

Related

store data issue with generic grid rendered in tabs

I have a generic grid component.
on click of menu item corresponding grid is displayed in independent tabs.
on rendering the grid component, store data is set dynamically and grid is populated.
The problem if I open two grids in two tabs, on navigating to the first tab, grid data is not displayed as the store data is set to second grid data.
Hoping to find solution.Thank you
code in main controller:
OnMenuItemClick: function(c){
var nodeText = c.text,
tabs = Ext.getCmp('app-tab'),
tabBar = tabs.getTabBar(),
tabIndex;
for(var i = 0; i < tabBar.items.length; i++) {
if (tabBar.items.get(i).getText() === nodeText) {
tabIndex = i;
}
}
if (Ext.isEmpty(tabIndex)) {
/* Note: While creating the Grid Panel,here we are passing the Menu/Grid Id along with it for future reference */
tabs.add(Ext.create('DemoApp.view.grid.GenericGrid',{title:nodeText,gridId:c.id,overflowY: 'scroll',closable:true}));
tabIndex = tabBar.items.length - 1 ;
}
tabs.setActiveTab(tabIndex);
}
code in generic grid controller:
renderGridMetadata: function(genericGrid) {
var store = Ext.getStore("DemoApp.store.GenericGrid"),
gridId = genericGrid.up().gridId,
resourceURL = "resources/data/" + gridId + ".json";
var serviceInput = Util.createServiceResponse(gridId);
/*Dynamically add the proxy URL to the ViewModel
DemoApp.model.GenericGrid.getProxy().setUrl(resourceURL);*/
Ext.getBody().mask("Loading... Please wait...", 'loading');
Ext.Ajax.request({
url: Util.localGridService,
method: 'POST',
headers: {
"Content-Type": "application/json",
'SM_USER': 'arun.x.kumar.ap#nielsen.com',
'SM_SERVERSESSIONID': 'asdfadsf'
},
jsonData: {
getConfigurationAndDataRequestType: serviceInput
},
success: function(conn, response, options, eOpts) {
Ext.getBody().unmask();
var data = Util.decodeJSON(conn.responseText);
/* Apply REST WebServices response Metadata to the Grid */
var recordsMetaData = data.getConfigurationAndDataReplyType.gridConfigDataResponse.data.record;
var jsonMetaDataArray = [];
for (var c = 0; c < recordsMetaData.length; c++) {
var jsonMetaDataObject = {};
var text = data.getConfigurationAndDataReplyType.gridConfigDataResponse.data.record[c].displayName;
var dataIndex = data.getConfigurationAndDataReplyType.gridConfigDataResponse.data.record[c].columnName;
jsonMetaDataObject["text"] = text;
jsonMetaDataObject["dataIndex"] = dataIndex;
jsonMetaDataArray.push(jsonMetaDataObject);
}
/* Apply REST WebServices response data to the Grid */
var recordsData = data.getConfigurationAndDataReplyType.gridDataResponse.record;
var jsonDataArray = [];
for (var r = 0; r < recordsData.length; r++) {
var columnsData = data.getConfigurationAndDataReplyType.gridDataResponse.record[r].column;
var jsonDataObject = {};
for (var c = 0; c < columnsData.length; c++) {
jsonDataObject[columnsData[c].columnId] = columnsData[c].columnValue;
}
jsonDataArray.push(jsonDataObject);
}
store.setData(jsonDataArray);
genericGrid.reconfigure(store, jsonMetaDataArray);
},
failure: function(conn, response, options, eOpts) {
Ext.getBody().unmask();
Util.showErrorMsg(conn.responseText);
}
});
store.load();
}
});
Most likely there is only one instance of DemoApp.store.GenericGrid.
Frankly, I only guess because I see that you call Ext.getStore("DemoApp.store.GenericGrid") that implies the store is declared in stores:["DemoApp.store.GenericGrid"] array probably in the application class.
If a store is declared this way then Ext automatically creates one instance of it setting storeId to the string listed in stores:[]. Hence, Ext.getStore() returns that instance.
If you want to have two independent instances of the grid you have to create store instances yourself preferably in initComponent override.

Stop backbone validation of collection if one model fails validation

Is there a way to stop backbone validation of colelction if one model fails validation? Currently, the code does this (taken from Backbone.js 1.1.0):
for (i = 0, l = models.length; i < l; i++) {
attrs = models[i];
if (attrs instanceof Model) {
id = model = attrs;
} else {
id = attrs[targetModel.prototype.idAttribute];
}
// If a duplicate is found, prevent it from being added and
// optionally merge it into the existing model.
if (existing = this.get(id)) {
if (remove) modelMap[existing.cid] = true;
if (merge) {
attrs = attrs === model ? model.attributes : attrs;
if (options.parse) attrs = existing.parse(attrs, options);
existing.set(attrs, options);
if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
}
models[i] = existing;
// If this is a new, valid model, push it to the `toAdd` list.
} else if (add) {
model = models[i] = this._prepareModel(attrs, options);
if (!model) continue;
toAdd.push(model);
// Listen to added models' events, and index models for lookup by
// `id` and by `cid`.
model.on('all', this._onModelEvent, this);
this._byId[model.cid] = model;
if (model.id != null) this._byId[model.id] = model;
}
if (order) order.push(existing || model);
}
So if _prepareModel returns false, which it will if a model is invalid, it will skip to next model and try to validate and add that.
I want to stop if one of the models fails validation? The app I'm working on is a tablet app and thousands of models are returned from the server (represented of course as JSON) so if the first model fails validation, I do not want all the others to be validated too, as the entire collection will be disguarded.
Any ideas how I can do this?
I would override the fetch method on your collection. Like this:
youCollection.fetch = function(options) {
options = options ? _.clone(options) : {};
if (options.parse === void 0) options.parse = true;
var success = options.success;
var collection = this;
options.success = function(resp) {
var method = options.reset ? 'reset' : 'set';
if (success) {
if (success(collection, resp, options))
collection[method](resp, options);
} else
collection[method](resp, options);
collection.trigger('sync', collection, resp, options);
};
wrapError(this, options);
return this.sync('read', this, options);
}
Here's what's happening. Focus on the line that starts with options.success =. When your server comes back with your requested models, a trigger invokes the function defined by options.success passing your models in as a parameter. In the original code, options.success looks like this:
options.success = function(resp) {
var method = options.reset ? 'reset' : 'set';
collection[method](resp, options);
if (success) success(collection, resp, options);
collection.trigger('sync', collection, resp, options);
};
What matters here that collection[method](resp, options); invokes set on the collection that called the fetch. And only then you have the option to invoke your own callback. Once collection.set is called, the validation loop on your models begins and there's nothing we could do.
We want to change that up. We want to first call our own success function. This function will run a for loop checking if each model.isValid(), and will return false as soon as one fails. If one does fail we never reach collection[method](resp, options); and set never happens.
To get a success function into your fetch call simply drop a reference to it in the options passed into fetch
yourCollection.fetch({ success: checkValidate });
I would additionally point you to Backbone.validate so that you can read about the 'invoke' trigger that gets fired when a model is invalid. That way you can handle another fetch or whatever.

Get properties of a Backbone model starting with certain text using Underscore.js

I have a Backbone Model in which there are certain properties like
test_id
test_name
test_desc
test_score
Now I want to retrieve properties which are starting with "test_".
I tried with code below and its working fine.
var MyModel = Backbone.Model.extend({
getTestProperties: function(str){
// get clone of attributes to iterate over
var testProperties = {};
var attrs = _.clone(this.attributes);
_.each(attrs, function(val, key){
if(key.indexOf(str) == 0){
testProperties[key]= val;
}
}, this);
}
});
But
Is there any other way I can get these properties using underscore methods ?
Thanks
Backbone proxies some methods from Underscore on models that can help you create a more readable _.filter: _.keys and _.pick
You can then simplify your function like this :
var MyModel = Backbone.Model.extend({
getTestProperties: function (str) {
// get the keys you want
var keys = _.filter(this.keys(), function (key) {
return key.indexOf(str) === 0;
});
// and build an object
return this.pick(keys);
}
});
And a demo http://jsfiddle.net/nikoshr/5a63c/
Try something like
var attrs = _.filter(_.keys(_.clone(this.attributes)), function(attr){
return attr.indexOf("text_") === 0;
});

Fetch a Backbone.Collection made up of other collections

So I have a few types of data:
Post
Project
Event
And each of those data models have their own collection and a route to view them:
/posts => app.postsCollection
/projects => app.projectsCollection
/events => app.eventsCollection
Now I want to add another route:
/ => app.everythingCollection
How can I create a collection which displays an aggregate of the other three collections, but without fetching all the post project and event data again?
Similarly, calling everythingCollection.fetch() would fill the postsCollection, projectsCollection and eventsCollection so that their data was available when they were rendered independently.
The whole point being never to download the same data twice.
Your app.everythingCollection doesn't have to be a really backbone collection. All it needs is just access and fetch to other collections.
You can inherit the Backbone.Events to gain all the events facilities also.
var fetchedRecords = {posts: 0, projects: 0, events: 0};
var Everything = function () {}
_.extend(Everything.prototype, Backbone.Events, {
fetch: function (option) {
that = this;
this.count = 0;
option.success = function () {that.doneFetch(arguments)};
if (fetchRecords.posts == 0) {
option.fetchedName = "posts";
app.postsCollection.fetch(option);
this.count ++;
}
if (fetchRecords.projects == 0) {
option.fetchedName = "projects";
app.projectsCollection.fetch(option);
this.count ++;
}
if (fetchRecords.events == 0) {
option.fetchedName = "events";
app.eventsCollection.fetch(option);
this.count ++;
}
},
donefetch: function (collection, response, options) {
if (this.count <=0) return;
this.count --;
if (this.count == 0) {
if (options.reset) this.trigger("reset");
}
fetchedRecords[options.fetchedName] ++;
},
posts: function () {return app.postsCollection},
projects: function () {return app.projectsCollection},
events: function () {return app.eventsCollection}
});
app.everythingCollection = new Everything;
everythingView.listenOn app.everythingCollection, "reset", everythingView.render;
app.everythingCollection.fetch({reset: true});
You will need to increment fetchedRecrods count to prevent fetch multiple times.
Something like this. Code is untested. But idea is the same.
var EverythingCollection = Backbone.Model.extend({
customFetch: function (){
var collections = [app.postsCollection, app.projectsCollection, app.eventsCollection],
index = -1,
collection,
that = this;
this.reset(); //clear everything collection.
//this function check collections one by one whether they have data or not. If collection don't have any data, go and fetch it.
function checkCollection() {
if (index >= collections.length) { //at this point all collections have data.
fillEverything();
return;
}
index = index + 1;
collection = collections[index];
if (collection && collection.models.length === 0) { //if no data in collection.
collection.fetch({success: function () {
checkCollection();
}});
} else { //if collection have data already, go to next collection.
checkCollection();
}
}
function fillEverything() {
collections.forEach(function (collection) {
if (collection) {
that.add(collection.models); //refer this http://backbonejs.org/#Collection-add
}
});
}
}
});
use like below.
app.everythingCollection = new EverythingCollection();
app.everythingCollection.customFetch();
for other collections, check models length before fetch data. Something like below.
if (app.postsCollection.models.length === 0) {
app.postsCollection.fetch();
}
Store all necessary collections in an array or object at app startup, attach an event listener to each of them listening for the first reset event and remember the ones you fetched in a second array. If the route where you need all collections is used you can fetch the ones not found in the array for the already fetched collections:
(untested, but it will give you the idea of how i suppose to do it)
var allCollections = [app.postsCollection, app.projectsCollection, app.eventsCollection];
var fetchedCollections = [];
$.each(allCollection, function(i, coll){
coll.once("reset", function(){
fetchedCollections.push(coll);
})
});
var fetchAll = function(){
$.each(allCollections, function(i, coll){
if( $.inArray(coll, fetchedCollections ) == -1 ){
coll.fetch();
}
});
}
Do this in your everythingCollection and you have the everythingCollection.fetchAll() functionality you need. You could also override the fetch function of the everythingCollection to first fetch all other collections:
fetch(options){
this.fetchAll();
return Backbone.Collection.prototype.fetch.call(this, options);
}
It sounds like braddunbar's supermodel or benvinegar's backbone.uniquemodel might address your problem
It's also worth checking out Soundcloud's article (see Sharing Models Between Views) on building Soundcloud next. They have a similar approach to the above two plugins in solving this problem.

Knockback: Remove item from an observable collection

Given an observable collection in Knockback, how do I remove an item from the underlying collection in response to a knockout.js click event?
If I'm right, a would say you want delete some item from the collection by clicking a button.
So we have the kb view:
var viewModel = kb.ViewModel.extend({
constructor: function(model, options) {
var self = this
this.delete= function(){
self.coll.delete(self)
}
this.coll = options.coll
this.name = kb.Observable(model, {key: 'name'})
}
});
var yourCollection = new Backbone.Collection();
var yourModel = new Backbone.Model({name: 'Stefan'});
var yourKBView = new viewModel (yourModel, {coll: yourCollection});
This is a simple way to store some nested information.
When you will do this automatic when a model is add in the collection you can override the create function of the view like this.
var collectionViewModel = kb.ViewModel.extend({
constructor: function(collection, options) {
var self = this
this.coll= kb.collectionObservable(collection, {
/**
* Calls by adding a model to the collcetion
* #param model -
* #param options -
*
*/
create: function(model, options){
var options = options || {}
options.coll = self
return new viewModel(model,options)
}
});
}
});

Resources