Skip to content

Commit

Permalink
feat(): adds the List type and namespace.
Browse files Browse the repository at this point in the history
Also adds the `List.$asList` method to generate lists from collections and
other lists.

Closes #169
  • Loading branch information
iobaixas committed Dec 10, 2014
1 parent cecdabc commit 7ee7b9c
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 12 deletions.
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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.
Expand Down
39 changes: 39 additions & 0 deletions src/module/api/list-api.js
Original file line number Diff line number Diff line change
@@ -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;
}
};

}]);
50 changes: 39 additions & 11 deletions src/module/factory.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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#
*
Expand Down Expand Up @@ -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

Expand All @@ -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
};

Expand Down Expand Up @@ -531,13 +554,18 @@ 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);
break;
case 'Resource':
helpDefine('Record', name, _fun);
helpDefine('Collection', name, _fun);
helpDefine('List', name, _fun);
helpDefine('Dummy', name, _fun);
break;
default:
Expand Down
3 changes: 3 additions & 0 deletions test/builder-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
});
});

Expand Down
45 changes: 45 additions & 0 deletions test/list-api-spec.js
Original file line number Diff line number Diff line change
@@ -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);
});

});
});

0 comments on commit 7ee7b9c

Please sign in to comment.