Skip to content

Instantly share code, notes, and snippets.

@ryan-scott-dev
Created April 22, 2014 00:27
Show Gist options
  • Save ryan-scott-dev/11161302 to your computer and use it in GitHub Desktop.
Save ryan-scott-dev/11161302 to your computer and use it in GitHub Desktop.
// 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