/*!
* @license NgModel for AngularJS
* (c) 2015 AyeCue
* License: MIT
* This is inspired by:
* ExtJS modellayer (https://docs.sencha.com/extjs/5.1/5.1.0-apidocs/#!/api/Ext.data.Model),
* ActiveRecord (http://github.com/bfanger/angular-activerecord)
*/
s
s
s
s
s
s
s
s
/**
* Batch for NgModel operations
*
* @class Batch
* @constructor
* @param {NgModel} model Model to use in batch.
* @param {String} operation Operation type like 'read' or 'delete'.
* @param {Object} [options] Extra options for batch.
* @private
*/
function Batch(model,operation,options){
var me = this;
angular.extend(me,{
model: model,
operation: operation,
options: options || {}
});
me.initOptions();
};
Batch.prototype = {
/**
* Initialize all options for batch.
*
* @method initOptions
* @return {Object}
* @private
*/
initOptions: function(){
var me = this,
model = me.model,
options = me.options;
options.data = options.data || {};
if (model.appendData[me.operation]) {
angular.extend(options.data,model.getRequestData());
angular.forEach(model.notPersist,function(toExclude) {
removeNamespace(options.data,toExclude,true);
});
}
if (!options.headers) {
options.headers = model.headers;
} else {
angular.extend(options.headers,model.headers);
}
if (!options.method) {
options.method = model.actionMethods[me.operation];
}
if (!options.url) {
options.url = model.buildUrl(me.operation,options);
}
return options;
},
/**
* Get if batch is reading.
*
* @method isReading
* @return {Boolean}
* @private
*/
isReading: function(){
return this.operation === 'read';
},
/**
* Get if batch is writing.
*
* @method isWriting
* @return {Boolean}
* @private
*/
isWriting: function(){
return this.operation === 'create' || this.operation === 'update';
},
/**
* Get options property of batch.
*
* @method get
* @return {Object}
* @private
*/
get: function(){
return this.options;
}
};
/**
* Process filters through data object.
*
* @param {Object} [filters] Collection of filters you want to apply.
* @param {Object} [properties] Object of the data.
* @private
*/
function applyFilters (filters, properties) {
if (!filters) {
return;
}
angular.forEach(filters, function (filter,namespace) {
var value = getNamespace(properties,namespace),
newValue;
if (isDefined(value)) {
newValue = angular.isFunction(filter) ? filter(value) : $parse(namespace + '|' + filter)(properties);
setNamespace(properties,namespace,newValue);
}
});
}
/**
* Check if value is defined.
*
* @param {Mixed} obj Value to check if it's defined.
* @return Boolean
* @private
*/
function isDefined (obj) {
return obj !== undefined && obj !== null;
}
/**
* Extend properties to target class.
*
* @param {Object} properties Properties to extend.
* @param {Mixed} to Target class.
* @private
*/
function extendTo(properties,to){
angular.forEach(properties,function(value,key){
if (!properties.hasOwnProperty(key)) {
return;
}
if (angular.isFunction(value)) {
value.$previous = to[key];
}
to[key] = value;
});
}
/**
* Call parent class method
*
* @param {Mixed} [args] Arguments you need in the parent method.
* @param {String} [method] If you use strict mode you may have to this argument to define the method you want to call.
* @return Mixed
* @private
*/
function callParent(args,method){
var caller = this[method] || this.callParent.caller || arguments.callee.caller || arguments.caller;
if (!caller) {
if (isStrict()) {
throw new Error('You are in strict mode use the second parameter in "callParent".');
}
throw new Error('The method "callParent" failed because cannot identify caller.');
}
return caller.$previous.apply(this,args);
}
/**
* Check if strict
*
* @return Boolean
* @private
*/
function isStrict() {
return !this;
}
/**
* Set namespace in object.
*
* @param {Object} obj Object you want to manipulate.
* @param {String} id Namespace for value you want to set.
* @param {Mixed} value Value for namespace.
* @private
*/
function setNamespace(obj,id,value){
$parse(id).assign(obj,value);
}
/**
* Get namespace in object.
*
* @param {Object} obj Object you want to search through.
* @param {String} id Namespace for value you want to get.
* @return {Mixed}
* @private
*/
function getNamespace(obj,id){
return $parse(id)(obj);
}
/**
* Remove namespace in object.
*
* @param {Object} obj Object you want to manipulate.
* @param {String} id Namespace for value you want to remove.
* @param {Boolean} [autoDelete] Auto delete parent if object is empty.
* @private
*/
function removeNamespace(obj,id,autoDelete){
var parts = id.split('.'),
last = parts.pop(),
pre = parts.join('.'),
root = getNamespace(obj,pre);
root[last] = null;
delete root[last];
if (autoDelete) {
for (var key in root) {
if (root.hasOwnProperty(key)) {
return;
}
}
removeNamespace(obj,pre,autoDelete);
}
}
/**
* Modellayer for AngularJS
*
* @class NgModel
* @constructor
* @param {Object} [properties] Start properties of record.
*/
function NgModel (properties){
var me = this;
me.response = null;
me.data = angular.extend({},me.defaultData,properties);
me.initialize.apply(me, arguments);
}
/**
* Create new child type of model.
*
* @method create
* @static
* @param {Object} [properties] prototypeProperties for child model prototype.
* @param {Object} [statics] staticProperties for child model static properties.
* @return {Function} Constructor
*/
NgModel.create = function(properties,statics){
var extend = this,
sub;
properties = properties || {};
statics = statics || {};
if (properties.hasOwnProperty('constructor')) {
sub = properties.constructor;
properties.constructor = null;
delete properties.constructor;
} else {
sub = function(){
return extend.apply(this,arguments);
};
}
sub.constructor.$previous = extend.constructor;
sub.prototype = Object.create(extend.prototype);
sub.$super = extend;
angular.extend(sub,extend);
extendTo(properties,sub.prototype);
extendTo(statics,sub);
return sub;
};
/**
* Fetch one record.
*
* @method fetchOne
* @static
* @param {Mixed} id
* @param {Object} [options] Additional options for request.
* @return $q.promise
*/
NgModel.fetchOne = function (id,options){
var model = new this();
model.setId(id);
return model.fetch(options);
};
/**
* Fetch collection of records.
*
* @method fetchAll
* @static
* @param {Object} [options] Additional options for request.
* @return $q.promise
*/
NgModel.fetchAll = function (options){
var model = this,
instance = new model(),
deferred = $q.defer();
instance.sync('read',instance,options).then(function (response){
var data = instance.parse(response.data, options),
models, filters;
if (angular.isArray(data)) {
models = [];
filters = model.prototype.readFilters;
angular.forEach(data, function (item) {
applyFilters(filters, item);
models.push(new model(item));
});
deferred.resolve(models);
} else {
deferred.reject('Not a valid response, expecting an array');
}
}, deferred.reject);
return deferred.promise;
};
/**
* Call parent class method
*
* @method callParent
* @static
* @private
* @param {Mixed} [args] Arguments you need in the parent method.
* @return {Mixed}
*/
NgModel.callParent = callParent;
NgModel.prototype = {
/**
* The default namespace for the id property.
*
* @property idProperty
* @type String
* @default "id"
*/
idProperty: 'id',
/**
* The default propertiers of a record.
*
* @property defaultData
* @type Object
* @default {}
*/
defaultData: {},
/**
* The default headers for a request.
*
* @property headers
* @type Object
* @default {}
*/
headers: {},
/**
* The root namespace which contain the data.
*
* @property resultRoot
* @type String
* @default null
*/
resultRoot: null,
/**
* The main request url.
*
* @property url
* @type String
* @default null
*/
url: null,
/**
* Filter for data processing when reading.
*
* @property readFilters
* @type Object
* @default null
*/
readFilters: null,
/**
* Filter for data processing when writing.
*
* @property writeFilters
* @type Object
* @default null
*/
writeFilters: null,
/**
* Exclude non persit data from request.
*
* @property notPersist
* @type Array<String>
* @default []
*/
notPersist: [],
/**
* Define your model crud schema.
*
* @property actionMethods
* @type Object
* @default {'create': 'POST','read': 'GET','update': 'PUT','delete': 'DELETE'}
*/
actionMethods: {
'create': 'POST',
'read': 'GET',
'update': 'PUT',
'delete': 'DELETE'
},
/**
* Specify certain url for certain operations (optional).
*
* @property api
* @type Object
* @default {'create': null,'read': null,'update': null,'delete': null}
*/
api: {
'create': null,
'read': null,
'update': null,
'delete': null
},
/**
* Specify for which operation your model should append data to the request.
*
* @property appendData
* @type Object
* @default {'create': true,'read': false,'update': true,'delete': false}
*/
appendData: {
'create': true,
'read': false,
'update': true,
'delete': false
},
/**
* Model initialization
*
* @method initialize
* @private
* @param {Object} [properties] Start properties of record.
* @param {Object} [options] Additional options for model.
*/
initialize: function (properties, options) {
var me = this;
options = options || {};
if (properties) {
if (options.parse) {
properties = me.parse(properties);
}
if (options.readFilters) {
applyFilters(me.readFilters, properties);
}
me.extend(properties);
me.previousProperties = function () {
return properties;
};
}
if (options.url) {
me.url = options.url;
}
},
/**
* Check if model data has changed since the last sync.
*
* @method hasChanged
* @param {String} [property] Property to control.
* @return {Boolean}
*/
hasChanged: function (property) {
var me = this,
changed = me.changedProperties();
if (property) {
return property in changed;
}
for (var i in changed) {
return true;
}
return false;
},
/**
* Create object which contains all changed properties.
*
* @method changedProperties
* @param {Object} [diff] An object to diff against, determining if there would be a change.
* @return {Object}
*/
changedProperties: function (diff) {
var me = this,
current = diff || me.data,
changed = {},
previousProperties = me.previousProperties();
if (!isDefined(diff)) {
angular.forEach(previousProperties,function(value,property){
if (!isDefined(value)) {
changed[property] = value;
}
});
}
angular.forEach(current,function(value,property){
if (current.hasOwnProperty(property) && !angular.equals(value, previousProperties[property]) === false) {
changed[property] = value;
}
});
return changed;
},
/**
* Get the state of a property before the last change.
*
* @method previous
* @param {String} property Property which you want to get.
* @return {Mixed}
*/
previous: function (property) {
var me = this,
previous = me.previousProperties();
return getNamespace(previous,property);
},
/**
* Get the state of data before the last change.
*
* @method previousProperties
* @return {Object}
*/
previousProperties: function () {
return {};
},
/**
* Read a record.
*
* @method fetch
* @param {Object} [options] Additonal request options.
* @return $q.promise
*/
fetch: function (options) {
var me = this,
deferred = $q.defer();
me.sync('read', me, options).then(function (response) {
var data = me.parse(response.data, options),
status = response.status,
success = status < 400;
me.response = response;
if (success && angular.isObject(data)) {
applyFilters(me.readFilters, data);
me.extend(data);
me.previousProperties = function () {
return data;
};
deferred.resolve(me);
} else if (success) {
deferred.resolve(me);
} else {
deferred.reject(me);
}
},function(response){
me.response = response;
deferred.reject(me);
});
return deferred.promise;
},
/**
* Save/Update a record.
*
* @method save
* @param {Object} [options] Additonal request options.
* @return $q.promise
*/
save: function (options) {
var me = this,
operation = me.isNew() ? 'create' : 'update',
deferred = $q.defer();
me.sync(operation, me, options).then(function (response) {
var data = me.parse(response.data, options),
status = response.status,
success = status < 400;
me.response = response;
if (success && angular.isObject(data)) {
applyFilters(me.readFilters, data);
me.extend(data);
me.previousProperties = function () {
return data;
};
deferred.resolve(me);
} else if (success) {
deferred.resolve(me);
} else {
deferred.reject(me);
}
},function(response){
me.response = response;
deferred.reject(me);
});
return deferred.promise;
},
/**
* Delete a record.
*
* @method destroy
* @param {Object} [options] Additonal request options.
* @return $q.promise
*/
destroy: function (options) {
var me = this,
deferred = $q.defer();
if (me.isNew()) {
deferred.resolve(me);
return deferred.promise;
}
me.sync('delete', me, options).then(function (response) {
me.response = response;
deferred.resolve(me);
}, function(response){
me.response = response;
deferred.reject(me);
});
return deferred.promise;
},
/**
* Sync a record.
*
* @method sync
* @private
* @param {String} operation Indicates which operation should be done.
* @param {NgModel} model Record used for request.
* @param {Object} [options] Additonal request options.
* @return $q.promise
*/
sync: function (operation, model, options) {
var me = this,
batch = new Batch(model,operation,options);
options = batch.get();
if (batch.isWriting()) {
applyFilters(me.writeFilters,options.data);
}
return $http(options);
},
/**
* Put together url for request.
*
* @method buildUrl
* @private
* @param {String} operation Indicates which operation should be done.
* @param {Object} [options] Batch options.
* @return {String}
*/
buildUrl: function (operation,options){
var me = this,
url = me.api[operation] || me.url,
data = angular.extend({},me.data,options.data),
formatedUrl = $interpolate(url)(data);
if (!isDefined(data[me.idProperty])) {
return formatedUrl;
}
if (!isDefined(formatedUrl)) {
throw 'Implement this.buildUrl() or specify this.url';
}
return $interpolate('{{root}}{{end}}{{id}}')({
root: formatedUrl,
end: formatedUrl.charAt(formatedUrl.length - 1) === '/' ? '' : '/',
id: encodeURIComponent(data[me.idProperty])
});
},
/**
* Parse raw data from request response.
*
* @method parse
* @private
* @param {Object} data Raw request response data.
* @param {Object} [options] Request options.
* @return {Object}
*/
parse: function (data, options) {
if (!isDefined(this.resultRoot)) {
return data;
}
return getNamespace(data,this.resultRoot);
},
/**
* Indicates if record is new or not (important for save/update operation).
*
* @method isNew
* @return {Boolean}
*/
isNew: function () {
return !isDefined(this.getId());
},
/**
* Getter for data property.
*
* @method toJSON
* @return {Object}
*/
toJSON: function (){
return this.data;
},
/**
* Easy way to extend model to a scope.
*
* @method toScope
* @param {Object} scope Scope you want to add the model.
* @param {String} key Property where you want to add the model to.
* @return {NgModel}
*/
toScope: function(scope,key){
scope[key] = this.data;
return this;
},
/**
* Process data for batch (advised to extend as soon as you want extended data processing for requests).
*
* @method getRequestData
* @private
* @return {Object}
*/
getRequestData: function (){
return this.data;
},
/**
* Get id of record.
*
* @method getId
* @return {Mixed}
*/
getId: function (){
return this.data[this.idProperty];
},
/**
* Change id of record.
*
* @method setId
* @param {Mixed} id Id value for record.
* @return {Mixed}
*/
setId: function (id){
this.data[this.idProperty] = id;
return this;
},
/**
* Getter for certain data properties.
*
* @method get
* @param {String} key Namespace for value you want to get.
* @return {Mixed}
*/
get: function (key){
return getNamespace(this.data,key);
},
/**
* Setter for certain data properties.
*
* @method set
* @param {String} key Namespace for value you want to set.
* @param {Mixed} value Value you want to set.
* @return {Mixed}
*/
set: function (key,value){
setNamespace(this.data,key,value);
return this;
},
/**
* Create a new object which all filtered data wanted.
*
* @method range
* @param {Mixed} [...] Property string you want to export or object with from/to information like {'myProp':'myNewProp'}.
* @return {Object}
*/
range: function (){
var me = this,
result = {};
angular.forEach(arguments,function(item){
var from = item,
to = item;
if (angular.isObject(item)) {
from = item.from;
to = item.to;
}
result[from] = me.data[to];
});
return result;
},
/**
* Easily extend the data object of your model.
*
* @method extend
* @param {Object} data Data to extend.
* @return {NgModel}
*/
extend: function (data){
angular.extend(this.data,data);
return this;
},
/**
* Call parent class method
*
* @method callParent
* @private
* @param {Mixed} [args] Arguments you need in the parent method.
* @return {Mixed}
*/
callParent: callParent
};
s
s
s