API Docs for: 0.1.6
Show:

File: temp/NgModel.js

/*!
 * @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