Skip to content

Instantly share code, notes, and snippets.

@asgeo1
Last active February 27, 2018 14:58
Show Gist options
  • Save asgeo1/6774805 to your computer and use it in GitHub Desktop.
Save asgeo1/6774805 to your computer and use it in GitHub Desktop.
Alternative to backbone-offline.

Works with nested backbone models. Can sync the whole nested collection at once, or just an individual model.

This is just something I hacked together. The goal was to replace it with an updated backbone-offline once that library supports nested models.

Requires:

  • backbone 0.9.10
  • backbone.localStorage 1.0
  • backbone-relational 0.7.0

Probably requires changes for a newer backbone or backbone-relational.

Probably has various issues / limitations - but worked for the use-cases that I was testing.

Usage:

class MyApp.MyCollection extends Backbone.Collection
  model: MyApp.MyModel
  url: => '/api/v1/mymodel'
  localStorage: new Backbone.LocalStorage(
    'mymodels',
    MyApp.MyModel.prototype.relations
  )

class MyApp.MyModel extends Backbone.Model
  relations: [{
    type: Backbone.HasMany
    key: 'anothermodels'
    relatedModel: MyApp.AnotherModel
    collectionType: MyApp.AnotherCollection
    includeInJSON: 'id'
    cascadeDelete: true
    reverseRelation:
      type: Backbone.HasOne
      key: 'mymodel'
      includeInJSON: 'id'
  }]

class MyApp.AnotherCollection extends Backbone.Collection
  model: MyApp.AnotherModel
  url: => '/api/v1/anothermodel'
  localStorage: new Backbone.LocalStorage(
    'anothermodels',
    MyApp.AnotherModel.prototype.relations
  )

class MyApp.AnotherModel extends Backbone.Model
  relations: [
    ...
  ]

When you call backbone save() there are some new options you can pass:

  • local - should the model be persisted to localStorage? (default true)
  • server - should the modele be persisted to the server? (default true)
  • successLocal - callback that occurs after data is persisted to localStorage
  • successServer - callback that occurs after data is persisted to the server
/**
* 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;
}));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment