From 1efa96bf670ded9837e3d055e39c12e5a2df2fe1 Mon Sep 17 00:00:00 2001 From: Gavin Davies Date: Wed, 11 Nov 2015 13:39:54 +0000 Subject: [PATCH] Adds documentation * New gulp task `gulp ngdocs` to generate documentation into build/docs * Add ngdoc-compliant documentation to source code * Add documentation to README.md * Add Markdown version docs in /docs directory --- .gitignore | 3 +- .jscsrc | 15 +- Gulpfile.js | 13 +- README.md | 298 ++++++++++++++ docs/api.md | 907 +++++++++++++++++++++++++++++++++++++++++++ package.json | 11 + src/angular-model.js | 379 +++++++++++++++++- 7 files changed, 1613 insertions(+), 13 deletions(-) create mode 100644 docs/api.md diff --git a/.gitignore b/.gitignore index 52cd695..2cf357e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store coverage/ -node_modules/ \ No newline at end of file +node_modules/ +build/ diff --git a/.jscsrc b/.jscsrc index efc1d6e..9cd25e4 100644 --- a/.jscsrc +++ b/.jscsrc @@ -1,5 +1,14 @@ { - "preset" : "google", - "maximumLineLength" : 120, - "disallowMultipleVarDecl" : false + "preset": "google", + "maximumLineLength": 120, + "disallowMultipleVarDecl": false, + "jsDoc": { + "checkAnnotations": { + "preset": "jsdoc3", + "extra": { + "ngdoc": "some", + "methodOf": "some" + } + } + } } diff --git a/Gulpfile.js b/Gulpfile.js index 0b8c647..6edbb61 100644 --- a/Gulpfile.js +++ b/Gulpfile.js @@ -7,7 +7,7 @@ var jshint = require('gulp-jshint'); var jscs = require('gulp-jscs'); var nsp = require('gulp-nsp'); var runSequence = require('run-sequence'); -var istanbul = require('gulp-istanbul'); +var istanbul = require('gulp-istanbul'); /* * PLEASE NOTE: run-sequence is a @@ -21,6 +21,17 @@ var paths = { 'src': 'src/**/*.js' }; +gulp.task('ngdocs', [], function () { + var gulpDocs = require('gulp-ngdocs'); + return gulp.src(paths.src) + .pipe(gulpDocs.process({ + html5Mode: false, + startPage: '/api', + title: 'angular-model' + })) + .pipe(gulp.dest('./build/docs')); +}); + gulp.task('test', function(done) { new Server({ configFile: __dirname + '/config/karma.conf.js', diff --git a/README.md b/README.md index 8cdf84d..f087892 100644 --- a/README.md +++ b/README.md @@ -11,3 +11,301 @@ **Angular Model** is a module that provides a simple way to bind client-side domain logic to JSON-based API resources. By sticking to hypermedia design principles, Angular Model allows you to implement client applications that are cleanly decoupled from your server architecture. + +## Basic Usage + +In your AngularJS application, include the JavaScript: + +```html +// your specific paths may vary + +``` + +In your app configuration, state a dependency on [Angular Model](https://github.com/radify/angular-model): + +```javascript +angular.module('myApp', [ + 'ur.model' +]); +``` + +## API documentation + +The source code is documented using the ngdoc standard using [gulp-ngdocs](https://www.npmjs.com/package/gulp-ngdocs/). A markdown version is browseable at [/docs](/docs/api.md). + +To generate documentation in HTML, run: + +```bash +gulp ngdocs +``` + +This will output docs into the `build/docs` directory. Then, using a server like `ws`, start a local web server: + +```bash +cd build/docs +npm install -g ws +ws +``` + +Then, you should be able to browse to http://localhost:8000 to view the API documentation for angular-model. + +## Configuration + +Here is a quick reference guide to all the configuration settings you can pass to the model() constructor, which is [documented in full in the API documentation](/docs/api.md). Each one is then described in detail later in this document, and in full in the source code in the `src` directory. + +Setting | Type | Description +------- | ---- | ----------- +url | string | API url that this model maps to +defaults | object literal | Default values of attributes of instances of this model. Similar to properties in OOP. +$instance | object literal | Instance methods available on each instance of this model. +$class | object literal | Class methods available on this model. Similar to static methods in OOP. +$collection | object literal | Collection + +### Defaults + +```javascript +yourApp.config(function(modelProvider) { + modelProvider.model('posts', { + /** + * @ngdoc object + * @name yourApp.posts.defaults + * @description + * Configure the default attributes for instances of posts. + * + * This is similar to an OOP class, which has attributes with defaults, e.g. "public string foo = 'bar';" + */ + defaults: { + name: '', // The name of the post + published: false, // Whether the post has been released to the general public + body: '', // Body text of this post + logo: null, // The logo to show for this post + author: 'John Doe'// Who wrote the post? + } + }); +}); +``` + +Here is an example of how the defaults get used: + +```javascript +var post = model('posts').create({}); +console.log(post.author); +=> John Doe +``` + +## Creating instances of your model + +You can use angular-model ad-hoc to construct object instances: + +```javascript +// From defaults +var post = model('posts').create({}); + +// Specifying fields +var post = model('posts').create({ + name: 'some post', + body: "body of some body, it's just some body, you know?", + author: 'Steve Davis' +}); + +console.log(post.author); +=> Steve Davis +``` + +## Instance Methods + +angular-model instances have instance methods, similar to objects in the OOP world. + +### Default instance methods + +The following methods are available to every angular-model instance. + +Function | Description +------- | ----------- +$save | Persist an instance to the API +$delete | Tell the API to delete an instance +$reload | Refresh an instance of a model from the API +$revert | Reset the model to the state it was originally in when you first got it from the API +$exists | Checks whether an object exists in the API, based on whether it has an identity URL. +$dirty | Returns boolean - true if a model instance has been modified, else false. Opposite of $pristine. +$pristine | Returns boolean - true if a model instance has unmodified, else false. Opposite of $dirty. +$related | Hydrates the $links property of the instance. $links are used so that an instance can tell the client which objects are related to it. For example, a `post` may have an `author` object related to it. +$modified | Returns a map of the properties that have been changed +$hasRelated | Does an instance have a relation of name `name`? + +> You can see full details of these methods in the [API documentation](/docs/api.md). + +### Custom instance methods + +angular-model allows you to define instance methods on instances. This is similar to adding methods by extending a base class in the OOP world. + +```javascript +yourApp.config(function(modelProvider) { + modelProvider.model('posts', { + // ... + + /** + * @ngdoc object + * @name yourApp.posts.$instance + * @description + * Instance methods that are callable on any individual instance of a post + */ + $instance: { + /** + * @ngdoc function + * @name yourApp.posts.$logo + * @description + * If this post instance has a logo, return it, otherwise return a default string + * + * @return string Either the logo for this post, or a default logo + */ + $logo: function() { + return this.logo || '/logos/default.png';. + } + } + }); +}); +``` + +Example: + +```javascript +var post = model('Posts').create({ + logo: 'foo.png' +}); +console.log(post.$logo()); +=> foo.png +``` + +## Class methods + +### Default class methods + +The following methods are available statically to angular-model: + +Function | Description +-------- | ----------- +all | Make a request to the API, based on the `url` configuration setting +first | Given a query, get the first model instance from the API +create | Create a new instance of the model. Defaults come from the `defaults` configuration setting. + +> You can see full details of these methods in the [API documentation](/docs/api.md). + +### Custom class methods + +angular-model allows you to define class methods on instances. This is similar to static methods in the OOP world. + +```javascript +yourApp.config(function(modelProvider) { + modelProvider.model('posts', { + // ... + + /** + * @ngdoc object + * @name yourApp.posts.$class + * @description + * Class methods that are callable on the posts class, or any instance thereof. These + * behave similarly to static methods in OOP languages. + */ + $class: { + /** + * @ngdoc function + * @name yourApp.posts.roles + * @description + * Get an array of valid post types. + * + * @return array The valid types that a post can have. Array of strings + */ + types: function() { + return ['announcement', 'article'] + } + } + }); +}); +``` + +Example: + +```javascript +console.log(model('Posts').types()); +=> ['announcement', 'article'] +``` + +## Collection methods + +You can use collection methods as well, so you can deal with a bunch of instances together. This allows you to have powerful and expressive methods on collections. + +### Default collection methods + +The following methods are available statically to angular-model: + +Function | Description +-------- | ----------- +add | Saves the `object` with `data` +remove | Find `index` and delete it from the API, then remove it from the collection + +> You can see full details of these methods in the [API documentation](/docs/api.md). + +### Custom collection methods + +```javascript +yourApp.config(function(modelProvider) { + modelProvider.model('posts', { + // ... + + /** + * @ngdoc object + * @name yourApp.posts.$collection + * @description + * Methods that apply to a collection of posts together + */ + $collection: { + /** + * @ngdoc function + * @name yourApp.posts.$hasArchived + * @description + * Operates on a collection of posts and determines whether any of them are archived + * + * @requires _ Lodash library is used to search the collection + * + * @return string Either the logo for this post, or a default logo + */ + $hasArchived: function() { + return !angular.isUndefined(_.find(this, { archived: true })); + } + } + }); +}); +``` + +Example: + +```javascript +model('Posts').all().then(function(posts) { + if (posts.$hasArchived()) { + // Some of the posts in the collection are archived + } +}); +``` + +Running unit tests +-- + +Install the test runner with npm: + +```bash +npm install +``` + +You can then run the tests with gulp: + +```bash +gulp +``` + +Tests can be found in the `spec` directory of this project. + +Related +-- + +You may wish to use [Angular Scaffold](https://github.com/radify/angular-scaffold/), which is is a collection of convenience wrappers around angular-model collections. Really helpful for building your AngularJS application with angular-model. diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..73ad727 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,907 @@ +# angular-model API documentation + + +`model` +======= + +function in module `ur` + +Description +----------- + +Main factory function for angular-model + +Usage +----- + +```javascript +model(name[, options]); +``` + +#### Parameters + + +++++ + + + + + + + + + + + + + + + + + + + +
ParamTypeDetails
namestring
+

