Example code in /example
folder: a simple GitHub Gist browser with a GitHub
API compatibility layer. Live example.
ThickM is a simplistic library for AngularJS which takes a class based approach to building model layers communicating with REST services. This is perfect for building rich ("thick"), extendable models with domain logic in the form of properties, methods and types.
ThickM is based on a belief that much of the logic for an application often is best placed in the model, and that they often are stored at and retrieved from a REST API service. It also wants implementation details for any such API to be based on code rather than configuration, for easier testing and debugging and for more flexibility.
This leads to simpler controllers, easier and fewer redundant tests and an easier to maintain codebase.
ThickM doesn't try to be a drop-in solution for every API, because APIs, applications, uses and technologies are different.
Wouldn't it be cool to have models like this?
Flight.query({from: 'New York', to: 'London'}).then(function(flights) {
console.log(flights.length); // outputs 8
console.log(flights.getAvailableSeats()); // outputs 324
// ...
var fastestFlight = fligths.getFastest();
console.log(fastestFlight instanceof Flight); // outputs true
fastestFligth.book(2); // books two seats
});
Think of ThickM as the bottom layer in a layer based model layer. Say layer one more time for good measure. The layers are:
- Your models
- API specific compatibility layer
- ThickM
Let's go though them from the top down:
These are the application or domain specific models for your application. They implement stuff like users, groups, cars, trees or any other entity you might need. They typically consist of one class per model, implemented in JavaScript, and can be as minimal as a couple of lines of code, or as big and complex as you want.
angular.module('myApp.model.plane')
.factory('Plane', function PlaneFactory(MyApiModel) {
function Plane(data) {
MyApiModel.call(this, data); // call parent constructor
}
MyApiModel.extend(Plane); // extend parent class
Plane.prototype._modelName = 'planes'; // goes in the URLs
// Possibly overwrite or add methods here ...
return Plane;
});
If two or more of the models share logic, it may make sense to create a common superclass which they both inherit from.
It may also make sense to create a specialized collection class for the model or models, which inherits from the API compatibility layer's.
The API compatibility layer specifies how your API works: how meta data is described and organized, where items are located in a response, implementation of support for technologies such as etags and so on.
It can also have its own logic and methods, which makes sense for the API or usage in question, or specialized error handling.
Compatibility is created by overwriting properties or methods of the
ThickModel
superclass.
A full implementation of an API compatibility layer with etags can be found at angular-evening, a library implemented for APIs created with the Python library Eve.
/* These classes implement a minimal API compatibility layer, which can be
* specific to your app or a stand-alone library facilitating apps for the
* kind of API you're using.
*
* This implementation only sets models' id field to '_id', and specifies that
* items in query responses are located in '_items', while meta data is in
* '_meta'.
*/
/**
* MyApiModel
*/
angular.module('myApi.model')
.factory('MyApiModel', function MyApiModelFactory(ThickModel,
MyApiModelCollection) {
function MyApiModel(data) {
this._primaryField = '_id'; // set 'id' field of models
ThickModel.call(this, data); // call parent constructor
}
ThickModel.extend(MyApiModel); // extend parent class
MyApiModel._collectionClass = MyApiModelCollection; // set collection class
MyApiModel._modelName = 'mymodel'; // for URLs
MyApiModel._baseUrl = '/api/v1/'; // for URLs, can also be set in API layer
// Possibly overwrite or add methods here ...
return MyApiModel;
});
/**
* MyApiModelCollection
*/
angular.module('myApi.collection')
.factory('MyApiModelCollection',
function MyApiModelCollection(ThickModelCollection) {
function MyApiModelCollection() {
ThickModelCollection.apply(this, arguments); // call parent constructor
}
ThickModelCollection.extend(MyApiModelCollection); // extend parent class
MyApiModelCollection._itemsField = '_items'; // set where the array of items
// (which will be converted to
// model instances) are
MyApiModelCollection._metaField = '_meta'; // set where resource meta data
// such as total available items
// and pagination is located
// Possibly overwrite or add methods here ...
return MyApiModelCollection;
});
The ThickM layer is at the bottom, consisting of the classes ThickModel
and
ThickModelCollection
, superclasses for models and model collections,
respectively. They are responsible for communication using $http
, with methods
like get()
, query()
, save()
and delete()
.
- Using bower:
bower install angular-thickm --save
. - Manually using either raw or minified versions in the
dist
folder.
There are no dependencies to this project other than AngularJS >= 1.2 (not tested for earlier verions).
Add thickm
to your application dependencies:
angular.module('myApp', ['thickm']);
Create an API compatibility layer, as shown in the API compatibility section above, subclassing ThickM's classes.
angular.module('myApp')
.factory('MyApiModel', function MyApiModelFactory(ThickModel,
MyApiModelCollection) {
function MyApiModel(data) {
// ...
}
MyApiModel._collectionClass = MyApiModelCollection;
// ...
return MyApiModel;
});
angular.module('myApp')
.factory('MyApiModelCollection',
function MyApiModelCollection(ThickModelCollection) {
function MyApiModelCollection() {
// ...
}
// ...
return MyApiModelCollection;
});
Then implement your models as shown in the "Your models" section above, subclassing your API compatibility section.
(Given a model named Plane
)
Fetch a collection of items:
Plane.query().then( ... ); // no parameters
Plane.query({make: 'airbus'}).then( ... ); // with parameters
Fetch one item by primary field:
Plane.get(89); // fetches plane with id 89
Create a new item and save it
var plane = new Plane({make: 'airbus', seats: 380});
plane.save();
Edit and save an item:
Plane.get(89).success(function(plane) {
plane.seats = 240;
plane.save();
});
Delete an item:
// ...
plane.delete()
These are the full list of methods and properties for the classes. Not all need to be used or overwritten.
Subclasses Array
and behaves like one: can be indexed or iterated over, and
has properties like length
.
ThickModelCollection._itemsField
Decides what field in the response holds the array of items. Default isnull
, indicating that the response is in itself an array.ThickModelCollection._metaField
Decides what field in the response holds meta data, like the total number of items available and pagination.ThickModelCollection.itemsFromResponse(cls, response)
Given a model class (subclass ofThickModel
) and a response from$http
, create an array of instances of the model class.ThickModelCollection.metaFromResponse(cls, response)
Given a model class and a response from$http
, create a meta data object with information about the total number of items available and pagination.ThickModelCollection.build(cls, response)
Given a model class and a response from$http
, create an instance of theThickModelCollection
class (or a subclass).ThickModelCollection.extend(cls)
Extend a subclass, e.g.MyApiModelCollection.extend(ThickModelCollection);
ThickModelCollection.toArray()
Return an array which passes Angular'sangular.isArray()
.
ThickModel(data)
Copies all properties ofdata
to the new instance (constructor).ThickModel.prototype._modelName
Holds the model name for the class, for building URLs, e.g.planes
.ThickModel.prototype._baseUrl
Holds the base URL for this model, e.g.http://myapp.com/api/v1/
.ThickModel.prototype._primaryField
The name of the field of models to look them up by when building URLs, e.g.id
.ThickModel._collectionClass
The class to use for collections of this model, default isThickModelCollection
. Use a reference to the class, not its name.ThickModel.build(data)
Factory method. Usesdata
object to create a instance of the model. Returns an instance ofThickModel
.ThickModel.prototype.getCollectionUrl()
Returns the collection URL as a string, based on the base URL set in_baseUrl
and the_modelName
.ThickModel.prototype.getModelUrl()
Returns the model URL, i.e. the URL used to fetch that instance, based ongetCollectionUrl()
and_primaryField
.ThickModel.transformCollectionResponse(response)
Transform a collection response into a model collection (ThickModelCollection
or the collection model given by_collectionClass
) instance. Returns an instance.ThickModel.transformItemResponse(response)
Transform an item response into a model instance. Returns an instance.ThickModel.prototype.transformItemRequest(headers)
Transform a request sent to the API, possibly by altering theheaders
object. Returns an object representing the instance, the default isthis
.ThickModel.queryUrl(url[, params])
Query the specific URL for a collection of items, usingparams
as additional parameters in the URL (see$http
documentation). Returns a promise resolving to an instance of the collection class.ThickModel.query([params])
Query to the URL given bygetCollectionUrl()
for a collection of items. Returns promise resolving to an instance of the collection class.ThickModel.getUrl(url[, params])
Get an item from the specific URL, usingparams
as additional parameters in the URL (see$http
documentation). Returns a promise resolving to an instance ofThickModel
.ThickModel.get(id[, params])
Get an item with the specificid
(primary field), usingparams
as additional parameters in the URL. Returns a promise resolving to an instance ofThickModel
.ThickModel.prototype.isNew()
Returns a boolean describing if the instance is fetched from the API or not. Default decides this based on whether itsid
(primary field) is set.ThickModel.prototype.update(data)
Overwrite the instance's data with new data form thedata
object.ThickModel.prototype.save()
Either POSTs or PUTs the instance to the URL given bygetCollectionUrl()
orgetModelUrl
, respectively, based on whether the object is "new" or not, given byisNew()
. Returns a promise resolving to an instance of the model based on data given from the API, and also updates the instance from which is was called with this data.ThickModel.prototype.delete()
DELETEs the model by a request to the URL given bygetModelUrl()
if the object is not new, or no-ops if it is. Either way, it returns a promise resolving to an empty object.ThickModel.extend(cls)
Extend a subclass, e.g.MyApiModel.extend(ThickModel);
See also the /example
folder.
Overwriting or creating new methods:
// Extending a class method
// Pull in the '_links' field
MyModelCollection.build = function(cls, response) {
// call the parent method
var rc = ThickModelCollection.build.call(this, cls, response);
// extend the returned value
rc._links = response.data._links;
return rc;
};
// Creating a new instance method
// See if there is more on the server
MyModelCollection.prototype.hasMore = function() {
return angular.isDefined(this._links && this._links.next);
};
- angular-evening for the Python REST framework Eve
Install
npm install && bower install
Build
grunt
Test
npm test