|
/** |
|
* Backbone Relational localStorage Adapter |
|
* Version 1.0 |
|
* |
|
* Based on https://github.com/jeromegn/Backbone.localStorage |
|
*/ |
|
(function (root, factory) { |
|
if (typeof define === "function" && define.amd) { |
|
// AMD. Register as an anonymous module. |
|
define(["underscore","backbone"], function(_, Backbone) { |
|
// Use global variables if the locals is undefined. |
|
return factory(_ || root._, Backbone || root.Backbone); |
|
}); |
|
} else { |
|
// RequireJS isn't being used. Assume underscore and backbone is loaded in <script> tags |
|
factory(_, Backbone); |
|
} |
|
}(this, function(_, Backbone) { |
|
|
|
var oldFind = Backbone.LocalStorage.prototype.find; |
|
var oldFindAll = Backbone.LocalStorage.prototype.findAll; |
|
var oldCreate = Backbone.LocalStorage.prototype.create; |
|
|
|
var localStorageSetup = function(model) { |
|
return model != null && (model.localStorage || (model.collection && model.collection.localStorage)); |
|
} |
|
|
|
var saveModel = function(model, options) { |
|
options = options ? _.clone(options) : {}; |
|
|
|
var parse = model.parse; |
|
model.parse = function(resp, options) { |
|
return parse.call( |
|
model, |
|
oldFind.call(localStorageSetup(model), model), |
|
options |
|
); |
|
} |
|
|
|
var success = options.success; |
|
var error = options.error; |
|
|
|
options.success = function(model, resp, options) { |
|
_.each(model.getRelations(), function(relation) { |
|
var relationModel = model.get(relation.key); |
|
|
|
if (relationModel != null && localStorageSetup(relationModel)) { |
|
if (options.nested && !relation.options.isAutoRelation) |
|
saveToLocalStorage(relationModel, _.omit(options, ['success', 'error'])); |
|
|
|
if (options.parents && relation.options.isAutoRelation) |
|
saveToLocalStorage(relationModel, _.omit(options, ['success', 'error'])); |
|
} |
|
}); |
|
model.parse = parse; |
|
if (success) success(model, resp, options); |
|
} |
|
|
|
options.error = function(model, xhr, options) { |
|
model.parse = parse; |
|
if (error) error(model, xhr, options); |
|
} |
|
|
|
if (options.relatedOnly == null || !options.relatedOnly){ |
|
model.save({}, _.extend({ |
|
local: true, |
|
server: false, |
|
silent: true, |
|
validate: true |
|
}, options)); |
|
} |
|
else{ |
|
delete options.relatedOnly; |
|
options.success(model, null, options); |
|
} |
|
}; |
|
|
|
var saveToLocalStorage = function(model, options) { |
|
if (model instanceof Backbone.Model) { |
|
saveModel(model, options); |
|
} else if (model instanceof Backbone.Collection) { |
|
var length, i, collection = model; |
|
collection.each(function(m){ saveModel(m, options); }); |
|
} |
|
}; |
|
|
|
// Override 'Backbone.sync' to default to localSync, |
|
// the original 'Backbone.sync' is still available in 'Backbone.ajaxSync' |
|
Backbone.sync = function(method, model, options) { |
|
options = options ? _.clone(options) : {}; |
|
|
|
var hasLocalStorageSetup = localStorageSetup(model) != null; |
|
|
|
if (options.local == null) |
|
options.local = hasLocalStorageSetup |
|
else if (options.local) { |
|
if (!hasLocalStorageSetup) { |
|
throw new Error('Must specify a localStorage attribute on the model or collection in order to use localStorage sync'); |
|
} |
|
} |
|
|
|
if (options.server == null) |
|
options.server = true |
|
|
|
if (!options.local && !options.server) { |
|
throw new Error('Must pass at least one of local=true or server=true in options'); |
|
} |
|
|
|
if (options.local) { |
|
var date = new Date(); |
|
|
|
//if this is a new model, then ensure the links from the parent relations |
|
//have been setup first |
|
if (method === 'create' && model.isNew && model.isNew()){ |
|
//at this stage the model will not have model.id set, only the id |
|
//attribute |
|
model.set(model.idAttribute, Backbone.guid(), {silent:true}); |
|
model.set({ |
|
'created_at': date, |
|
'updated_at': date |
|
}, {silent: true}); |
|
} |
|
|
|
if (method === 'update') |
|
model.set('updated_at', date, {silent: true}); |
|
|
|
// Sync takes: success(model, resp, options), which is passed in from |
|
// backbone fetch, save, destroy |
|
// |
|
// Sync internally then wraps that success handler in success(resp) which |
|
// it passes to $.ajax |
|
var success = options.success, sync = this |
|
|
|
// --- LOCAL SUCCESS --- |
|
options.success = function(model, resp, options) { |
|
|
|
// don't validate the parent models, because they may not have all of |
|
// their requirements met yet |
|
var fixParents = function(model) { |
|
saveToLocalStorage(model, { |
|
nested: false, |
|
parents: true, |
|
relatedOnly: true, |
|
validate: false |
|
}); |
|
}; |
|
|
|
if (options.server) { |
|
if (options.successLocal) options.successLocal(model, resp, options); |
|
|
|
// omit nested keys from data returned by localStorage sync, so when |
|
// passing to success to reload the returned data back into the model, |
|
// the nested models aren't broken |
|
var parse; |
|
if (method === 'create' || method === 'update'){ |
|
parse = model.parse; |
|
|
|
model.parse = function(resp, options) { |
|
resp = resp ? _.clone(resp) : {}; |
|
resp = _.omit(resp, _.map( |
|
model.getRelations(), |
|
function(relation){ return relation.key } |
|
)); |
|
|
|
return parse(resp, options); |
|
} |
|
} |
|
|
|
// don't let backbone set the id - it needs to stay null in order for |
|
// isNew to work and for the ajax request to send the correct REST |
|
// request |
|
var id = model.id; |
|
if(success != null) success(model, resp, _.extend({mode:'local'}, options)); |
|
if (id == null) model.id = null; |
|
|
|
if (method === 'create' || method === 'update') { |
|
model.parse = parse; |
|
|
|
if(options.fixParents == null || options.fixParents) |
|
fixParents(model); |
|
|
|
// don't send nested keys or any nested data to the server |
|
options.attrs = _.omit(model.toJSON(options), ['dirty'].concat(_.map( |
|
model.getRelations(), |
|
function(relation){ |
|
if (!relation.options.isAutoRelation && !relation.options.postToServer) |
|
return relation.key |
|
} |
|
))); |
|
} |
|
|
|
// --- SERVER SUCCESS --- |
|
options.success = function(model, resp, options) { |
|
// Backbone success handler: |
|
// |
|
// calling this success will populate any data coming back from the |
|
// server into the model/collection |
|
if(success != null) success(model, resp, _.extend({mode:'server'}, options)); |
|
|
|
if (method === 'create' || method === 'update') |
|
model.unset('dirty', {silent: true}); |
|
|
|
//don't recursively fix parent ids after doing a fetch from the server |
|
//way too slow and unnesseary |
|
saveToLocalStorage(model, { |
|
nested: true, |
|
fixParents: false |
|
}); |
|
|
|
if (options.successServer) options.successServer(model, resp, options); |
|
}; |
|
|
|
return Backbone.ajaxSync.apply(sync, [method, model, options]); |
|
} |
|
else { |
|
if(success != null) success(model, resp, _.extend({mode:'local'}, options)); |
|
if((method === 'create' || method === 'update') && (options.fixParents == null || options.fixParents)) |
|
fixParents(model); |
|
} |
|
}; |
|
|
|
if (options.server && (method === 'create' || method === 'update')) |
|
model.set('dirty', true, {silent:true}); |
|
|
|
return Backbone.localSync.apply(this, [method, model, options]); |
|
} |
|
|
|
else if (options.server) { |
|
if (method === 'create' || method === 'update') { |
|
// don't send nested keys or any nested data to the server |
|
options.attrs = _.omit(model.toJSON(options), ['dirty'].concat(_.map( |
|
model.getRelations(), |
|
function(relation){ |
|
if (!relation.options.isAutoRelation) |
|
return relation.key |
|
} |
|
))); |
|
} |
|
|
|
return Backbone.ajaxSync.apply(this, [method, model, options]); |
|
} |
|
}; |
|
|
|
// override constructor, so we can capture a class to assist with fetching the |
|
// nested models from localStorage |
|
var oldConstructor = Backbone.LocalStorage; |
|
Backbone.LocalStorage = window.Store = function(name, relations) { |
|
this.relations = relations; |
|
oldConstructor.apply(this, [name]); |
|
}; |
|
|
|
// override default implementation of find and findAll |
|
// our version will return nested data for a given model |
|
_.extend(Backbone.LocalStorage.prototype, oldConstructor.prototype, { |
|
create: function(model) { |
|
var out = oldCreate.call(this, model); |
|
|
|
// clear out id, so it stays as 'new'. We can set the id once the model has |
|
// been synced to the server |
|
model.id = null; |
|
|
|
return out; |
|
}, |
|
|
|
find: function(model) { |
|
var data = this.jsonData(this.localStorage().getItem(this.name+"-"+model.id)); |
|
|
|
if (this.relations != null && data != null) { |
|
_.each(this.relations, function(relation) { |
|
var ids = data[relation.key]; |
|
var store, nested; |
|
|
|
if (relation.collectionType != null) |
|
store = relation.collectionType.prototype.localStorage; |
|
|
|
else if (relation.relatedModel != null) |
|
store = relation.relatedModel.prototype.localStorage; |
|
|
|
if (store == null) |
|
return; |
|
|
|
if(ids == null) |
|
return; |
|
|
|
if(_.isArray(ids)) |
|
nested = []; |
|
else { |
|
ids = [ids]; |
|
nested = null; |
|
} |
|
|
|
_.each(ids, function(id) { |
|
if(_.isArray(nested)) |
|
nested.push(store.find({id:id})); |
|
else |
|
nested = store.find({id:id}); |
|
}); |
|
|
|
data[relation.key] = nested; |
|
}); |
|
} |
|
|
|
return data; |
|
}, |
|
|
|
findAll: function() { |
|
return _(this.records).chain() |
|
.map(function(id){ |
|
return this.find({id:id}); |
|
}, this) |
|
.compact() |
|
.value(); |
|
} |
|
}); |
|
|
|
Backbone.guid = function(){ |
|
// Generate four random hex digits. |
|
function S4() { |
|
return (((1+Math.random())*0x10000)|0).toString(16).substring(1); |
|
}; |
|
|
|
// Generate a pseudo-GUID by concatenating random hexadecimal. |
|
function guid() { |
|
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); |
|
}; |
|
|
|
return guid(); |
|
} |
|
|
|
return Backbone.LocalStorage; |
|
})); |