function CollectionFactory(Base, Singleton) {
/**
Base model that represents multiple objects.
@class Collection
@extends Base
@prop {number} length - Number of known items in the instance
@prop {boolean} $busy - If instance is currently in the middle of an API call, equals `true`; else `false`
@prop {boolean} $loaded - If instance has been loaded or instantiated with data, equals `true`; else `false`
@prop {array} $selected - Array of selected items
@prop {number} $selectedCount - Count of items that are currently selected
@prop {boolean} $allSelected - If all known items in the instance are selected, equals `true`; else `false`
@prop {boolean} $noneSelected - If none of the items in the instance are selected, equals `true`; else `false`
@prop {string} $type - The type of model the instance is
*/
var Collection = function() {};
var reSortExpression = /^\s+([+-]?)(.*)\s+$/;
function evalSelected() {
/*jshint validthis:true */
var self = this;
self.$allSelected = false;
self.$noneSelected = true;
if (self.$selectedCount === self.length && self.length > 0) {
self.$allSelected = true;
}
if (self.$selectedCount > 0) {
self.$noneSelected = false;
}
}
function getValue(field, obj) {
var val,
f;
if (m_isString(field) && field.length > 0) {
field = field.split('.');
while (field.length > 0) {
f = field.shift();
if (m_isFunction(obj[f]) === true) {
val = obj[f]();
} else if (m_isObject(val) === false) {
return undefined;
} else {
val = obj[f];
}
}
return val;
}
return undefined;
}
/**
@typedef ChildModel
@type {Singleton}
@prop {Collection} $parent - Link to the parent instance of the child (eg; the Collection instance)
*/
/**
Marks the child model as selected
@name ChildModel.select
@type function
@arg {boolean} value - The value to set the selection to
@arg {boolean} [forBulk] - Passing `true` will prevent re-evaluation of the selected state of the Collection instance (used for bulk selections)
@returns {ChildModel} `this`
*/
/**
* Define constructor
*/
Collection = Base.extend(
/** @lends Collection.prototype */
{
$type: 'Collection',
/**
The model to use when retrieving child objects.
@type {Singleton}
*/
childModel: Singleton,
/**
Instantiates the Collection.
@override
*/
init: function (data, forClone) {
/*jshint unused:false */
var self = this._super.apply(this, arguments);
self.$$data = data || [];
self.$$addData = [];
self.length = self.$$data.length;
self.$loaded = self.length > 0;
self.$$origData = null;
self.$selected = [];
self.$selectedCount = 0;
self.$allSelected = false;
self.$noneSelected = true;
self.$busy = false;
},
/**
Triggers `cb` for each current child in the instance.
@arg {Collection~eachCB} cb - Method to call for each child
@returns {Collection} `this`
*/
/**
Callback for Collection.each.
@callback Collection~eachCB
@param data - The child object
@param {number} index - Index position of the child in the current data for the instance
@this {Singleton} `this`
*/
each: function (cb) {
var self = this;
if (m_isFunction(cb) === true) {
m_forEach(self.get(), cb);
}
return self;
},
/**
Triggers `cb` for each current child in the instance and returns the resulting array.
@arg {Collection~mapCB} cb - Method to call for each child
@returns {Array} Result of the map opperation
*/
/**
Callback for Collection.map.
@callback Collection~mapCB
@param data - The child object
@param {number} index - Index position of the child in the current data for the instance.
@this {Singleton} `this`
*/
map: function (cb) {
if (m_isFunction(cb) === true) {
return map(this.get(), cb);
}
return [];
},
/**
Method to retrieve all the current data for the instance.
@returns {ChildModel[]}
*/
get: function () {
var self = this;
if ( self.$$modeled ) {
return self.$$modeled;
}
self.$$modeled = new Array(self.length);
m_forEach(self.$$data, function (obj, i) {
var ret = new self.childModel(obj);
ret.$parent = self;
ret.select = function (value, forBulk) {
this.$selected = value;
self.$selected[i] = value;
/*jshint -W030 */
value ? self.$selectedCount++ : self.$selectedCount--;
if (forBulk !== true) {
evalSelected.call(self);
}
return this;
};
self.$$modeled[i] = ret;
});
return self.$$modeled;
},
/**
Method to retrieve specific fields from all the current data for the instance.
@returns {Object[]}
*/
pluck: function (fields) {
var self = this,
ret = new Array(self.length);
if (m_isArray(fields) === false) {
fields = [fields];
}
self.each(function (child, idx) {
m_forEach(fields, function (f) {
if (m_isFunction(child[f])) {
ret[idx] = ret[idx] || {};
ret[idx][f] = child[f]();
}
});
});
return ret;
},
/**
Method to set the data for the instance. Also sets `this.$loaded = true`. Will re-apply any sorting/filtering after setting the data.
@arg {array} val - The data to set on the instance
@returns {Collection} `this`
*/
set: function (val) {
var self = this.end(true);
self.$$data = val;
self.length = self.$$data.length;
self.$loaded = self.$loaded || self.length > 0;
self.$$modeled = null;
if (self.$$filter) {
self.filter(self.$$filter);
}
if (self.$$sort) {
self.sort(self.$$sort);
}
return self;
},
/**
Creates one or more linked ChildModels, but does not add them into the current data.
@arg {undefined|null|ChildModel|object|Collection|array} val - The pending data to set on the instance
@returns {ChildModel|Collection|array} `val`
*/
add: function (obj) {
var self = this,
ret = [];
if (m_isUndefined(obj) || obj === null) {
ret.push({});
} else if (obj instanceof self.childModel) {
ret.push(obj);
} else if (obj instanceof Singleton) {
ret.push(obj.get());
} else if (obj instanceof Collection) {
ret = ret.concat(obj.get());
} else if (m_isArray(obj) === true) {
m_forEach(obj, function (i, val) {
ret.push(val);
});
} else if (m_isObject(obj) === true) {
ret.push(obj);
} else {
throw new Error('Invalid object added to Collection: ' + obj);
}
m_forEach(ret, function (obj, i) {
if ((obj instanceof self.childModel) === false) {
if (obj instanceof Singleton) {
obj = obj.get();
}
obj = new self.childModel(obj);
obj.$parent = self;
obj.select = function (value, forBulk) {
this.$selected = value;
self.$selected[i] = value;
/*jshint -W030 */
value ? self.$selectedCount++ : self.$selectedCount--;
if (forBulk !== true) {
evalSelected.call(self);
}
};
ret[i] = obj;
}
});
self.$$addData = ret;
if (obj instanceof Collection || m_isArray(obj) === true) {
return ret;
} else {
return ret[0];
}
},
filter: function (_filter) {
var self = this,
newData = [];
if (self.$$data.length > 0) {
if (m_isFunction(_filter) === true) {
self.$$filter = _filter;
self.$$origData = self.$$origData || m_copy(self.$$data);
self.$$data = filter(self.get(), _filter);
self.length = self.$$data.length;
self.$$modeled = null;
} else if (m_isObject(_filter) === true) {
if (keys(_filter).length > 0) {
self.$$filter = _filter;
self.$$origData = self.$$origData || m_copy(self.$$data);
filter(self.get(), function (val) {
var ret = true;
pick(_filter, function (v, k) {
var value;
if (m_isFunction(val[k]) === true) {
value = val[k]();
} else {
value = val[k];
}
if (m_isArray(value) === true) {
ret = ret && value.indexOf(v) > -1;
} else {
ret = ret && m_equals(value, v);
}
if (ret === false) {
val.select(false);
return ret;
}
});
if (ret === true) {
newData.push(val.get());
}
});
self.$$data = newData;
self.length = self.$$data.length;
self.$$modeled = null;
evalSelected.call(self);
}
} else {
throw new Error('Invalid filter value provided: ' + filter);
}
} else {
self.$$filter = _filter;
}
return self;
},
sort: function (sort, preserveCase) {
var self = this,
len, sf;
function compare(f, descending) {
var field = f;
if (m_isFunction(f) === false) {
f = function (a, b) {
a = getValue(field, a);
b = getValue(field, b);
if (m_isObject(a)) {
a = JSON.stringify(a);
}
if (m_isObject(b)) {
b = JSON.stringify(b);
}
if (preserveCase !== true) {
a = ('' + a).toLowerCase();
b = ('' + b).toLowerCase();
}
if (descending) {
return a > b ? -1 : a < b ? 1 : 0;
}
return a > b ? 1 : a < b ? -1 : 0;
};
}
return f;
}
function baseF(f, descending) {
f = compare(f, descending);
f.next = function (y, d) {
var x = this;
y = compare(y, d);
return baseF(function (a, b) {
return x(a, b) || y(a, b);
});
};
return f;
}
if (self.length > 0) {
if (m_isString(sort) === true) {
sort = sort.split();
}
if (m_isFunction(sort) === true) {
self.$$sort = sort;
self.$$origData = self.$$origData || m_copy(self.$$data);
self.$$modeled = self.get().sort(sort);
} else if (m_isArray(sort) === true && sort.length > 0) {
self.$$origData = self.$$origData || m_copy(self.$$data);
len = sort.reverse().length;
while (--len) {
sort[len] = sort[len].exec(reSortExpression);
if (sort[len].length !== 3) {
throw new Error('Invalid sort value provided: ' + sort[len]);
}
if (sf) {
sf.next(sort[len][2], (sort[len][1] === '-' ? true : false));
} else {
sf = baseF(sort[len][2], (sort[len][1] === '-' ? true : false));
}
}
self.$$modeled = self.get().sort(sf);
} else {
throw new Error('Invalid sort value provided: ' + sort);
}
self.$$data = new Array(self.length);
self.each(function (item, idx) {
self.$$data[idx] = item.get();
});
} else {
self.$$sort = sort;
}
return self;
},
end: function (keepHistory) {
var self = this;
if (self.$$origData !== null) {
self.select(false);
self.$$data = m_copy(self.$$origData);
self.$$addData = [];
self.$$modeled = null;
self.length = self.$$data.length;
self.$$origData = null;
if (keepHistory !== true) {
delete self.$$sort;
delete self.$$filter;
}
}
return self;
},
unique: function (field) {
var self = this,
uniques = {},
ret = [];
if (m_isString(field) && field.length > 0) {
self.each(function (item) {
var val = getValue(field, item);
if (m_isArray(val) === true) {
m_forEach(val, function(v) {
if (m_isObject(v) === true) {
v = JSON.stringify(v);
}
if (uniques[v.toString()] === undefined) {
uniques[v.toString()] = true;
ret.push(v);
}
});
} else {
if (m_isObject(val) === true) {
val = JSON.stringify(val);
}
debugger;
if (val !== null && val !== undefined && uniques[val.toString()] === undefined) {
uniques[val.toString()] = true;
ret.push(val);
}
}
});
}
return ret;
},
select: function (index, value) {
var self = this;
if (index === true) {
self.$selected = new Array(self.length);
self.$selectedCount = 0;
self.each(function (item) {
item.select(true, true);
});
} else if (index === false) {
self.each(function (item) {
item.select(false, true);
});
self.$selected = [];
self.$selectedCount = 0;
} else if (m_isNumber(index) === true) {
self.get()[index].select(value);
}
evalSelected.call(self);
return self;
},
clone: function () {
var self = this,
ret = self._super.apply(self, arguments);
ret.$$data = m_copy(self.$$data);
ret.$$addData = m_copy(self.$$addData);
ret.$$origData = m_copy(self.$$origData);
ret.length = self.length;
ret.$loaded = ret.$loaded;
ret.$selected = self.$selected;
ret.$selectedCount = self.$selectedCount;
ret.$allSelected = self.$allSelected;
ret.$noneSelected = self.$noneSelected;
return ret;
},
resolve: function() {
var self = this;
self.$loaded = true;
delete self.$busy;
return self._super.apply(self, arguments);
},
reject: function() {
var self = this;
self.$loaded = true;
delete self.$busy;
return self._super.apply(self, arguments);
},
/**
Re-runs the last `read` call or, if never called, calls `read`.
@returns {Collection} `this`
*/
refresh: function () {
var self = this;
if (self.$$lastReadData) {
return self.read(self.$$lastReadData);
}
return self.read();
},
/**
Success callback passed into a service.
@arg data - The data resulting from a sucessful service call
@callback Collection~successCallback
*/
/**
Fail callback passed into a service.
@arg data - The data resulting from an erroring service call
@callback Collection~failCallback
*/
/**
Service to read (GET) the data for this instance. Services should return `false` if they are currently invalid.
@arg data - Data to be used during the read
@arg {Collection~successCallback} Success callback for the service
@arg {Collection~failCallback} Failure callback for the service
@abstract
@returns {boolean}
*/
readService: false,
/**
Uses the readService (if defined) to attempt to retrieve the data for the instance. Will finalize the instance.
@arg [data] - Data to be provided to the readService
@returns {Collection} `this`
*/
read: function (data, idx) {
var self = this,
ret;
if (self.$busy === true) {
self.always(function() {
self.read(data, idx);
});
idx = self.unfinalize();
return self;
} else {
idx = idx || self.unfinalize();
}
if (m_isFunction(self.readService)) {
self.$busy = true;
self.$$lastReadData = data || {};
ret = self.readService(
data,
function (data) {
delete self.$errors.read;
self.set(data);
self.resolve(idx);
},
function (data) {
self.$errors.read = data;
self.reject(idx);
}
);
if (ret === false) {
self.$errors.read = true;
self.reject(idx);
}
}
return self;
},
/**
Service to update (PUT) the data for this instance. Services should return `false` if they are currently invalid.
@arg data - Data to be used during the update
@arg {Collection~successCallback} Success callback for the service
@arg {Collection~failCallback} Failure callback for the service
@abstract
@returns {boolean}
*/
updateService: false,
/**
Uses the updateService (if defined) to attempt to update the current data for the instance. Will finalize the instance upon success.
@arg [data] - Data to be provided to the updateService
@returns {Collection} `this`
*/
update: function (data, idx) {
var self = this,
ret;
if (self.$busy === true) {
self.always(function() {
self.update(data, idx);
});
idx = self.unfinalize();
return self;
} else {
idx = idx || self.unfinalize();
}
if (m_isFunction(self.updateService)) {
self.$busy = true;
if (arguments.length === 0) {
delete self.$errors.update;
return self.resolve(idx);
}
ret = self.updateService(
data,
function (data) {
delete self.$errors.update;
self.resolve(idx);
},
function (data) {
self.$errors.update = data;
self.reject(idx);
}
);
if (ret === false) {
self.$errors.update = true;
self.reject(idx);
}
} else {
self.$errors.update = true;
self.reject(idx);
}
return self;
},
}
);
/**
* Return the constructor function
*/
return Collection;
}
angular.module( 'angular-m' )
.factory( 'Collection', [ 'Base', 'Singleton', CollectionFactory ] );