radify/angular-scaffold

View on GitHub
src/angular-scaffold.js

Summary

Maintainability
D
1 day
Test Coverage
/**
 * Angular Scaffold is a collection of convenience wrappers around angular-model collections.
 *
 * {@copyright 2015, Radify, Inc (http://radify.io/)}
 * {@link https://github.com/radify/angular-model#readme}
 *
 * @license BSD-3-Clause
 */
(function(window, angular, undefined) {
'use strict';

/**
 * @ngdoc overview
 * @name ur.scaffold
 * @requires angular
 * @requires ur.model
 * @description
 * Angular Scaffold is a collection of convenience wrappers around angular-model collections.
 */
angular.module('ur.scaffold', ['ur.model'])
/**
 * @ngdoc object
 * @name ur.scaffold
 * @requires angular
 * @requires ur.model
 * @description
 * Provider for angular-scaffold
 *
 *      var s = scaffold("Dogs", {
 *       paginate: { size: 1, strategy: 'infinite' }
 *     });
 */
.provider('scaffold', function() {
    var modelClass, q;
    var registry = {};

    function ScaffoldClass(options) {
        var self = this,
            config = angular.extend({ query: {} }, options),
            paginate = { size: 10, page: 1, strategy: 'paged' },
            total = 0;

        if (angular.isObject(options.paginate)) {
            config.paginate = angular.extend({}, paginate, options.paginate);
        }

        if (options.paginate === true) {
            config.paginate = paginate;
        }

        function paginateHeaders() {
            if (config.paginate) {
                var size = config.paginate.size,
                    page = config.paginate.page,
                    first = size * (page - 1),
                    last  = (size * page) - 1;

                return {
                    'Range': 'resources=' + first + '-' + last
                };
            }

            return null;
        }

        function getPages(headers) {
            if(!config.paginate || !headers['content-range']) {
                return [];
            }

            var regex = /^resources \d+-\d+\/(\d+|\*)$/,
                matches = headers['content-range'].match(regex);

            if (matches === null || angular.isUndefined(matches[1]) || matches[1] === '*') {
                return null;
            }

            total = matches[1];
            return Math.ceil(total / config.paginate.size);
        }

        angular.extend(this, {

            /**
             * @ngdoc number
             * @name ur.scaffold:pages
             * @propertyOf ur.scaffold
             * @description
             * the count of pages that the API reported to the scaffold
             */
            pages: null,

            /**
             * @ngdoc object
             * @name ur.scaffold:$ui
             * @propertyOf ur.scaffold
             * @description
             * User interface related convenience properties, so in your UI, you can show saving and loading states
             */
            $ui: {
                loading: false,
                saving: false,
            },

            /**
             * @ngdoc function
             * @name ur.scaffold:$config
             * @methodOf ur.scaffold
             * @description
             * Get the configuration for this angular-scaffold instance
             * @returns {object} Configuration of this angular-scaffold instance
             */
            $config: function() {
                return config;
            },

            /**
             * @ngdoc function
             * @name ur.scaffold:$init
             * @methodOf ur.scaffold
             * @description
             * Initialise this scaffold
             * @returns {object} Returns this object, supporting method chaining
             */
            $init: function() {
                if (modelClass && !angular.isObject(config.model)) {
                    config.model = modelClass(config.model);
                }

                return this.refresh();
            },

            /**
             * @ngdoc function
             * @name ur.scaffold:model
             * @methodOf ur.scaffold
             * @description
             * Returns the configuration for this angular-scaffold instance
             * @returns {string} The underlying model this object is configured with
             */
            model: function() {
                return config.model;
            },

            /**
             * @ngdoc function
             * @name ur.scaffold:query
             * @methodOf ur.scaffold
             * @description
             * Get the configuration for this angular-scaffold instance
             *
             * @param {object} query e.g. { name: 'name to search for' }
             * @param {number=} page If supplied, used to determine which page of results to show
             * @returns {object} Configuration of this angular-scaffold instance
             */
            query: function(query, page) {
                config.query = query;

                if (page) {
                    return this.page(page);
                }

                return this.refresh();
            },

            /**
             * @ngdoc function
             * @name ur.scaffold:page
             * @methodOf ur.scaffold
             * @description
             * Select a page
             *
             * @param {number} page Page of results to show
             * @returns {object} Configuration of this angular-scaffold instance
       */
            page: function(page) {
                config.paginate.page = page;

                if (config.paginate.strategy === 'infinite') {
                    return this.refresh({append: true});
                }

                return this.refresh();
            },

            /**
             * @ngdoc function
             * @name ur.scaffold:total
             * @methodOf ur.scaffold
             * @description
             * How many results in total are in the scaffold (not just the current page)
             *
             * @returns {number} Total results
             */
            total: function() {
                return total;
            },

            /**
             * @ngdoc function
             * @name ur.scaffold:refresh
             * @methodOf ur.scaffold
             * @description
             * Go to the API and refresh
             * @param {object=} options Query options to pass to the underlying angular-model `all` query.
       * Includes `callback`, which can be a callback function for when the refresh completes.
             *
             * @returns {object} Returns this object, supporting method chaining
             */
            refresh: function(options) {
                options = options || {
                    append: false
                };

                this.$ui.loading = true;

                var promise = config.model.all(config.query, paginateHeaders());

                var success = function(data) {
                    self.pages = getPages(promise.$response.headers());

                    if (options.append === true) {
                        self.items = self.items.concat(data);
                        return data;
                    }

                    self.items = data;

                    return data;
                };

                var error = function(data) {
                    self.items = [];
                };

                var cleanup = function() {
                    self.$ui.loading = false;
                };

                if (angular.version.minor > 1) {
                    promise.then(success).catch(error).finally(cleanup);
                } else {
                    promise.then(success, error).always(cleanup);
                }

                if (angular.isFunction(config.callback)) {
                    promise.then(config.callback);
                }

                if (angular.isFunction(options.callback)) {
                    promise.then(options.callback);
                }

                return this;
            },

            /**
             * @ngdoc function
             * @name create
             * @methodOf ur.scaffold
             * @description
             * Creates a new object when the deferred promise is resolved
             * @returns {object} Deferred promise
             */
            create: function() {
                var deferred = q.defer(),

                    defaults = {
                        save: true
                    },

                    saved = function() {
                        self.items.push(deferred.$instance);
                        self.$ui.saving = false;
                    };

                deferred.promise.then(function(options) {
                    self.$ui.saving = true;

                    options = angular.extend({}, defaults, options);

                    if (options.save === false) {
                        return saved();
                    }

                    deferred.$instance.$save().then(saved);
                });

                deferred.$instance = config.model.create();

                return deferred;
            },

            /**
             * @ngdoc function
             * @name edit
             * @methodOf ur.scaffold
             * @param {(number|object)} index Either the index of the item in the collection to edit,
             *   or the object itself, which will be searched for in the collection
             * @description
             * Find `index` and set it up for editing.
             *
             * Updates the object when the deferred promise is resolved
             * @returns {object} Deferred promise
             */
            edit: function(index) {
                var deferred = q.defer(),

                    defaults = {
                        save: true
                    },

                    saved = function() {
                        self.items[index] = deferred.$instance;
                        self.$ui.saving = false;
                    };

                deferred.promise.then(function(options) {
                    self.$ui.saving = true;

                    options = angular.extend({}, defaults, options);

                    if (options.save === false) {
                        return saved();
                    }

                    deferred.$instance.$save().then(saved);
                });

                deferred.$instance = angular.copy(this.items[index]);

                return deferred;
            },

            /**
             * @ngdoc function
             * @name delete
             * @methodOf ur.scaffold
             * @param {(number|object)} index Either the index of the item in the collection to remove, or the object
             *     itself, which will be searched for in the collection
             * @description
             * Find `index` and delete it from the API, then remove it from the collection
             * @returns {object} Promise
             */
            delete: function(index) {
                var deferred = q.defer();

                deferred.promise.then(function(data) {
                    self.$ui.saving = true;

                    deferred.$instance.$delete().then(function() {
                        self.$ui.saving = false;
                    });
                });

                deferred.$instance = this.items[index];

                return deferred;
            }
        });
    }

    function config(name, options) {
        if (registry[name]) {
            var current = registry[name].$config();
            registry[name] = new ScaffoldClass(angular.extend({}, current, options));
        } else {
            if (angular.isUndefined(options.model)) {
                options.model = name;
            }

            registry[name] = new ScaffoldClass(options);
        }

        return registry[name];
    }

    angular.extend(this, {

        /**
         * @ngdoc function
         * @name ur.scaffoldProvider:scaffold
         *
         * @description
         * Configure a scaffold
         *
         * @example
         * var s = scaffold("Dogs", {
                    paginate: true
             });
         * @param {string} name The name
         * @param {object=} options Configuration object
         * @returns {object} Created scaffold object
     */
        scaffold: function(name, options) {
            config(name, options);
            return this;
        },

        $get: ['$q', 'model', function($q, model) {
            modelClass = model;
            q = $q;

            function ScaffoldClassFactory(name, options) {
                if (!angular.isUndefined(options)) {
                    config(name, options);
                }

                return registry[name].$init() || undefined;
            }

            return ScaffoldClassFactory;
        }]
    });
});

})(window, window.angular);