From 7ee7b9cee197c235eeba2b28fbbd9cf6be4f57ca Mon Sep 17 00:00:00 2001 From: Ignacio Baixas Date: Wed, 10 Dec 2014 11:29:45 -0300 Subject: [PATCH] feat(): adds the List type and namespace. Also adds the `List.$asList` method to generate lists from collections and other lists. Closes #169 --- README.md | 46 ++++++++++++++++++++++++++++++++++- src/module/api/list-api.js | 39 +++++++++++++++++++++++++++++ src/module/factory.js | 50 +++++++++++++++++++++++++++++--------- test/builder-spec.js | 3 +++ test/list-api-spec.js | 45 ++++++++++++++++++++++++++++++++++ 5 files changed, 171 insertions(+), 12 deletions(-) create mode 100644 src/module/api/list-api.js create mode 100644 test/list-api-spec.js diff --git a/README.md b/README.md index 54ac5ad..982cc2c 100644 --- a/README.md +++ b/README.md @@ -912,7 +912,7 @@ var User = restmod.model('/users').mix({ ## Custom methods -A restmod object is composed of three main APIs, the Model statis API, the record API and the collection API. +A restmod object is composed of three main APIs, the Model static API, the record API and the collection API. Each one of these APis can be extended using the `$extend` block in the object definition: @@ -949,6 +949,7 @@ The following API's are available for extension: * Collection: model collection instance api. * Scope: Same as extending Model and Collection * Resource: Same as extending Record and+ Collection +* List: special api implemented by any record list, including collections So, to add a static method we would use: @@ -978,6 +979,49 @@ var Bike = restmod.model('/bikes').mix({ }); ``` +### Custom methods and Lists + +A `List` namespace is provided for collections and lists, this enables the creation of chainable list methods: + +For example, lets say you need to be able to filter a collection of records and then do something with the resulting list: + +```javascript +var Part = restmod.model('/parts').mix({ + $extend: { + List: { + filterByCategory: function(_category) { + return this.$asList(function(_parts) { + return _.filter(_parts, function(_part) { + return _part.category == _category; + }); + }); + }, + filterByBrand: function(_brand) { + return this.$asList(function(_parts) { + return _.filter(_parts, function(_part) { + return _part.brand == _brand; + }); + }); + }, + getTotalWeight: function(_category) { + return _.reduce(this, function(sum, _part) { + return sum + _part.weight; + }; + } + } + } +}); +``` + +Now, since `List` methods are shared by both collections and lists, you can do: + +```javascript +Part.$search().filterByCategory('wheels').filterByBrand('SRAM').$then(function() { + // use $then because the $asList method will honor promises. + scope.weight = this.getTotalWeight(); +}); +``` + ## Hooks (callbacks) Just like you do with ActiveRecord, you can add hooks on certain steps of the object lifecycle, hooks are added in the `$hooks` block of the object definition. diff --git a/src/module/api/list-api.js b/src/module/api/list-api.js new file mode 100644 index 0000000..7615c20 --- /dev/null +++ b/src/module/api/list-api.js @@ -0,0 +1,39 @@ +'use strict'; + +RMModule.factory('RMListApi', [function() { + + /** + * @class ListApi + * + * @description Common methods for Lists and Collections. + */ + return { + + /** + * @memberof ListApi# + * + * @description Generates a new list from this one. + * + * If called without arguments, the list is popupated with the same contents as this list. + * + * If there is a pending async operation on the host collection/list, then this method will + * return an empty list and fill it when the async operation finishes. If you don't need the async behavior + * then use `$type.list` directly to generate a new list. + * + * @param {function} _filter A filter function that should return the list contents as an array. + * @return {ListApi} list + */ + $asList: function(_filter) { + var list = this.$type.list(), + promise = this.$asPromise(); + + // set the list initial promise to the resolution of the parent promise. + list.$promise = promise.then(function(_this) { + list.push.apply(list, _filter ? _filter(_this) : _this); + }); + + return list; + } + }; + +}]); \ No newline at end of file diff --git a/src/module/factory.js b/src/module/factory.js index 039e0fd..ba167ed 100644 --- a/src/module/factory.js +++ b/src/module/factory.js @@ -1,7 +1,7 @@ 'use strict'; -RMModule.factory('RMModelFactory', ['$injector', 'inflector', 'RMUtils', 'RMScopeApi', 'RMCommonApi', 'RMRecordApi', 'RMCollectionApi', 'RMExtendedApi', 'RMSerializer', 'RMBuilder', - function($injector, inflector, Utils, ScopeApi, CommonApi, RecordApi, CollectionApi, ExtendedApi, Serializer, Builder) { +RMModule.factory('RMModelFactory', ['$injector', 'inflector', 'RMUtils', 'RMScopeApi', 'RMCommonApi', 'RMRecordApi', 'RMListApi', 'RMCollectionApi', 'RMExtendedApi', 'RMSerializer', 'RMBuilder', + function($injector, inflector, Utils, ScopeApi, CommonApi, RecordApi, ListApi, CollectionApi, ExtendedApi, Serializer, Builder) { var NAME_RGX = /(.*?)([^\/]+)\/?$/, extend = Utils.extendOverriden; @@ -41,15 +41,16 @@ RMModule.factory('RMModelFactory', ['$injector', 'inflector', 'RMUtils', 'RMScop } var Collection = Utils.buildArrayType(), + List = Utils.buildArrayType(), Dummy = function(_asCollection) { this.$isCollection = _asCollection; - this.$initialize(); + this.$initialize(); // TODO: deprecate this }; - // Collection factory (since a constructor cant be provided...) - function newCollection(_scope, _params) { + // Collection factory + function newCollection(_params, _scope) { var col = new Collection(); - col.$scope = _scope; + col.$scope = _scope || Model; col.$params = _params; col.$initialize(); return col; @@ -93,9 +94,7 @@ RMModule.factory('RMModelFactory', ['$injector', 'inflector', 'RMUtils', 'RMScop }, // creates a new collection bound by default to the static scope - $collection: function(_params, _scope) { - return newCollection(_scope || Model, _params); - }, + $collection: newCollection, // gets scope url $url: function() { @@ -203,6 +202,21 @@ RMModule.factory('RMModelFactory', ['$injector', 'inflector', 'RMUtils', 'RMScop return new Dummy(_asCollection); }, + /** + * Creates a new record list. + * + * A list is a ordered set of records not bound to a particular scope. + * + * Contained records can belong to any scope. + * + * @return {List} the new list + */ + list: function(_items) { + var list = new List(); + if(_items) list.push.apply(list, _items); + return list; + }, + /** * @memberof StaticApi# * @@ -361,10 +375,18 @@ RMModule.factory('RMModelFactory', ['$injector', 'inflector', 'RMUtils', 'RMScop // provide collection constructor $collection: function(_params, _scope) { _params = this.$params ? angular.extend({}, this.$params, _params) : _params; - return newCollection(_scope || this.$scope, _params); + return newCollection(_params, _scope || this.$scope); } - }, ScopeApi, CommonApi, CollectionApi, ExtendedApi); + }, ListApi, ScopeApi, CommonApi, CollectionApi, ExtendedApi); + + ///// Setup list api + + extend(List.prototype, { + + $type: Model + + }, ListApi, CommonApi); ///// Setup dummy api @@ -384,6 +406,7 @@ RMModule.factory('RMModelFactory', ['$injector', 'inflector', 'RMUtils', 'RMScop Model: Model, Record: Model.prototype, Collection: Collection.prototype, + List: List.prototype, Dummy: Dummy.prototype }; @@ -531,6 +554,10 @@ RMModule.factory('RMModelFactory', ['$injector', 'inflector', 'RMUtils', 'RMScop switch(api) { // Virtual API's + case 'List': + helpDefine('Collection', name, _fun); + helpDefine('List', name, _fun); + break; case 'Scope': helpDefine('Model', name, _fun); helpDefine('Collection', name, _fun); @@ -538,6 +565,7 @@ RMModule.factory('RMModelFactory', ['$injector', 'inflector', 'RMUtils', 'RMScop case 'Resource': helpDefine('Record', name, _fun); helpDefine('Collection', name, _fun); + helpDefine('List', name, _fun); helpDefine('Dummy', name, _fun); break; default: diff --git a/test/builder-spec.js b/test/builder-spec.js index 6d256e5..82b29d3 100644 --- a/test/builder-spec.js +++ b/test/builder-spec.js @@ -68,6 +68,7 @@ describe('Restmod builder:', function() { test6: fun }); this.define('Dummy.test7', fun); + this.define('List.test8', fun); }); expect(Bike.$new().test1).toBeDefined(); @@ -82,6 +83,8 @@ describe('Restmod builder:', function() { expect(Bike.$new().test6).toBeDefined(); expect(Bike.dummy().test6).toBeDefined(); expect(Bike.dummy().test7).toBeDefined(); + expect(Bike.list().test8).toBeDefined(); + expect(Bike.$collection().test8).toBeDefined(); }); }); diff --git a/test/list-api-spec.js b/test/list-api-spec.js new file mode 100644 index 0000000..50d48fc --- /dev/null +++ b/test/list-api-spec.js @@ -0,0 +1,45 @@ +'use strict'; + +describe('Restmod list:', function() { + + var restmod, $httpBackend, Bike; + + beforeEach(module('restmod')); + + beforeEach(inject(function($injector) { + restmod = $injector.get('restmod'); + $httpBackend = $injector.get('$httpBackend'); + Bike = restmod.model('/api/bikes'); + })); + + describe('$asList', function() { + + it('should retrieve a list with same contents as collection if called with no arguments', function() { + var query = Bike.$collection().$decode([ { model: 'Slash' }, { model: 'Remedy' } ]); + var list = query.$asList().$asList().$asList(); + expect(list.length).toEqual(2); + expect(list[0]).toBe(query[0]); + expect(list[1]).toBe(query[1]); + }); + + it('should wait for last aync operation before populating list', function() { + $httpBackend.when('GET', '/api/bikes').respond([ { model: 'Slash' }, { model: 'Remedy' } ]); + var list = Bike.$search().$asList().$asList(); + expect(list.length).toEqual(0); + $httpBackend.flush(); + expect(list.length).toEqual(2); + }); + + it('should accept a transformation function as parameter', function() { + $httpBackend.when('GET', '/api/bikes').respond([ { model: 'Slash' }, { model: 'Remedy' } ]); + var list = Bike.$search().$asList(function(_original) { + return [_original[1]]; + }).$asList(); + expect(list.length).toEqual(0); + $httpBackend.flush(); + expect(list.length).toEqual(1); + }); + + }); +}); +