Name of the 'class', e.g. 'posts'

+
options +
+(optional) +
object
+

Config to initialise the model 'class' with. You can supply an object literal to configure your model here.

+
+ +#### Returns + + ++++ + + + + + + +
ur.model
+

instance of angular-model for the 'class' identified by 'name'

+
+ +Example +------- + + yourApp.config(function(modelProvider) { + modelProvider.model('posts', { + // configuration options + $instance: { + // custom instance functions + }, + $class: { + // custom class functions + }, + $collection: { + // custom collection functions + } + }); + }); + + +`$instance` +=========== + +object in module `ur` + +Description +----------- + +Methods available on model instances + +You can use these when you have created or loaded a model instance, see the example + + var post = model('posts').first({_id: 42}); + console.log(post.name); + => "Post with ID 42" + + post.name = 'renamed'; + post.$save(); + +You can specify custom instance methods: + + yourApp.config(function(modelProvider) { + modelProvider.model('posts', { + $instance: { + $logo: function() { + return this.logo || '/logos/default.png';. + } + } + }); + }); + +Methods +------- + +### $delete() + +Delete an instance from the API + +##### Returns + + ++++ + + + + + + +
object
+

Promise from an API request

+
+ +#### Example + + post.$delete(); + +### $dirty() + +Returns boolean - true if a model instance is unmodified, else false. Inverse of $pristine. + +##### Returns + + ++++ + + + + + + +
boolean
+

true if a model instance is modified, else false. Inverse of $pristine.

+
+ +#### Example + + if (post.$pristine()) { console.log('It is just as it was when we got it from the API'); } + +### $exists() + +Checks whether an object exists in the API, based on whether it has an identity URL. + +##### Returns + + ++++ + + + + + + +
boolean
+

True if the identifier of this instance exists in the API

+
+ +#### Example + + if (post.$exists()) { console.log('It exists'); } + +### $hasRelated(name) + +Does an instance have a relation of name `name`? + +##### Parameters + + +++++ + + + + + + + + + + + + + + +
ParamTypeDetails
namestring
+

Name of the related property to check for

+
+ +##### Returns + + ++++ + + + + + + +
boolean
+

true if a $link to name exists on this instance

+
+ +#### Example + + if (post.$hasRelated('author')) { console.log('Post has an author'); } + +### $modified() + +Returns a map of the properties that have been changed + +##### Returns + + ++++ + + + + + + +
object
+

Map of the fields that have been changed from the $pristine version

+
+ +#### Example + + console.log(post.$modified()); + +### $pristine() + +Returns boolean - false if a model instance is unmodified, else true. Inverse of $dirty. + +##### Returns + + ++++ + + + + + + +
boolean
+

true if a model instance is unmodified, else false. Inverse of $dirty.

+
+ +#### Example + + if (post.$dirty()) { console.log('Post has been modified'); } + +### $related() + +Hydrates the $links property of the instance. $links are used so that an instance can tell the client which objects are related to it. For example, a `post` may have an `author` object related to it. + +##### Returns + + ++++ + + + + + + +
object
+ +#### Example + + console.log(post.links()); + +### $reload() + +Refresh an instance of a model from the API + +##### Returns + + ++++ + + + + + + +
object
+

Promise from an API request

+
+ +#### Example + + post.$reload(); + +### $revert() + +Reset the model to the state it was originally in when you first got it from the API + +#### Example + + post.$revert(); + +### $save(data) + +Persist an instance to the API + +##### Parameters + + +++++ + + + + + + + + + + + + + + +
ParamTypeDetails
data +
+(optional) +
object
+

Data to save to this model instance. Defaults to the result of this.$modified()

+
+ +##### Returns + + ++++ + + + + + + +
object
+

Promise from an API request

+
+ +#### Example + + var post = model('posts').create({ name: 'some post' }); + post.$save(); + + +`$class` +======== + +object in module `ur` + +Description +----------- + +Methods available on the model class + +Analogous to static methods in the OOP world + +You can specify custom class methods: + + yourApp.config(function(modelProvider) { + modelProvider.model('posts', { + $class: { + types: function() { + return ['announcement', 'article'] + } + } + }); + }); + +Methods +------- + +### all(data, headers) + +Retrieve collection of post instances from the API + +##### Parameters + + +++++ + + + + + + + + + + + + + + + + + + + +
ParamTypeDetails
data +
+(optional) +
object
+

Configuration of the request that will be sent to your API

+
headers +
+(optional) +
object
+

Map of custom headers to send to your API

+
+ +##### Returns + + ++++ + + + + + + +
object
+

Promise from an API request

+
+ +#### Example + + model('posts').all().then(function(posts) { + console.log(posts.length); + }); + => 4 + +### create(data) + +Creates an instance on of the model + +##### Parameters + + +++++ + + + + + + + + + + + + + + +
ParamTypeDetails
data +
+(optional) +
object
+

Configuration of the instance that you are creating. Merged with any defaults specified when this model was declared.

+
+ +##### Returns + + ++++ + + + + + + +
object
+

angular-model instance

+
+ +#### Example + + var post = model('Posts').create({}); + +### first(data) + +Retrieve a single post instances from the API + +##### Parameters + + +++++ + + + + + + + + + + + + + + +
ParamTypeDetails
data +
+(optional) +
object
+

Configuration of the request that will be sent to your API

+
+ +##### Returns + + ++++ + + + + + + +
object
+

Promise from an API request

+
+ +#### Example + + model('posts').first({name: 'some post'}).then(function(post) { + console.log(post._id); + }); + => 42 + + +`$collection` +============= + +object in module `ur` + +Description +----------- + +Methods available on model collections + +You can use collection methods to deal with a bunch of instances together. This allows you to have powerful and expressive methods on collections. + +You can specify custom collection methods: + + yourApp.config(function(modelProvider) { + modelProvider.model('posts', { + $collection: { + $hasArchived: function() { + return !angular.isUndefined(_.find(this, { archived: true })); + } + }, + }); + }); + +Methods +------- + +### add(object, data) + +Saves the `object` with `data` + +##### Parameters + + +++++ + + + + + + + + + + + + + + + + + + + +
ParamTypeDetails
objectobject
+

Object to persist data onto

+
data +
+(optional) +
object
+

Data to persist onto the object

+
+ +##### Returns + + ++++ + + + + + + +
boolean
+

true if a $link to name exists on this instance

+
+ +### remove(index) + +Find `index` and delete it from the API, then remove it from the collection + +##### Parameters + + +++++ + + + + + + + + + + + + + + +
ParamTypeDetails
indexnumberobject
+

Either the index of the item in the collection to remove, or the object itself, which will be searched for in the collection

+
+ +##### Returns + + ++++ + + + + + + +
object
+

Promise from the API

+
+ + +`$get` +====== + +function in module `ur` + +Description +----------- + +Get the model class factory + +Usage +----- + +```javascript +$get($http, $parse, $q); +``` + +#### Parameters + + +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
ParamTypeDetails
$httpobject
$parseobject
$qobject
+ +#### Returns + + ++++ + + + + + + +
object
+

The model service

+
+ + +`link` +====== + +directive in module `ur.model` + +Description +----------- + +angular-model will scan your page looking for `` tags. It will use these to work out where your API endpoints are for your angular-model classes. + +So, if you have a "class" Posts, you would define a link with an href pointing to the API endpoint for Posts. This should be a HATEOS-compliant API endpoint. + +Dependencies +------------ + +`model` + +Usage +----- + +as element: +```javascript + + +``` + +#### Parameters + + +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
ParamTypeDetails
relstring
namestring
hrefstring
+ +Example +------- + + + + My Posts Application + + + diff --git a/package.json b/package.json index 24b3f9c..fad35b9 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,16 @@ "name": "John Mercer", "email": "john@radify.io", "web": "http://radify.io" + }, + { + "name": "Gavin Davies", + "email": "gavin@radify.io", + "web": "http://radify.io" + }, + { + "name": "Warren Seymour", + "email": "warren@radify.io", + "web": "http://radify.io" } ], "repository": { @@ -35,6 +45,7 @@ "gulp-istanbul": "^0.10.2", "gulp-jscs": "^3.0.2", "gulp-jshint": "^1.12.0", + "gulp-ngdocs": "^0.2.13", "gulp-nsp": "^2.0.1", "jasmine-core": "^2.3.4", "karma": "^0.13.14", diff --git a/src/angular-model.js b/src/angular-model.js index bc36492..caab631 100644 --- a/src/angular-model.js +++ b/src/angular-model.js @@ -9,8 +9,7 @@ (function(window, angular, undefined) { 'use strict'; -var noop = angular.noop, - forEach = angular.forEach, +var forEach = angular.forEach, extend = angular.extend, copy = angular.copy, isFunc = angular.isFunction, @@ -79,6 +78,35 @@ function serialize(obj, prefix) { return str.join('&'); } +/** + * @ngdoc overview + * @name ur.model + * @requires angular + * @requires window + * @description + * Simple HATEOS-oriented persistence module for AngularJS. + * + * Angular Model is a module that provides a simple way to bind client-side domain logic to JSON-based API resources + * + * By sticking to hypermedia design principles, Angular Model allows you to implement client applications that are + * cleanly decoupled from your server architecture. + * + * angular-model allows you to perform CRUD against an API in a manner similar to Active Record. + * + * In your AngularJS application, include the JavaScript: + ```html + // your specific paths may vary + + ``` + * + * In your app configuration, state a dependency on Angular Model: + + ```javascript + angular.module('myApp', [ + 'ur.model' + ]); + ``` + */ angular.module('ur.model', []).provider('model', function() { var expr, // Used to evaluate expressions; initialized by the service invokation @@ -138,11 +166,66 @@ angular.module('ur.model', []).provider('model', function() { var DEFAULT_METHODS = { - // Methods available on the model class + /** + * @ngdoc object + * @name ur.model:$class + * @description + * Methods available on the model class + * + * Analogous to static methods in the OOP world + * + * You can specify custom class methods: + * + yourApp.config(function(modelProvider) { + modelProvider.model('posts', { + $class: { + types: function() { + return ['announcement', 'article'] + } + } + }); + }); + */ $class: { + /** + * @ngdoc function + * @name all + * @methodOf ur.model:$class + * @param {object=} data Configuration of the request that will be sent to your API + * @param {object=} headers Map of custom headers to send to your API + * + * @description + * Retrieve collection of post instances from the API + * @example + ``` + model('posts').all().then(function(posts) { + console.log(posts.length); + }); + => 4 + ``` + * @returns {object} Promise from an API request + */ all: function(data, headers) { return $request(null, this, 'GET', data, headers); }, + + /** + * @ngdoc function + * @name first + * @methodOf ur.model:$class + * @param {object=} data Configuration of the request that will be sent to your API + * + * @description + * Retrieve a single post instances from the API + * @example + ``` + model('posts').first({name: 'some post'}).then(function(post) { + console.log(post._id); + }); + => 42 + ``` + * @returns {object} Promise from an API request + */ first: function(data) { return this.all(data).then(function(response) { return angular.isArray(response) ? response[0] : response; @@ -150,13 +233,70 @@ angular.module('ur.model', []).provider('model', function() { return null; }); }, + + /** + * @ngdoc function + * @name create + * @methodOf ur.model:$class + * @param {object=} data Configuration of the instance that you are creating. Merged with any defaults + * specified when this model was declared. + * + * @description + * Creates an instance on of the model + * + * @example + ``` + var post = model('Posts').create({}); + ``` + * @returns {object} angular-model instance + */ create: function(data) { return this.instance(deepExtend(copy(this.$config().defaults), data || {})); }, }, - // Methods available on model instances + /** + * @ngdoc object + * @name ur.model:$instance + * @description + * Methods available on model instances + * + * You can use these when you have created or loaded a model instance, see the example + * + var post = model('posts').first({_id: 42}); + console.log(post.name); + => "Post with ID 42" + + post.name = 'renamed'; + post.$save(); + * + * You can specify custom instance methods: + * + yourApp.config(function(modelProvider) { + modelProvider.model('posts', { + $instance: { + $logo: function() { + return this.logo || '/logos/default.png';. + } + } + }); + }); + */ $instance: { + /** + * @ngdoc function + * @name $save + * @methodOf ur.model:$instance + * @description + * Persist an instance to the API + * @example + ``` + var post = model('posts').create({ name: 'some post' }); + post.$save(); + ``` + * @param {object=} data Data to save to this model instance. Defaults to the result of `this.$modified()` + * @returns {object} Promise from an API request + */ $save: function(data) { var method, requestData; @@ -174,12 +314,50 @@ angular.module('ur.model', []).provider('model', function() { return $request(this, this.$model(), method, requestData); }, + + /** + * @ngdoc function + * @name $delete + * @methodOf ur.model:$instance + * @description + * Delete an instance from the API + * @example + ``` + post.$delete(); + ``` + * @returns {object} Promise from an API request + */ $delete: function() { return $request(this, this.$model(), 'DELETE'); }, + + /** + * @ngdoc function + * @name $reload + * @methodOf ur.model:$instance + * @description + * Refresh an instance of a model from the API + * @example + ``` + post.$reload(); + ``` + * @returns {object} Promise from an API request + */ $reload: function() { return $request(this, this.$model(), 'GET'); }, + + /** + * @ngdoc function + * @name $revert + * @methodOf ur.model:$instance + * @description + * Reset the model to the state it was originally in when you first got it from the API + * @example + ``` + post.$revert(); + ``` + */ $revert: function() { var original = copy(this.$original()); @@ -191,15 +369,67 @@ angular.module('ur.model', []).provider('model', function() { this[prop] = original[prop]; } }, + + /** + * @ngdoc function + * @name $exists + * @methodOf ur.model:$instance + * @description + * Checks whether an object exists in the API, based on whether it has an identity URL. + * @example + ``` + if (post.$exists()) { console.log('It exists'); } + ``` + * @returns {boolean} True if the identifier of this instance exists in the API + */ $exists: function() { return !!expr(this, this.$model().$config().identity).get(); }, + + /** + * @ngdoc function + * @name $dirty + * @methodOf ur.model:$instance + * @description + * Returns boolean - true if a model instance is unmodified, else false. Inverse of $pristine. + * @example + ``` + if (post.$pristine()) { console.log('It is just as it was when we got it from the API'); } + ``` + * @returns {boolean} true if a model instance is modified, else false. Inverse of $pristine. + */ $dirty: function() { return !this.$pristine(); }, + + /** + * @ngdoc function + * @name $pristine + * @methodOf ur.model:$instance + * @description + * Returns boolean - false if a model instance is unmodified, else true. Inverse of $dirty. + * @example + ``` + if (post.$dirty()) { console.log('Post has been modified'); } + ``` + * @returns {boolean} true if a model instance is unmodified, else false. Inverse of $dirty. + */ $pristine: function() { return equals(this, this.$original()); }, + + /** + * @ngdoc function + * @name $modified + * @methodOf ur.model:$instance + * @description + * Returns a map of the properties that have been changed + * @example + ``` + console.log(post.$modified()); + ``` + * @returns {object} Map of the fields that have been changed from the $pristine version + */ $modified: function() { var original = this.$original(), diff = {}; @@ -215,6 +445,21 @@ angular.module('ur.model', []).provider('model', function() { return diff; }, + + /** + * @ngdoc function + * @name $related + * @methodOf ur.model:$instance + * @description + * Hydrates the $links property of the instance. $links are used so that an instance + * can tell the client which objects are related to it. For example, a `post` may have an + * `author` object related to it. + * @example + ``` + console.log(post.links()); + ``` + * @returns {object} Promise from the API + */ $related: function(name) { var link, model, instance; @@ -233,16 +478,71 @@ angular.module('ur.model', []).provider('model', function() { return instance.$reload(); }, + + /** + * @ngdoc function + * @name $hasRelated + * @methodOf ur.model:$instance + * @param {string} name Name of the related property to check for + * @description + * Does an instance have a relation of name `name`? + * @example + ``` + if (post.$hasRelated('author')) { console.log('Post has an author'); } + ``` + * @returns {boolean} true if a $link to `name` exists on this instance + */ $hasRelated: function(name) { return isObject(this.$links[name]); } }, - // Methods available on model collections + /** + * @ngdoc object + * @name ur.model:$collection + * @description + * Methods available on model collections + * + * You can use collection methods to deal with a bunch of instances together. This allows you to have powerful + * and expressive methods on collections. + * + * You can specify custom collection methods: + * + yourApp.config(function(modelProvider) { + modelProvider.model('posts', { + $collection: { + $hasArchived: function() { + return !angular.isUndefined(_.find(this, { archived: true })); + } + }, + }); + }); + */ $collection: { + /** + * @ngdoc function + * @name add + * @methodOf ur.model:$collection + * @param {object} object Object to persist data onto + * @param {object=} data Data to persist onto the object + * @description + * Saves the `object` with `data` + * @returns {boolean} true if a $link to `name` exists on this instance + */ add: function(object, data) { return object.$save(data || {}); }, + + /** + * @ngdoc function + * @name remove + * @methodOf ur.model:$collection + * @param {(number|object)} index Either the index of the item in the collection to remove, or the object + * itself, which will be searched for in the collection + * @description + * Find `index` and delete it from the API, then remove it from the collection + * @returns {object} Promise from the API + */ remove: function(index) { index = (typeof index !== 'number') ? index = this.indexOf(index) : index; var self = this, result = self[index].$delete(); @@ -302,13 +602,50 @@ angular.module('ur.model', []).provider('model', function() { } extend(this, { - + /** + * @ngdoc function + * @name ur.model:model + * @param {string} name Name of the 'class', e.g. 'posts' + * @param {object=} options Config to initialise the model 'class' with. You can supply an object literal to + * configure your model here. + * @description + * Main factory function for angular-model + * + * @example + ``` + yourApp.config(function(modelProvider) { + modelProvider.model('posts', { + // configuration options + $instance: { + // custom instance functions + }, + $class: { + // custom class functions + }, + $collection: { + // custom collection functions + } + }); + }); + ``` + * @returns {ur.model} instance of angular-model for the 'class' identified by 'name' + */ model: function(name, options) { config(name, options); return this; }, - // Returns the model service + /** + * @ngdoc function + * @name ur.model:$get + * @description: + * Get the model class factory + * + * @param {object} $http https://docs.angularjs.org/api/ng/service/$http + * @param {object} $parse https://docs.angularjs.org/api/ng/service/$parse + * @param {object} $q https://docs.angularjs.org/api/ng/service/$q + * @return {object} The model service + */ $get: ['$http', '$parse', '$q', function($http, $parse, $q) { q = $q; http = $http; @@ -383,8 +720,34 @@ angular.module('ur.model', []).provider('model', function() { }; } -}).directive('link', ['model', function(model) { +}) +/** + * @ngdoc directive + * @name ur.model.directive:link + * @element link + * @restrict 'E' + * @param {string} rel Must be equal to "resource". + * @param {string} name The name of the angular-model "class" to use. + * @param {string} href Where should angular-model look for the API for this resource. + * @description + * angular-model will scan your page looking for `` tags. It will use these + * to work out where your API endpoints are for your angular-model classes. + * + * So, if you have a "class" Posts, you would define a link with an href pointing to the API endpoint + * for Posts. This should be a HATEOS-compliant API endpoint. + * + * @requires ur.model + * + * @example + ```html + + + My Posts Application + + ``` + */ +.directive('link', ['model', function(model) { return { restrict: 'E', link: function(scope, element, attrs) {