Ember.Model
Introduction
Ember Model (EM) is a simple and lightweight model library for Ember. It intentionally supports a limited feature set. The main goal is to provide primitives on top of $.ajax that are required by Ember.
EM is still a work in progress, but it's flexible and stable enough to be used in production apps today. It was extracted out of an Ember app. Please see the issues section for a list of bugs and planned features.
Getting Started with Ember Model
Ember CLI
ember install ember-model
Bower
bower install ember-model --save
Need more help getting started? Join us in #ember-model on Freenode.
Features
- BYO$A (bring your own $.ajax)
- Relationships (hasMany/belongsTo)
- Focused on performance
- Automatic coalescing of multiple findById calls into a single findMany
- Fixtures
- Identity map (per class)
- Promises everywhere
- Customizable RESTAdapter
If you want more features than Ember Model provides, file an issue. Feature requests/contributions are welcome but the goal is to keep things simple and fast.
Example usage
Ember CLI / ES6 modules
// app/models/user.js
import { Model, attr, hasMany } from 'ember-model';
var User = Model.extend({
id: attr(),
name: attr(),
comments: hasMany("comment", {key: 'comment_ids'})
}).reopenClass({
url: "/users"
});
export default User;
// app/models/comment.js
import { Model, attr, hasMany } from 'ember-model';
var Comment = Model.extend({
id: attr(),
text: attr()
}).reopenClass({
url: "/comments"
});
export default Comment;
// create example
var newUser = this.store.createRecord('user', {name: "Erik"});
newUser.save(); // POST to /users
// hasMany example
var comments = newUser.get('comments');
comments.create({text: "hello!"});
comments.save(); // POST to /comments
// find & update example
this.store.find('user', 1).then(user => { // GET /users/1
user.set('name', 'Kris');
user.get('isDirty'); // => true
user.save(); // PUT /users/1
});
Globals
var attr = Ember.attr, hasMany = Ember.hasMany;
// Model definitions
App.User = Ember.Model.extend({
id: attr(),
name: attr(),
comments: hasMany("App.Comment", {key: 'comment_ids'})
});
App.User.url = "/users";
App.User.adapter = Ember.RESTAdapter.create();
App.Comment = Ember.Model.extend({
id: attr(),
text: attr()
});
App.Comment.url = "/comments";
App.Comment.adapter = Ember.RESTAdapter.create();
// create example
var newUser = App.User.create({name: "Erik"});
newUser.save(); // POST to /users
// hasMany example
var comments = newUser.get('comments');
comments.create({text: "hello!"});
comments.save(); // POST to /comments
// find & update example
var existingUser = App.User.find(1); // GET /users/1
existingUser.set('name', 'Kris');
existingUser.get('isDirty'); // => true
existingUser.save(); // PUT /users/1
Store API
Store#find(<type>)
- find all records by type, returns a promise
Store#find(<type>, <id>)
- find by primary key (multiple calls within a single run loop can coalesce to a findMany), returns a promise
Store#find(<type>, <object>)
- find query - object gets passed directly to your adapter, returns a promise
Store#createRecord(<type>, <object>)
- create a new record
Model API
Model.create
- create a new record
Model#save
- save or update record
Model#deleteRecord
- delete a record
Model#load(<id>, <object>)
- load JSON into the record (typically used inside adapter definition)
Model#toJSON
- serialize the record to JSON
Model.find()
- find all records
Model.find(<String|Number>)
- find by primary key (multiple calls within a single run loop can coalesce to a findMany)
Model.find(<object>)
- find query - object gets passed directly to your adapter
Model.fetch()
- find all records, returns a promise
Model.fetch(<String|Number>)
- find by primary key (multiple calls within a single run loop can coalesce to a findMany), returns a promise
Model.fetch(<object>)
- find query - object gets passed directly to your adapter, returns a promise
Model.load(<array>)
- load an array of model data (typically used when preloading / sideloading data)
Adapter API
Ember.Adapter = Ember.Object.extend({
find: function(record, id) {}, // find a single record
findAll: function(klass, records) {}, // find all records
findMany: function(klass, records, ids) {}, // find many records by primary key (batch find)
findQuery: function(klass, records, params) {}, // find records using a query
createRecord: function(record) {}, // create a new record on the server
saveRecord: function(record) {}, // save an existing record on the server
deleteRecord: function(record) {} // delete a record on the server
});
Attribute types
Attributes by default have no type and are not typecast from the representation provided in the JSON format.
Built in attribute types
Ember Model has built in Date
and Number
types. The Date
type will deserialize
strings into a javascript Date object, and will serialize dates into
ISO 8601 format. The Number
type will
cast into a numeric type on serialization and deserialization.
App.Post = Ember.Model.extend({
date: attr(Date),
comment_count: attr(Number)
});
Custom attribute types
To provide custom attribute serialization and deserialization, create an object that has serialize and deserialize functions, and pass it into the attr helper:
var Time = {
serialize: function(time) {
return time.hour + ":" + time.min;
},
deserialize: function(string) {
var array = string.split(":");
return {
hour: parseInt(array[0], 10),
min: parseInt(array[1], 10)
};
}
};
var Post = Ember.Model.extend({
time: attr(Time)
});
Default values
Attributes can have a default value.
App.Post = Ember.Model.extend({
tags: attr(Array,{defaultValue:[]})
});
Relationships
Ember Model provides two types of relationships hasMany
and belongsTo
. Both types of relationships can either be embedded or referenced by ids.
Defining Relationships
Relationships are defined by using relationship computed property macros in place of Ember.attr
. There are two macros available, one for each type of relationship.
Ember.belongsTo(type, options)
- Provides access to a single related object.
Ember.hasMany(type, options)
- Provides access to an array of related objects.
Both relationships take two arguments.
-
type
- Class of the related model or string representation (eg. App.Comment or 'App.Comment'). -
options
- An object with two properties,key
which is required andembedded
which is optional and defaults tofalse
.key
- indicates what property of the JSON backing the model will be accessed to access the relationshipembedded
- Iftrue
the related objects are expected to be present in the data backing the model. Iffalse
only the primaryKeys are present in the data backing the model. These keys will be used to load the correct model.
Relationship Examples
// Embedded Relationship Example
postJson = {
id: 99,
title: 'Post Title',
body: 'Post Body',
comments: [
{
id: 1,
body: 'comment body one',
},
{
id: 2,
body: 'comment body two'
}
]
};
App.Post = Ember.Model.extend({
id: Ember.attr(),
title: Ember.attr(),
body: Ember.attr(),
comments: Ember.hasMany('App.Comment', {key: 'comments', embedded: true})
});
App.Comment = Ember.Model.extend({
id: Ember.attr(),
body: Ember.attr()
});
// ID-based Relationship Example
postJson = {
id: 99,
title: 'Post Title',
body: 'Post Body',
comment_ids: [1, 2]
};
commentsJson = [
{
id: 1,
body: 'Comment body one',
post_id: 99
},
{
id: 2,
body: 'Comment body two',
post_id: 99
}
];
App.Post = Ember.Model.extend({
id: Ember.attr(),
title: Ember.attr(),
body: Ember.attr(),
comments: Ember.hasMany('App.Comment', {key: 'comment_ids'})
});
App.Comment = Ember.Model.extend({
id: Ember.attr(),
body: Ember.attr(),
post: Ember.belongsTo('App.Post', {key: 'post_id'})
})
Working with relationships
Working with a belongsTo
relationship is just like working any other Ember.Model
. An Ember.Model
instance is returned when accessing a belongsTo
relationship, so any Model
methods can be used such as save()
or reload()
.
comment.get('post').reload(); // Reloads the comments post
post.get('comments.lastObject').save(); // Saves the last comment associated to post
Accessing a hasMany
relationship returns a HasManyArray
or an EmbeddedHasManyArray
which have useful methods for working with the collection of records. On any type of hasMany
relationship you can call save()
and all the dirty records in the collection will have their save()
methods called. When working with an embedded hasMany
relationship you can use the create(attrs)
method to add a new record to the collection.
post.get('comments').save(); // Saves all dirty comments on post
// Below only works on embedded relationships
post.get('comments').create({body: 'New Comment Body'}); // Creates a new comment associated to post
Customizing
There are a few properties you can set on your class to customize how either
Ember.Model
or Ember.RESTAdapter
work:
primaryKey
The property Ember Model uses for a per-record unique value (default: "id").
App.User = Ember.Model.extend({
token: attr(),
name: attr()
});
App.User.primaryKey = 'token';
GET /users/a4bc81f90
{"token": "a4bc81f90", "name": "Brian"}
rootKey
When RESTAdapter
creates a record from data loaded from the server it will
use the data from this property instead of the whole response body.
App.User = Ember.Model.extend({
name: attr()
});
App.User.rootKey = 'user';
GET /users/1
{"user": {"id": 1, "name": "Brian"}}
collectionKey
When RESTAdapter
creates multiple records from data loaded from the server it
will use the data from this property instead of the whole response body.
App.User = Ember.Model.extend({
name: attr()
});
App.User.collectionKey = 'users';
GET /users
{"users": [{"id": 1, "name": "Brian"}]}
camelizeKeys
If the server sends keys with underscores (ex: created_at
),
rather than camelized (ex: createdAt
), setting this option to true
makes Ember Model automatically camelize the keys.
App.User = Ember.Model.extend({
firstName: attr()
});
App.User.camelizeKeys = true;
GET /users/1
{"id": 1, "first_name": "Brian"}
user.get('firstName') // => Brian
urlSuffix
By default no suffix is added to the url. You may want to specifically add one if using the same url for html and json requests.
App.User = Ember.Model.extend({
first_name: attr()
});
App.User.urlSuffix = '.json';
GET /users/1.json
{"id": 1, "first_name": "Brian"}
Customize Ajax Settings
When using RESTAdapter
custom headers and ajax settings can be applied by extending RESTAdapter
and defining ajaxSettings
App.CustomAdapter = Ember.RESTAdapter.extend({
ajaxSettings: function(url, method) {
return {
url: url,
type: method,
headers: {
"authentication": "xxx-yyy"
},
dataType: "json"
};
}
});
or it can be done at create time of the RESTAdapter
App.User.adapter = Ember.RESTAdapter.create({
ajaxSettings: function(url, method) {
return {
url: url,
type: method,
headers: {
"authentication": "xxx-yyy"
},
dataType: "json"
};
}
});
Building Ember Model
Ember Model uses node.js and grunt as a build system, These two libraries will need to be installed before building.
To build Ember Model, clone the repository, and run npm install
to install build dependencies
and grunt
to build the library.
Unminified and minified builds of Ember Model will be placed in the dist
directory.
How to Run Unit Tests
Setup
Ember Model uses node.js and grunt as a build system and test runner, and bower for dependency management.
If you have not used any of these tools before, you will need to run npm install -g bower
and
npm install -g grunt-cli
to be able to use them.
To test Ember Model run npm install
to install build dependencies, bower install
to install the
runtime dependencies and grunt test
to execute the test suite headlessly via phantomjs.
If you prefer to run tests in a browser, you may start a development server using
grunt develop
. Tests are available at http://localhost:8000/tests
Who's using Ember Model?
Are you using Ember Model? Submit a pull request to add your project to this list!
Special Thanks
Yehuda Katz (@wycats), Tom Dale (@tomdale), Igor Terzic (@igorT), and company for their amazing work on Ember Data. I believe it's the most ambitious JS project today. The goal is someday everyone's JSON APIs will be conventional enough that Ember Data will be the best choice of data library for Ember. Until then, Ember Model will make it easy to get up and running quickly with Ember.