Simple and lightweight yet feature-rich models to use with AngularJS apps. Started as a loose port of Backbone models, inspired by Restangular and Ember.Data but with AngularJS in mind and with different features. Follows the "Convention over Configuration" principle and significantly reduces the amount of boilerplate code necessary for trivial actions.
- Getting started
- Features
- Guide
- Defining/extending models
- Model static methods
- Extending collections
- Model names and
modelClassCache
- Resolving model classes
- Initializing models
- Fetching model data
- Using model static methods to query remote data
- Saving models
- Deleting models
- Model
diff
ing - Serialization notes
- Using
$request
for arbitrary HTTP requests - Tracking model
$loading
state - Parsing validation errors
- mzModelError directive
- API reference (Coming soon!)
- Contributing
- License
Modelizer depends on angular
and (since version 0.2.0 Modelizer
doesn't depend on lodash
lodash
anymore). Supported Angular versions are 1.2.X and 1.3.X.
Note: Angular 1.0.X and 1.1.X might just be supported as well because
Modelizer does not really rely on too many Angular components,
only $q
and $http
services.
With bower:
$ bower install angular-modelizer --save
Attach to your app (with dependencies):
<!-- index.html -->
<script src="angular.js"></script>
<script src="lodash.js"></script>
<script src="angular-modelizer.js"></script>
// app.js
angular.module('app', ['angular-modelizer']);
This step is completely optional unless some rich model
or collection
attributes are needed
angular.module('app')
.run(['modelize', function (modelize) {
// Note: `modelize.defineModel(...)` is an alias for `modelize.Model.extend(...)`
modelize.defineModel('post', {
baseUrl: '/posts',
// `id` is assumed to be created by the server
name: '',
title: 'New post', // `New post` becomes the default value
publishedOn: modelize.attr.date(),
author: modelize.attr.model({ modelClass: 'user' }),
comments: modelize.attr.collection({ modelClass: 'comment' })
});
}])
// Expose as an Angular service for convenience
.factory('Post', ['modelize', function (modelize) {
return modelize('post').$modelClass;
}]);
}]);
angular.module('app').controller('PostListCtrl', ['$scope', 'Post', function ($scope, Post) {
$scope.posts = Post.$newCollection();
$scope.posts.fetch();
// OR
Post.all().then(function (posts) {
$scope.posts = posts;
});
// OR
$scope.posts = Post.all().$future;
}]);
OR
angular.module('app').controller('PostCtrl', ['$scope', 'modelize', function ($scope, modelize) {
// modelizer automatically resolves the class by model name,
// plural model name, or baseUrl. If no model "classes" are
// found during resolution, it falls back to default `Model`
$scope.posts = modelize('posts').$newCollection();
$scope.posts.fetch();
// OR
$scope.posts = modelize('posts').all().$future;
}]);
See Guide and API Reference for more detail.
- Easy and straightforward model definition/extension
- Clean and intuitive model/collection API
- Custom model classes are completely optional, default
Model
would suffice for many cases - Special types of attributes for custom model classes with
modelize.attr
helper. Custom attributes can bemodel
,collection
,computed
ordate
(date
is WIP) - Automatic model "class" resolution based on specified model name, collection name (pluralized model name in fact) or base URL with fallback to default
Model
when no appropriate model isn't found - Resource URL building by using "modelizer" chains (e.g.,
modelize('blog').one('post', 123).many('comments')
). That takes the custom modelsbaseUrl
property into account for convenience. - All model-related requests return Promises enhanced with
$future
object that represents the result of request wrapped into model or collection - Collections are
Array
instances with custom methods mixed in (Array
prototype is not modified) so can be used with AngularJS as regular arrays. - Model maintains the last known server state and can perform a
diff
to see whats changed. Very useful to performPATCH
operations. - Models and collections track "loading" state accessible via
$loading
property. Useful to show spinners, progress bars, etc - Models maintain links to all the collections they're in. These links are automatically removed when models are
destroy()
ed or removed from collections explicitly - Parses model validation errors from responses with
422
status code and maintains$modelErrors
property. Useful to use in templates withngMessages
- Many more smaller features and hidden gems - see Guide for details.
Possible future features:
- Multiple
modelize
contexts configuration that work with different APIs (being designed) - Decoupled
store
layer to maintain model data locally for offline-first approach (being discussed) - Identity maps for underlying store (being discussed)
- Relation attribute type via
modelize.attr.relation.hasMany(...)
andmodelize.attr.relation.belongsTo(...)
(being discussed) - Real-time utility module for easy real-time sync via WebSockets out of the box (reviewing the possibilities)
Modelizer is a very simple thing. It provides the default Model
class which is sufficient for many use cases unless some attribute should be a model
or a collection
of particular model class on its own.
- Default
Model
is exposed asmodelize.Model
property - New model class can be defined by calling
modelize.defineModel()
method ormodelize.Model.extend()
(the former is the shortcut for the latter) - Custom models can be extended further by calling
MyCustomModel.extend()
. defineModel()
andextend()
return model "class". It is convenient to wrap it into Angular service (e.g.,.factory(...)
) that returns that model class.- There is a
modelize.attr
property to help define custom model attributes:modelize.attr.model(options)
to define a rich model propertymodelize.attr.model(options)
to define a collection propertymodelize.attr.computed(computeFn)
to define a "computed" property with no setter. Pass thecomputeFn
function as a parametermodelize.attr.date(options)
to define a property ofDate
type. While you could use just regular property for that, using this attribute helper ensures that the property will always have theDate
type.- Provide optional
modelClass: SomeModelClass
option to specify what model class that attribute should have. Only applicable toattr.model
andattr.collection
. If not specified, the defaultModel
is set as attributemodelClass
. - Properties defined with
modelize.attr
are lazily initialized. Objects and arrays of objects of particular type are only created when requested - When defining model attributes as
modelize.attr.model()
ormodelize.attr.collection()
make sure the dependency models are defined already. Thats why its worth callingmodelize.defineModel()
ormodelize.Model.extend()
inside Angularrun
blocks. - If string is provided as
modelClass: ...
option tomodelize.attr.model()
ormodelize.attr.collection()
then its class will be lazily resolved at the time attribute is requested for a first time.
- If your model should have some other attribute as
id
, useidAttribute
property to define that - If you define a value for some attribute, it will become its default value
- Static model class methods are defined inside special
static
property
Code speaks louder than plain English does:
// app/models/post.js
angular.module('app')
.run(['modelize', function (modelize) {
// Note: `modelize.defineModel(...)` is an alias for `modelize.Model.extend(...)`
return modelize.defineModel('post', {
baseUrl: '/posts',
// `id` is assumed to be created by the server
name: '',
title: 'New post', // `New post` becomes the default value
publishedOn: modelize.attr.date(), // Make sure `publishedOn` is always a Date object
author: modelize.attr.model({ modelClass: 'user' }), // Define nested model
comments: modelize.attr.collection({ modelClass: 'comment' }) // Define collection property
});
}])
.factory('Post', ['modelize', function (modelize) {
return modelize('post').$modelClass;
}]);
// app/models/comment.js
angular.module('app')
.run(['modelize', function (modelize) {
modelize.defineModel('comment', {
baseUrl: '/comments',
// `id` is assumed to be created by the server
text: '',
author: modelize.attr.model({ modelClass: 'user' })
});
}])
.factory('Comment', ['modelize', function (modelize) {
return modelize('comment').$modelClass;
}]);
To define custom static methods for model (to use alongside default all()
,
query()
, get()
, etc) you could:
// When defining the custom model itself
modelize.defineModel('post', {
title: '',
...
static: {
getRecent: function () {
return this.$request.get(this.baseUrl + '/recent');
}
}
});
// Using pure JavaScript
Post.getRecent = function () {
return this.$request.get(this.baseUrl + '/recent');
};
The collection in Modelizer is just a regular JavaScript Array
with
some methods, properties and "state" mixed in for convenience. Aside
from default set of methods and properties, you could add your own.
This is done using either of two main ways:
1. Extend collection when defining model:
modelize.defineModel('post', {
title: '',
...
collection: {
someCollectionMethod: function () {
// ...
}
}
});
2. Extend collection afterwards.
Useful if you want to extend the arbitrary model class including default Model
.
Please notice that extendCollection
method modifies the model class it is
being called on by extending its internal metadata.
var Post = modelize.defineModel('post', {
title: '',
...
});
Post.extendCollection({
someCollectionMethod: function () {
// ...
}
});
Note: extendCollection
returns the model class it has been called on.
modelClassCache
is where model classes are stored internally for fast lookups by:
- Model name
- Collection name
- Model
baseUrl
This is needed for correct model class resolution.
Model class appears on the modelClassCache
every time new model is defined
with modelize.defineModel(...)
, modelize.Model.extend(...)
or SomeModelClass.extend(...)
.
All of above-mentioned methods accept model name as the first parameter.
This is the name under which the model class appears in modelClassCache
:
modelize.defineModel('post', { ... });
But what about collection name?
Well, by default the collection name is generated internally based on the specified model name:
// collection name is 'posts'
modelize.defineModel('post', { ... });
// collection name is 'comments'
modelize.defineModel('comment', { ... });
// collection name is 'princesses'
// Note: ends with 's', so 'es' is appended
modelize.defineModel('princess', { ... });
But you could specify that explicitly too by providing array of String
s as the first argument
to Model.extend(...)
:
// collection name is 'companys' which is hardly desired
modelize.defineModel('company', { ... });
// collection name is 'companies'
modelize.defineModel(['company', 'companies'], { ... });
// collection name is 'colossi'
modelize.defineModel(['colossus', 'colossi'], { ... });
You could set the baseUrl
when defining model:
// all instances of `post` model will have '/posts' base URL unless
// explicitly overridden for particular instance
var Post = modelize.defineModel('post', {
baseUrl: '/posts'
});
Or when creating the new model instance:
// post1 will have '/posts' baseUrl
var post1 = Post.$new();
// post2 will have '/some/special/posts' baseUrl
var post2 = Post.$new({ baseUrl: '/some/special/posts' });
If you don't provide the baseUrl
on either model definition or new instance creation,
it will be automatically generated based on collectionName
:
var Post = modelize.defineModel('post', {
// No baseUrl is set here
});
// post will have '/posts' baseUrl
var post = Post.$new();
var Mouse = modelize.defineModel(['mouse', 'mice'], {
// No baseUrl is set here
});
// mouse will have '/mice' baseUrl
var mouse = Mouse.$new();
Note: baseUrl
is a special property and is excluded from model upon serialization.
urlPrefix
is a simple property whose value will be prepended to baseUrl
when resolving model instance resource URL.
Why is that given we already have baseUrl
?
Well, this way we allow different urlPrefix
es in different contexts while allow
models to handle baseUrl
on their own. This is heavily used for dynamic URL building
on model class resolution.
This is the sweet thing in Modelizer. Basically, in order to work with models, you have
to get the "model class" somehow first to create model or collection instances, or use
convenience static methods like all()
or get()
.
You could just get the model class directly as a value returned from modelize.defineModel(...)
:
// app/models/post.js
angular.module('app').factory('Post', ['modelize', function (modelize) {
// Just make the `Post` angular service return the Post model class
return modelize.defineModel('post', {
// No baseUrl is set here
});
}]);
// app/controllers/post-list-ctrl.js
angular.module('app').controller('PostListCtrl', ['Post', function (Post) {
// Init new collection of `Post` models
$scope.posts = Post.$newCollection();
// Request all posts using static method on model class
Post.all().then(function (posts) {
$scope.posts = posts;
});
}]);
Note: You don't need to know all the internal detail in order to use the Modelizer API. But if you're curious - make sure to read carefully since the stuff below might be a bit complicated to get right away.
There is also another way to obtain the model class and this is where the Modelizer
object comes into play.
The core API is:
modelize.one(resourceName, [id])
modelize.many(resourceName)
modelize(resourceName)
which is just a shortcut formodelize.many(resourceName)
Some points to note:
- These
one()
andmany()
methods returnModelizer
instance that in turn exposes the methods to create model instances and request remote data. But before we're able to work with models, we should know what model class we need to use. modelize.one()
returns theModelizer
instance withmodelModelizer
methods mixed in and is intended to work in the context of a single item (e.g., has methods likesave()
ordestroy()
)modelize.many()
andmodelize()
return theModelizer
instance withcollectionModelizer
methods mixed in and is intended to work in the context of a collection of items (e.g. has methods likequery()
,get()
,all()
, etc)- Resolved model class is exposed as a
$modelClass
property onModelizer
- If no model class is found in
modelClassCache
byModelizer
, it falls back to defaultmodelize.Model
- Aside from resolving the model class,
Modelizer
also defines thebaseUrl
andurlPrefix
that should be set on model instances. - All methods that make HTTP requests return promises. These promises are enhanced
with
$future
property whose value is already initialized model or collection but without server data. Model/collection is being updated with actual data when that is returned from server (similar to how Angular$resource
works and to Restangular$object
property on promises). Please note that$future
always references only thedata
portion of the response, ignoring thefullResponse
orrawData
parameters.
The model class resolution strategy relies on the fact that resourceName
argument
can be either:
- Model property name
- Model name
- Collection name
- Model
baseUrl
Basic examples (see next sections for more):
// Lets define some models first:
// =============================
modelize.defineModel('post', {
baseUrl: '/blog/posts',
comments: modelize.attr.collection({
modelClass: 'comment',
baseUrl: '/special-comments'
})
});
modelize.defineModel('comment', {
baseUrl: '/comments',
...
});
// Then use that (e.g., in a controller):
// =====================================
// Resolves to 'post' model (by collection name)
// and exposes the 'collection modelizer'
modelize('posts');
// Resolves to 'post' model (by model name)
// and exposes the 'model modelizer'
modelize.one('post', 123);
// Resolves to 'post' model (by collection name)
// and exposes the 'model modelizer'
modelize.one('posts', 123);
// Resolves to 'comment' model (by collection name)
// and exposes the 'collection modelizer'
// The baseUrl is set to '/comments' (taken from 'comment' model class)
modelize('comments');
// Resolves to 'comment' model (by 'post' model attrubute name)
// and exposes the 'collection modelizer'
// The urlPrefix is set to '/blog/posts'
// The baseUrl is set to '/special-comments'
// (taken from 'post' attribute definition metadata)
modelize.one('posts', 123).many('comments');
// No custom model is found, resolves to default Model class
// baseUrl is set to '/things'
modelize('things');
// No custom model is found, resolves to default Model class
// baseUrl is set to '/things/123/subthings'
modelize('things/123/subthings');
// No custom model is found, resolves to default Model class
// baseUrl is set to '/subthings'
// urlPrefix is set to '/things/123'
modelize.one('things', 123).many('subthings');
// No custom model is found (because Post baseUrl is /blog/posts in our case),
// resolves to default Model class instead. Only searched by baseUrl since provided
// `resourceName` is immediately considered URL because of '/'.
// baseUrl is set to '/posts'
modelize('/posts');
Refer to the next sections on detail about how to work with these model classes and model data.
You can easily create new model and collection instances:
// Using model "class":
// ===================
// Empty collection
var posts = Post.$newCollection();
// Collection With data
var posts = Post.$newCollection([{
title: 'Post 1',
...
}, {
title: 'Post 2',
...
}]);
// Empty model
var post = Post.$new();
var post = new Post();
// Model with some attributes set
var post = Post.$new({ title: 'Post 1' });
var post = new Post({ title: 'Post 1' });
// Same thing using `Modelizer`:
// ============================
var posts = modelize('posts').$newCollection();
// OR
var posts = modelize('posts').$newCollection([{ ... }, { ... }, { ... }]);
var post = modelize('posts').$new();
// OR
var post = modelize.one('post').$new();
var post = modelize.one('posts').$new();
var post = modelize.one('posts').$new({ ... });
You can easily fetch the model data by calling fetch()
on model instance.
Note that fetch()
method accepts url
option to fetch from arbitrary
URL. options
are also passed to the underlying $http
service calls
so feel free to provide any params
or headers
there.
// Note: all methods that work with HTTP return promises
// Fetch collection
// ================
// Create empty collection
var posts = Post.$newCollection();
// OR
var posts = modelize('posts').$newCollection();
// Then fetch the entire collection
// GET /posts
posts.fetch(); // returns promise
// Fetch single item
// =================
var post = modelize('posts').$new({ id: 123 });
// GET /posts/123
post.fetch();
// Get first item from already "fetched" collection
// Note that item already has the 'id' attribute set
var post = posts[0];
// GET /posts/123
post.fetch(); // Sync, returns promise
// GET /some/custom/url-to-fetch
post.fetch({ url: '/some/cusom/url-to-fetch' }); // returns promise
// GET /posts/123/comments
post.comments.fetch(); // Property defined with 'modelize.attr.collection()'
Model class exposes a set of concenience methods to make remote requests that return promises resolved with either model or collection instance (depending on method). As usual, methods can be invoked on model class directly or via modelizer.
// Get a single item
// =================
// GET /posts/123
modelize('posts').get(123).then(function (post) {
// Note: Use promise callbacks when you have
// something to do after request has completed.
// In simple cases just use `$future` property
// of modelizer promises.
$scope.post = post;
});
// OR
var post = Post.get(123).$future;
var post = modelize('posts').get(123).$future;
var post = modelize.one('post', 123).get().$future;
// Get collection of items
// =======================
// GET /posts
var posts = modelize('posts').all().$future;
// OR
var posts = Post.all().$future;
// GET /posts?published=false
var posts = modelize('posts').query({ published: false }).$future;
Once you have changed the model data, you might need its new state to be posted
back to the API server. This is what model save()
method for. Depending on
current model state and data, we can either create, update or partially
update the model data on the server.
- Overridable
isNew()
is invoked on the model before saving. DefaultisNew()
implementation only checks whether there is anid
(or whateveridAttribute
is) set on the model instance. If that returnstrue
then thePOST
request is sent to the modelbaseUrl
(orurl
passed as an option). - If the model isn't considered new, then the
PUT
request is sent to a resource URL that model represents. - Pass the
patch: true
option to perform aPATCH
operation instead ofPUT
(only makes effect when the model is notisNew()
) - When performing
PATCH
updates, the model performs thediff
between its current state and last known remote state and only sends the changed attributes (or completely new ones).
There is also a save()
method on Modelizer
that accepts the data to
send to the server. This method basically initializes the new model with provided data
and calls save()
on it. So, all the same rules (from above) apply here.
// Using `save()` method on Modelizer
// =================================
// POST /posts
modelize('posts').save({ title: 'Some post title' });
// PUT /posts/123
modelize('posts').save({ id: 123, title: 'Some post title' });
// PATCH /posts/123
modelize('posts').save({ id: 123, title: 'Some post title' }, { patch: true });
// Using `model.save()` method
// =========================
// Create an item
var post = modelize('posts').$new({ title: 'Some new title' });
// Does not cause an update since
// we've never saved the model before
post.title = 'Some other title';
// POST /posts
post.save(); // returns promise
// There is also a convenience `create()` method on collection:
var posts = modelize('posts').all().$future;
...
// POST /posts
posts.create({ title: 'Some new post' }); // returns promise
// Note: Changing properties does not cause an update since
// we've never saved the model before.
var post = modelize('posts').$new({ title: 'Some new title' });
post.title = 'Some other title';
// POST /posts
post.save(); // returns promise
// Update an item
var post = modelize('posts').get(123).$future;
...
post.title = 'Another title';
// PUT /posts/123
post.save();
// Partially update an item
var post = modelize('posts').get(123).$future;
...
post.title = 'Another title';
// PATCH /posts/123
post.save({ patch: true });
Deleting models is extraordinary simple. Again, both model instance and
Modelizer
have destroy()
methods for this.
- By default, when the model is "destroyed", it is removed from collections that contain it.
- If the model was never saved, no HTTP request is issued and model is just removed from collections.
- Provide
wait: true
option to wait until HTTP request completes before removing the model from collections. - When the model is destroyed, its
$destroyed
property value becomestrue
. - Model
destroy()
method accepts thekeepInCollections: true
option to prevent it from being removed from collections. Be careful, because from now on, the collection is in somewhat inconsistent state and handling that is developers responsibility (i.e., using explicitcollection.remove(model)
). This might be useful if you want to keep your model inside the collection marked as$destroyed
for some reason (like allowing to "undo").
// Using `destroy()` method on Modelizer
// =================================
// DELETE /posts/123
modelize('posts').destroy(123);
// Using `model.destroy()` method
// =========================
// No request is done in this case since we never saved the model
var post = modelize('posts').$new({ title: 'Some new title' });
post.destroy(); // returns promise
// Now get the post for below examples
var post = modelize('posts').get(123);
// Deleting post, immediately remove from collections
// DELETE /posts/123
post.destroy(); // returns promise
// Deleting post, but wait for the successful response before
// removing from collections
// DELETE /posts/123
post.destroy({ wait: true }); // returns promise
// Deleting post, but keep the model in collections.
// Use with caution! The collection is in inconsistent state now.
// DELETE /posts/123
post.destroy({ keepInCollections: true }); // returns promise
// In any case, post obtains the $destroyed property
post.$destroyed; // true
Model has the following methods to assist serialization:
getAttributes()
getChangedAttributes()
serialize()
toJSON()
The getAttributes()
method is used to only get "serializable"
attributes of a model. Note that reserved properties are excluded
from that set of attributes. Specify includeComputed: true
option to
have computed properties added to resulting object.
The current list of reserved properties is the following:
// The list of reserved internal properties
// that are "non-attributes" and should be excluded
// from model when getting its attributes (workaround
// to allow model attributes on a model directly
// side by side with system/internal properties).
var _reservedProperties = [
'$$hashKey',
'$iid',
'_modelClassMeta',
'_collections',
'_remoteState',
'_loadingTracker',
'_initOptions',
'idAttribute',
'baseUrl',
'urlPrefix',
'$modelErrors',
'$error',
'$valid',
'$invalid',
'$loading',
'$selected',
'$destroyed'
];
There is also a getChangedAttributes()
method that only returns
attributes that are changed since the last known server
state. Used to perform PATCH
operations. Uses diff
method
internally (see next section).
The serialize()
method on a model simply calls getAttributes()
and then handles the special cases when some model attribute
is a model
or collection
on its own (and calls serialize()
on them).
The serialize()
method on a collection only runs serialize()
on each model from that collection and returns the array
of serialized models.
The toJSON()
method only transforms the already serialize()
d
models to actual JSON string.
The model saves its last known server state internally and
when attributes change, it knows how to perform the correct
PATCH
. How is that? Well, thats what diff
for.
Model performs diff
between current and last known server state
in the changedAttributes()
method.
diff
can be performed against any other object though. It doesn't
compare models by reference and instead compares attribute values
one by one. So, any object can be used to diff
model against.
The final diff is a hash of differences only keyed with attribute name
and contains current and compared value:
var post = modelize('post').$new({
title: 'Some post title',
text: 'Some text'
});
var diff = post.diff({ title: 'Some other title', text: 'Some text' });
// `diff` is now as the following:
// {
// title: {
// currentValue: 'Some post title',
// comparedValue: 'Some other title'
// }
// }
You can perform arbitrary HTTP requests by using
the same convenience wrapper around Angular $http
service - modelizer $request
. The only thing it does
is makes successful requests promises being resolved
with data
only and not entire response (pass rawData: true
option to obtain the entire response as $http
does by default).
It also adds convenience $future
object to promises (always contains
data even if fullResponse: true
option is passed.
For convenience, the $request
is set as a property of:
modelize
- Model class
- Model instance
- Collection instance
All the options of $request
methods are passed to corresponding
$http
methods so params
, data
, method
, headers
, etc are
all acceptable.
// Make arbitrary HTTP requests
// posts isn't a model or collection, just a regular object here
var posts = modelize.$request.get('/blog/posts').$future;
// Note: We don't work with Models here at all, just raw requests
// Create an item
modelize.$request.post('/blog/posts', {
title: 'Some post title',
text: 'Some post text'
});
// Update an item
modelize.$request.put('/blog/posts/123', {
title: 'Some updated post title',
text: 'Some updated post text'
});
// Perform a partial update
modelize.$request.patch('/blog/posts/123', {
title: 'Some updated post title'
});
// Useful to define custom model and collection methods
// that need to perform some HTTP calls.
modelize.defineModel('post', {
title: '',
text: '',
someCustomAction: function () {
return this.$request.post(this.resourceUrl() + '/some-custom-action',
this.getAttributes({ includeComputed: true }));
},
static: {
getRecent: function () {
// `this` is now model class, it has $request property too
return this.$request.get(this.baseUrl + '/recent');
}
},
collection: {
someCollectionMethod: function () {
// collections have $request property as well
return this.$request.get(...);
}
}
})
One of pretty common use cases is to show some kind of loading indicator (aka "spinner") while HTTP request associated with model is being performed.
This is what model or collection $loading
property for.
It always returns true
when there is active request for a model
or collection (either to fetch data, update or destroy the model).
The "loading" state tracker is attached to the model as the _loadingTracker
property. It supports the addPromise()
method so feel free to add new
promises which will cause the model to be $loading
.
This property is one of the reserved properties so its not included when model is serialized.
Assume we have model/collection on the controller $scope
:
// post-list-ctrl.js
angular.module('app').controller('PostListCtrl', ['$scope', 'modelize',
function ($scope, modelize) {
$scope.posts = modelize('posts').all().$future;
}]);
Show spinners while the entire collection or a single model is loading:
<div class="list-loading-spinner" ng-show="posts.$loading">
The list of posts is loading...
</div>
<div ng-repeat="post in posts">
<div class="single-post-loading-spinner" ng-show="post.$loading">
Post is loading...
</div>
<h1>{{ post.title }}</h2>
<div>
...
</div>
</div>
When model save fails, the promise save()
method returns is rejected
with error server responds with. The model.save()
method handles that
error as well by parsing the validation error and maintains the $modelErrors
object with:
- Keys named after model attriubute names
- Values having arrays of error messages from the server
To parse the error response, model has the parseModelErrors()
method
which by default uses the configurable global parseModelErrors()
. Note
that global parseModelErrors()
only handles server errors with status
code 422 Unprocessable entity
So, to change the error parsing logic, you could:
- Override the
parseModelErrors()
method for particular model class. - Override the global
parseModelErrors()
method by configuring themodelizeProvider
.
The default server response format is expected to have these fields:
{
"fieldErrors": [{
"field": "name",
"message": "Your name field is invalid"
}, {
"field": "description",
"message": "Your description field is invalid"
}, {
"field": "description",
"message": "Yet another problem with your description"
}]
}
Model maintains its $modelErrors
property in the following form:
{
name: ['Some name error'],
description: ['Some descriotion error', 'Another description error']
}
Override easily:
// Override for marticular model class
modelize.defineModel('thing', {
name: '',
description: '',
parseModelErrors: function (responseData, options) {
// Custom parsing logic
// ...
// Result should follow the format:
return {
name: ['Some name error'],
description: ['Some descriotion error', 'Another description error']
};
}
});
// Override global one
angular.module('app').config(['modelizeProvider', function (modelizeProvider) {
// This will be applied to all model classes
// unless explicitly overridden for particular model
modelizeProvider.parseModelErrors = function (responseData, options) {
// Custom parsing logic
// ...
};
});
There is a tiny mzModelError
directive in Modelizer
to help you to show model errors to the user. Requires ng-model
attribute set on the same element (will be ignored otherwise).
Think of it as of just another validator like 'required' but
related to model errors returned from server.
In the example below, the message with modelError
validation token
will appear for form control when server returns the error for title
field on post
model. See previous section on how these errors are parsed.
<form name="postForm" novalidate>
<!-- The model error property will be implied automatically based
on ng-model property and the value will be parsed against
"post.$modelErrors.title" expression -->
<imput name="title" ng-model="post.title" required minlength="4" mz-model-error>
<!-- OR: explicitly set the object to take model errors from -->
<imput name="title" ng-model="post.title" required minlength="4" mz-model-error="post.$modelErrors.title">
<!-- Show the errors with ngMessages -->
<div ng-messages="postForm.title.$error">
<div ng-message="required">The title is required for a blog post</div>
<div ng-message="minlength">The title should be at least 4 charactes long</div>
<div ng-message="modelError">
<div ng-repeat="err in post.$modelErrors.title">{{ err }}</div>
</div>
</form>
Note: mzModelError
can work with any object,
not only modelizer models. The error will appear on
postForm.title.$error
under modelError
key.
Coming soon!
If you have found some bug or want to request some new feature, please use GitHub Issues to let us know about that. Pull Requests are welcome too.