Created
April 22, 2014 00:27
-
-
Save ryan-scott-dev/11161302 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// A plugin to add Ajax communication with a RESTful JSON API. | |
// | |
// Depends on JSON.stringify() and jQuery's $.ajax() | |
// | |
// Expects models to have toJSON and fromJSON methods. | |
// ============================================================================ | |
// Define the maria.Repository class. The base class for Ajax/JSON Repositories. | |
maria.Repository = function() { | |
this._links = {}; | |
}; | |
maria.Repository.prototype.getModel = function() { | |
return this._model; | |
}; | |
// only intended to be called by the model setRepository method | |
maria.Repository.prototype.setModel = function(model) { | |
this._model = model; | |
}; | |
maria.Repository.prototype.getLinks = function() { | |
return this._links; | |
}; | |
maria.Repository.prototype.setLinks = function(links) { | |
this._links = links; | |
}; | |
maria.Repository.prototype.addLink = function(rel, link) { | |
this.reset(); | |
this.addNewLink(rel, link); | |
}; | |
maria.Repository.prototype.addNewLink = function(rel, link) { | |
this.getLinks()[rel] = link; | |
}; | |
maria.Repository.prototype.addLinks =function(links) { | |
this.reset(); | |
for (var i = 0, ilen = links.length; i < ilen; i++) { | |
this.addNewLink(links[i].rel, links[i]); | |
} | |
}; | |
maria.Repository.prototype.resetLinks = function() { | |
this.setLinks({}); | |
}; | |
maria.Repository.prototype.reset = function() { | |
this.resetLinks(); | |
}; | |
maria.Repository.prototype.hasLink = function(rel) { | |
return (typeof this.getLinks()[rel] !== 'undefined'); | |
}; | |
maria.Repository.prototype.getLink = function(rel) { | |
return this.getLinks()[rel]; | |
}; | |
maria.Repository.prototype.getLinkHref = function(rel) { | |
return this.getLinks()[rel]['href']; | |
}; | |
maria.Repository.prototype.getRootURL = function() { | |
throw new Error('maria.Repository.prototype.getRootURL: Abstract method. Must be implemented in subclass.'); | |
}; | |
maria.Repository.prototype.getLoadURL = function() { | |
var model = this.getModel(); | |
if (model.isNew()) { | |
throw new Error('maria.Repository.prototype.getLoadURL: Cannot create a REST URL for this new resource.'); | |
} | |
// RS - Was previously working for set repositories | |
// return this.getRootURL() + '/' + encodeURIComponent(model.getId()); | |
return this.getLinkHref('self'); | |
}; | |
maria.Repository.prototype.getCreateURL = function() { | |
// return this.getRootURL(); | |
return this.getLinkHref('create'); | |
}; | |
maria.Repository.prototype.getUpdateURL = function() { | |
// return this.getLoadURL(); | |
return this.getLinkHref('update'); | |
}; | |
maria.Repository.prototype.getDeleteURL = function() { | |
// return this.getLoadURL(); | |
return this.getLinkHref('delete'); | |
}; | |
// transform the server's response JSON to JSON that | |
// can be sent back to the model for its fromJSON method. | |
maria.Repository.prototype.deserialize = function(json) { | |
return json; | |
}; | |
// massage the JSON (likely from the model's toJSON method) | |
// to the format the server wants to recieve in the request body | |
maria.Repository.prototype.serialize = function(json) { | |
return json; | |
}; | |
maria.Repository.prototype.load = function(options) { | |
options = options || {}; | |
$.ajax({ | |
url: this.getLoadURL(), | |
context: this, | |
data: options.data, | |
dataType: "json", | |
success: function(data, status, jqXHR) { | |
data = this.deserialize(data); | |
if (options.success) { | |
options.success(data); | |
} | |
}, | |
error: function() { | |
if (options.failure) { | |
options.failure.apply(options, arguments); | |
} | |
} | |
}); | |
}; | |
maria.Repository.prototype.create = function(options) { | |
options = options || {}; | |
$.ajax({ | |
type: 'POST', | |
url: this.getCreateURL(), | |
contentType: 'application/json', | |
data: JSON.stringify(this.serialize(this.getModel().toJSON())), | |
dataType: "json", | |
context: this, | |
success: function(data, status, jqXHR) { | |
data = this.deserialize(data); | |
if (options.success) { | |
options.success(data); | |
} | |
}, | |
error: function() { | |
if (options.failure) { | |
options.failure.apply(options, arguments); | |
} | |
} | |
}); | |
}; | |
maria.Repository.prototype.update = function(options) { | |
options = options || {}; | |
$.ajax({ | |
type: 'PUT', | |
url: this.getUpdateURL(), | |
contentType: 'application/json', | |
data: JSON.stringify(this.serialize(this.getModel().toJSON())), | |
dataType: "json", | |
context: this, | |
success: function(data, status, jqXHR) { | |
data = this.deserialize(data); | |
if (options.success) { | |
options.success(data); | |
} | |
}, | |
error: function() { | |
if (options.failure) { | |
options.failure.apply(options, arguments); | |
} | |
} | |
}); | |
}; | |
maria.Repository.prototype.save = function(options) { | |
if (this.getModel().isNew()) { | |
this.create(options); | |
} | |
else { | |
this.update(options); | |
} | |
}; | |
maria.Repository.prototype['delete'] = function(options) { | |
options = options || {}; | |
if (this.getModel().isNew()) { | |
// the server never new about this model anyway | |
if (options.success) { | |
options.success(); | |
} | |
return; | |
} | |
$.ajax({ | |
type: 'DELETE', | |
url: this.getDeleteURL(), | |
context: this, | |
contentType: 'application/json', | |
dataType: 'json', | |
success: function() { | |
if (options.success) { | |
options.success(); | |
} | |
}, | |
error: function() { | |
if (options.failure) { | |
options.failure.apply(options, arguments); | |
} | |
} | |
}); | |
}; | |
maria.Repository.subclass = function(namespace, name, options) { | |
options = options || {}; | |
var properties = options.properties = options.properties || {}; | |
if (!Object.prototype.hasOwnProperty.call(properties, 'getRootURL') && | |
Object.prototype.hasOwnProperty.call(options, 'rootURL')) { | |
options.properties.getRootURL = function() { | |
if (null !== options.rootURL) | |
return options.rootURL; | |
return this.getLinkHref('self'); | |
}; | |
} | |
maria.subclass.call(this, namespace, name, options); | |
}; | |
// ============================================================================ | |
// Define the maria.SetRepository class. | |
maria.Repository.subclass(maria, 'SetRepository', { | |
properties: { | |
getLoadURL: function() { | |
return this.getRootURL(); | |
}, | |
create: function() { | |
throw new Error('maria.SetRepository.prototype.create: unsupported.'); | |
}, | |
update: function() { | |
throw new Error('maria.SetRepository.prototype.create: unsupported.'); | |
}, | |
save: function() { | |
throw new Error('maria.SetRepository.prototype.save: unsupported.'); | |
}, | |
"delete": function() { | |
throw new Error('maria.SetRepository.prototype["delete"]: unsupported.'); | |
} | |
} | |
}); | |
maria.SetRepository.subclass = function() { | |
maria.Repository.subclass.apply(this, arguments); | |
}; | |
// ============================================================================ | |
// Monkey patch the maria.Model class. | |
// - - - | |
// RESTful APIs use resource ids in the URLs to identify the resource on | |
// the server. Therefore we give all models an "id" attribute. The id | |
// does not necessarily need to be a number. For example, it could be | |
// a string that even contains spaces. | |
maria.Model.prototype._id = null; | |
maria.Model.prototype.getId = function() { | |
return this._id; | |
}; | |
maria.Model.prototype.setId = function(id) { | |
if ((this._id !== null) && | |
(this._id !== undefined) && | |
(id !== this._id)) { | |
throw new Error('maria.Model.prototype.setId: id was already set to "'+this._id+'" and so cannot be set again to a different value of "'+id+'".'); | |
} | |
else if (this._id !== id) { | |
this._id = id; | |
this.dispatchEvent({type: 'change'}); | |
} | |
}; | |
// If a model has been saved to the server then the server will | |
// have assigned an id and so the model will have an id. | |
maria.Model.prototype.isNew = function() { | |
return this.getId() === null; | |
}; | |
// - - - | |
// If any Ajax operation (i.e. GET, POST, PUT, DELETE) is in progress | |
// then the model will be in a "loading" state. | |
maria.Model.prototype._loading = false; | |
maria.Model.prototype.isLoading = function() { | |
return this._loading; | |
}; | |
maria.Model.prototype.setLoading = function(loading) { | |
loading = !!loading; | |
if (loading !== this._loading) { | |
this._loading = loading; | |
this.dispatchEvent({type: 'change'}); | |
} | |
}; | |
maria.Model.prototype.canLoad = function() { | |
return this.getRepository().hasLink('self'); | |
}; | |
// - - - | |
// A model uses a repository object to take care of communciation. | |
// This means a model does not need to know about Ajax. The model | |
// doesn't even know if the data is stored in the browser's cookies, | |
// localStorage, on the server via Ajax, or on a server via JSONP. | |
maria.Model.prototype.getDefaultRepositoryConstructor = function() { | |
return maria.Repository; | |
}; | |
maria.Model.prototype.getDefaultRepository = function() { | |
var constructor = this.getDefaultRepositoryConstructor(); | |
if (!constructor) { | |
console.error('Could not find repository for model "' + this.__name__ + '".'); | |
} | |
return new constructor(); | |
}; | |
maria.Model.prototype.getRepository = function() { | |
if (!this._repository) { | |
this.setRepository(this.getDefaultRepository()); | |
} | |
return this._repository; | |
}; | |
maria.Model.prototype.setRepository = function(repository) { | |
if (repository !== this._repository) { | |
if (this._repository) { | |
this._repository.setModel(null); | |
this._repository = null; | |
} | |
if (repository) { | |
this._repository = repository; | |
repository.setModel(this); | |
} | |
} | |
}; | |
maria.Model.prototype.load = function(options) { | |
options = options || {}; | |
var repository = this.getRepository(); | |
this.setLoading(true); | |
var thisC = this; | |
repository.load({ | |
success: function(data) { | |
thisC.setLoading(false); | |
if (data) { | |
thisC.fromJSON(data); | |
} | |
if (options.success) { | |
options.success(thisC); | |
} | |
}, | |
failure: function() { | |
thisC.setLoading(false); | |
if (options.failure) { | |
options.failure.apply(options, arguments); | |
} | |
}, | |
data: options.data | |
}); | |
}; | |
maria.Model.prototype.create = function(options) { | |
options = options || {}; | |
var repository = this.getRepository(); | |
this.setLoading(true); | |
var thisC = this; | |
repository.create({ | |
success: function(data) { | |
thisC.setLoading(false); | |
thisC.fromJSON(data); | |
if (options.success) { | |
options.success(thisC); | |
} | |
}, | |
failure: function() { | |
thisC.setLoading(false); | |
if (options.failure) { | |
options.failure.apply(options, arguments); | |
} | |
} | |
}); | |
}; | |
maria.Model.prototype.update = function(options) { | |
options = options || {}; | |
var repository = this.getRepository(); | |
this.setLoading(true); | |
var thisC = this; | |
repository.update({ | |
success: function(data) { | |
thisC.setLoading(false); | |
thisC.fromJSON(data); | |
if (options.success) { | |
options.success(thisC); | |
} | |
}, | |
failure: function() { | |
thisC.setLoading(false); | |
if (options.failure) { | |
options.failure.apply(options, arguments); | |
} | |
} | |
}); | |
}; | |
maria.Model.prototype.save = function(options) { | |
if (this.isNew()) { | |
this.create(options); | |
} | |
else { | |
this.update(options); | |
} | |
}; | |
maria.Model.prototype["delete"] = function(options) { | |
options = options || {}; | |
var repository = this.getRepository(); | |
this.setLoading(true); | |
var thisC = this; | |
repository['delete']({ | |
success: function(data) { | |
thisC.setLoading(false); | |
thisC.destroy(); | |
if (options.success) { | |
options.success(thisC); | |
} | |
}, | |
failure: function() { | |
thisC.setLoading(false); | |
if (options.failure) { | |
options.failure.apply(options, arguments); | |
} | |
} | |
}); | |
}; | |
// - - - | |
// Wrap the usual maria.Model.subclass so that a model has | |
// a conventional and overridable repository constructor. | |
maria.Model.subclass = (function() { | |
var original = maria.Model.subclass; | |
return function(namespace, name, options) { | |
options = options || {}; | |
var repositoryConstructor = options.repositoryConstructor; | |
var repositoryConstructorName = options.repositoryConstructorName || name.replace(/(Model|)$/, 'Repository'); | |
var properties = options.properties || (options.properties = {}); | |
if (!Object.prototype.hasOwnProperty.call(properties, 'getDefaultRepositoryConstructor')) { | |
properties.getDefaultRepositoryConstructor = function() { | |
return repositoryConstructor || namespace[repositoryConstructorName]; | |
}; | |
} | |
return original.call(this, namespace, name, options); | |
}; | |
}()); | |
// ============================================================================ | |
// Monkey patch the maria.SetModel class. | |
maria.SetModel.prototype.getDefaultRepositoryConstructor = function() { | |
return maria.SetRepository; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment