Easily query and update your JSON API using angular.js.
Inspired by ngResource, I wanted to create a service that can manage the data retrieved from an API and always return the same objects, so Angular will automatically update all your views.
I'll try to document as much of the Collection API as possible, but if you want to know what really happens, you should read the (well-documented) source code in src/model.coffee. Additionally, the tests may give you a nice overview of some the possibilities.
To describe an API endpoint, you instantiate a new instance of a Collection using the new method. (Please note, that you might want to call it using Collection['new'] if you need to support ECMAScript 3 in old browsers.)
Options:
name (string) for the collection. The URL may be derived from this (e.g. a collection called "Users" has the default URL "/users").
A config object with options like url, detailUrl and is_singleton (more info see below)
An extras object for additional API methods (for more info see below)
Collection Options
This option can be set when creating a collection:
url (string or function): The URL for the list resources (e.g. /users)
detailUrl (string or function): The URL for the detail resources (e.g. /users/42). Default is [list_url]/[entry.id]. See URL Formatting for information on how to change this.
baseUrl (string) to overwrite the global API base URL
is_singleton (boolean): Set to true when the resource is not a list of entries, but just a single data point, e.g. "/me" (as an alias for requesting the current user).
matchingCriteria (function): Specify how to match entries (e.g. to update an old representation). The default is to use entry.id. If you specify you own function, it should return an object that can be used in lodash's where method.
E.g., to use MongoDB's _id field as an identifier, use this a a matching function: function (item) {return {_id: item._id};}
Using API Data
Each Collection instance has the default CRUD methods available as all(), get({id}), create(data), update({id}, data) and destroy({id}).
Whereas create, update and destroy return promises, all and get return objects with special keys so they can be used directly in your view.
As you can see, the actual data is stored in the all property (or data for get and other calls). Your typical view would look like this:
<png-show="posts.$loading">Loading</p><png-show="posts.$error">Oh noes!</p><ulng-repeat="post in posts.all"><li>{{post.title}}</li></ul>
When API response is received, it will be used to update that object's properties, so that angular's dirty checking can detect a change and update your view automatically.
This has several advantages. Since EpicModel stores all API data in an internal cache, it can set the posts.all value to all the posts it has already cached initially. When the new list of posts is received, it can then update the internal cache, adding new entries and updating existing ones. These updates will immediately be reflected in your view.
Even better: Since the objects themselves are always the same ones, even if you have several views displayed each showing a filtered list of posts from this Collection, they will all be updated. You don't even have to use track by in your ng-repeat!
Adding Additional API Methods to a Collection
To add more methods to your collection, specify them as keys of the extras object when creating the collection. Their values can either be functions or objects.
Below, three different variants will be shown. For more information (e.g. onSuccess or onFail transforms), have a look at the extras tests or the extras implementation.
Add a Static Method
The easiest case: Each extras property that is a function will become static method of your collection.
var options = {};
var extras = {
isNew: function(msg) {returntrue;
}
};
var messages = Collection.new("Messages", options, extras);
messages.isNew({id: 21})
// => true
To make methods more powerful, they are bound to the collection configuration, i.e. this in your method will be the object with all configuration options set during the collection creation.
Please note that some helpful methods not documented in the configuration section above are also part of this object, e.g. config.getDetailUrl(entry) and config.Data = {get(), replace(), updateEntry(), removeEntry()}.
Add a New HTTP Call
You could use static methods to make HTTP calls, but EpicModel offers an easier alternative: Just specify an object with HTTP options.
var options = {};
var extras = {
markUnread: {
method: 'PUT',
url: '/messages/{id}/mark_unread',
params: {
skip_return: true
},
data: {
'read': false
}
}
};
var messages = Collection.new("Messages", options, extras);
var message = {id: 42};
var httpCall = messages.markUnread(message);
// => PUT /messages/42/mark_unread?skip_return=true
httpCall
.then(function(response) {
console.log(response.data.read);
// => false
})
.then(null, function(error) {
console.log(error.status);
// => e.g. 404
})
The only option you need to set is the HTTP method using method. If the URL cannot be guessed from the property key, you can overwrite it using url (cf. URL Formatting). Additionally, you can specify all the options $http can process.
Calling a method specified like this will return a promise that settles with the HTTP response, just like using $http.
URL Formatting
One of the clever things in EpicModel is how it allows you to set detail URLs.
When you specify a URL as a string, you can use curly braces to include some values from the entry you are requesting, e.g. /users/{id}-{name.first}_{name.last}.
When you specify a function, it will be called with the information about the entry you are requesting, the API's base URL and the list URL (from config.url). It should return string.
Global Configuration
You can inject the CollectionProvider into your module's config to set the following options:
EpicModel
Easily query and update your JSON API using
angular.js
.Inspired by ngResource, I wanted to create a service that can manage the data retrieved from an API and always return the same objects, so Angular will automatically update all your views.
var Messages = Collection.new("Messages", {url: "/messages"}); Messages.all() // => {Array all, $promise: {Function then}, ...} Messages.get({id: 42}) // => {Object data, $promise: {Function then}, ...}
Installation
Grab the source code, e.g. by using bower, which will fetch the latest release tag and all dependencies:
Include all dependencies and EpicModel in your build process or HTML file. (Choose one of
dist/model.js
,dist/model.min.js
orsrc/model.coffee
.)Add EpicModel as a dependency to the angular modules that use it:
angular.module('DemoApp', ['EpicModel']) .run(function (Collection) { console.log("Did we load EpicModel?", !!Collection); });
Dependencies
Usage
I'll try to document as much of the Collection API as possible, but if you want to know what really happens, you should read the (well-documented) source code in
src/model.coffee
. Additionally, the tests may give you a nice overview of some the possibilities.Creating a Collection Instance
To describe an API endpoint, you instantiate a new instance of a
Collection
using thenew
method. (Please note, that you might want to call it usingCollection['new']
if you need to support ECMAScript 3 in old browsers.)Options:
url
,detailUrl
and is_singleton (more info see below)Collection Options
This option can be set when creating a collection:
/users
)/users/42
). Default is[list_url]/[entry.id]
. See URL Formatting for information on how to change this.entry.id
. If you specify you own function, it should return an object that can be used in lodash'swhere
method. E.g., to use MongoDB's_id
field as an identifier, use this a a matching function:function (item) {return {_id: item._id};}
Using API Data
Each Collection instance has the default CRUD methods available as
all()
,get({id})
,create(data)
,update({id}, data)
anddestroy({id})
.Whereas
create
,update
anddestroy
return promises,all
andget
return objects with special keys so they can be used directly in your view.var Posts = Collection.new("Posts"); var posts = Posts.all(); // => { // all: undefined, // $promise: {then()}, // $loading: true, // $resolved: false, // $error: false // }
As you can see, the actual data is stored in the
all
property (ordata
forget
and other calls). Your typical view would look like this:<p ng-show="posts.$loading">Loading</p> <p ng-show="posts.$error">Oh noes!</p> <ul ng-repeat="post in posts.all"> <li>{{post.title}}</li> </ul>
When API response is received, it will be used to update that object's properties, so that angular's dirty checking can detect a change and update your view automatically.
This has several advantages. Since EpicModel stores all API data in an internal cache, it can set the
posts.all
value to all the posts it has already cached initially. When the new list of posts is received, it can then update the internal cache, adding new entries and updating existing ones. These updates will immediately be reflected in your view.Even better: Since the objects themselves are always the same ones, even if you have several views displayed each showing a filtered list of posts from this Collection, they will all be updated. You don't even have to use
track by
in yourng-repeat
!Adding Additional API Methods to a Collection
To add more methods to your collection, specify them as keys of the extras object when creating the collection. Their values can either be functions or objects.
Below, three different variants will be shown. For more information (e.g.
onSuccess
oronFail
transforms), have a look at the extras tests or the extras implementation.Add a Static Method
The easiest case: Each extras property that is a function will become static method of your collection.
var options = {}; var extras = { isNew: function (msg) { return true; } }; var messages = Collection.new("Messages", options, extras); messages.isNew({id: 21}) // => true
To make methods more powerful, they are bound to the collection configuration, i.e.
this
in your method will be the object with all configuration options set during the collection creation.Please note that some helpful methods not documented in the configuration section above are also part of this object, e.g.
config.getDetailUrl(entry)
andconfig.Data = {get(), replace(), updateEntry(), removeEntry()}
.Add a New HTTP Call
You could use static methods to make HTTP calls, but EpicModel offers an easier alternative: Just specify an object with HTTP options.
var options = {}; var extras = { markUnread: { method: 'PUT', url: '/messages/{id}/mark_unread', params: { skip_return: true }, data: { 'read': false } } }; var messages = Collection.new("Messages", options, extras); var message = {id: 42}; var httpCall = messages.markUnread(message); // => PUT /messages/42/mark_unread?skip_return=true httpCall .then(function (response) { console.log(response.data.read); // => false }) .then(null, function (error) { console.log(error.status); // => e.g. 404 })
The only option you need to set is the HTTP method using
method
. If the URL cannot be guessed from the property key, you can overwrite it usingurl
(cf. URL Formatting). Additionally, you can specify all the options $http can process.Calling a method specified like this will return a promise that settles with the HTTP response, just like using $http.
URL Formatting
One of the clever things in EpicModel is how it allows you to set detail URLs.
When you specify a URL as a string, you can use curly braces to include some values from the entry you are requesting, e.g.
/users/{id}-{name.first}_{name.last}
.When you specify a function, it will be called with the information about the entry you are requesting, the API's base URL and the list URL (from
config.url
). It should return string.Global Configuration
You can inject the
CollectionProvider
into your module'sconfig
to set the following options:angular.module('app', ['EpicModel']) .config(function (CollectionProvider) { CollectionProvider.setBaseUrl("http://localhost:3000"); });
License
MIT