- Robust relational data handling
- Absolutely no view layer
- Modularize via CommonJS
- No reliance on jQuery for ajax
- No reliance on Underscore or lodash for functions
- Promises, not callbacks
- Emphasis on mixins
Immunid is distributed with npm and can be imported as CommonJS modules.
npm install immunid --save
All models must be created or retrieved through the store. The store is what allows relational behavior, caching, and identity mapping. It is strictly possible to create a new instance of a model directly, but it will not be able to retrieve any associations.
// create a model class
var Post = Model.extend({
path: function() {
return '/posts';
}
});
// configure communication with your server
var adapter = new Adapter({
headers: { 'Content-Type': 'application/json' },
host: '/api'
});
// create a store with an adapter and a mapping
// the keys of the mapping become namespaces for your model classes
// e.g. 'Post' -> 'posts'
var store = new Store(adapter, {
'Post': Post
});
// Performs a GET to `/api/posts/1`
store.find('posts', 1).then(function(post) {
// do something with the post
});
All data returned from the GET
request to /posts/1
will be parsed into the
appropriate location within the store and wrapped in a corresponding model
object. This allows the Post
to synchronously retrieve associated data. This
mechanism relies on embedded ids within the parent record:
post.get('comment_ids'); // [1, 2, 3]
post.comments().all(); // No request, returns an array of comments
The path used for a particular model or relation can be customized within the
model itself. There is no basePath
or automatic id appending.
var Comment = Model.extend({
path: function(post) {
return '/post/' + post.id + '/comments';
}
});
Records instantiated through the store expose some simple persistence methods.
model.save(); // POST or PUT request to model.path()
model.reload(); // GET request to model.path()
model.destroy(); // DELETE request to model.path()
Persisting models with associations does not create, update, or destroy any of the associated models.
Records can be added to or removed from the store without making a call to
#find
or #destroy
:
var store = new Store(adapter, {
'Post': Post
});
// instantiates a Post model with existing attributes
var post = store.add('posts', { id: 42, body: 'Lorem ipsum' });
// removes a Post model that has already been destroyed or should not be used
store.remove(post);
Calling #add
with new attributes for a model that already exists updates the
existing model attributes instead of overwriting them:
store.add('posts', { id: 42, body: 'Lorem ipsum' });
store.add('posts', { id: 42, group: 'beta' });
var post = store.get('posts', 42);
post.get('body'); // returns 'Lorem ipsum'
post.get('group'); // return 'beta'
During testing or in certain production situations you may want clear all, or
part of the store. For that, there is clear
:
store.clear('posts'); // all models in the posts namespace
store.clear(); // all models in all namespaces
Clearing is only a local operation and doesn't make any external requests.
- There is no
belongsTo
relation. OnlyhasOne
orhasMany
currently. - Relation names are always singular.
var Project = Model.extend({
account: Relation.hasOne('account'),
missions: Relation.hasMany('mission'),
screeners: Relation.hasMany('screener'),
memberships: Relation.hasMany('membership')
});
project.account().get(); // Returns instance of loaded account
project.memberships().all(); // Returns array of loaded memberships
The store emits events under namespaces when model instances are changed. Event names are always past tense to indicate the model is ready for use:
var adapter = new Adapter();
var Tag = Model.extend({});
var store = new Store(adapter, { Tag: Tag });
function handleChangeTag(tag) {
// tag is the model instance that has changed
// `this` value is implicitly bound to the store
}
var myTag = store.get('tags', 42).then(function(tag) {
store.on('tags:changed', handleChangeTag); // subscribe
});
myTag.set({ name: 'banana' }); // calls handleChangeTag
myTag.unset('name'); // calls handleChangeTag
myTag.clear(); // calls handleChangeTag
store.off('tags:changed', handleChangeTag); // unsubscribe
Set the event handler's this
value with the third argument to on
:
var context = { model: null };
store.on('model:changed', handleChange, context);
function handleChange(model) {
// `this` value is the `context` object
this.model = model;
}
There are a few ways to unsubscribe event handlers:
// unsubscribe `handleChange` handler
store.off('tags:changed', handleChange);
// unsubscribe all `tags:changed` handlers
store.off('tags:changed', null, null);
// unsubscribe all handlers from `tags` namespace
store.off('tags', null, null);
The store also emits events when parsing a response from the server:
store.find('tags'); // store emits 'tags:fetched'
store.find('tags', 42); // store emits 'tags:fetched'
var tag = store.build('tags', { name: 'apple' });
tag.save(); // store emits 'tags:created'
tag.reload(); // store emits 'tags:reloaded'
tag.set({ name: 'banana' }).save(); // store emits 'tags:updated'
tag.destroy(); // store emits 'tags:destroyed'
The payload for fetched
, created
, reloaded
, and updated
events will be
an array of one or more models. The payload for destroyed
events will be a
single model.
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request
Released under the MIT license. See LICENSE for details.