UNPKG

basap

Version:

Angular 1.x ES6 base app register module.

780 lines (657 loc) 25.3 kB
import angular from 'angular'; import Logger from './logger'; import Area from './area'; import configs from './configs'; import BaseCtrl from './baseCtrl'; import RouteCtrl from './routeCtrl'; /** * Angular application. * @class */ class Base { /** * Base constructor. * @constructor * @param ns - the app namespace * @param [deps] - the main module dependencies. * @param [options] - area default options. * @returns {Base} */ constructor(ns, deps, options) { // normalize route module name. function normalizeRouter(m) { if(m.indexOf('.') !== -1){ m = m.split('.'); if(angular.isArray(m)) return (m[0] + m[1].charAt(0).toUpperCase() + m[1].slice(1)); return m + m.charAt(0).toUpperCase() + m.slice(1); } return m; } // lookup verify supported router. // TODO: need to change this so can't be found other than by initial array of deps. function lookupRouter(arr){ arr = arr || []; let filtered = arr.filter(function (a) { return /^(ngRoute|ngNewRouter|ui\.router)$/.test(a); }); if(filtered.length > 1) throw new Error(`Only one router can be initialized attempted to initialize with ${filtered.join(', ')}`); if(!filtered || !filtered.length) throw new Error('Attempted to initialize using router module of undefined. ' + 'Supported Modules: ngRoute, ngNewRouter, ui.router'); return normalizeRouter(filtered[0]); } if(!angular.isString(ns)){ if(angular.isObject(ns) && !angular.isArray(ns)){ options = ns; deps = undefined; ns = undefined; } if(angular.isArray(ns)){ options = deps; deps = ns; ns = undefined; } } // allow options as second arg. if(!angular.isArray(deps) && angular.isObject(deps)){ options = deps; deps = undefined; } options = options || {}; // allow dependencies in options if(options.dependencies){ deps = options.dependencies; delete options.dependencies; } // define root namespace for app. this.ns = ns || 'app'; // an alternate name used in page // title. in some cases you may wish // to use an abbreviated ns which would // not good for display this property // allows for displaying a more readable // title. this.name = undefined; // main module dependencies. this.dependencies = deps || []; // create module this.module = angular.module(this.ns, this.dependencies); // the element to bootstrap this.element = document; // app areas. this.areas = {}; // enable/disable html5 mode. this.html5Mode = undefined; // globally prefixes all base // paths. used when entire directory // is within a sub dir from the // web servers's root path. // NOTE: unlike mounting a router // this only mounts the template // and component paths NOT your // routes. this.mount = ''; // globally prepends route paths. this.routeBase = undefined; // globally prepends view and // component paths. this.viewBase = ''; // globally prepends view paths. this.templateBase = ''; // global path for components. this.componentBase = ''; // lower route paths. // undefined or true paths // are lowered. this.lowerPaths = undefined; // the expression name used for // "controller" property in routes. // when componetizing routes for // ngRoute/uiRoute ONLY the controller is // added to the options as // follows: SomeController as ctrl. // where ctrl is the property defined // below. this.controllerAs = 'ctrl'; // Basap needs to know what controller // suffix to use when wiring up // component controllers. this.controllerSuffix = undefined; // This method if defined enables // you to define how controller // names are generated when a component // is being componentized. when called // two params are injected, the first // being the component name, the second // being the complete route config. // You MUST return a string to be // used for the controller name. // // By default // controller names are generated based // on the "component" property name. // these names are always capitalized. // if a component is specified as // component: 'some/component' and // the controller suffix is 'ctrl' it // will become and look to use a controller // named 'SomeComponentCtrl'. this.onControllerName = undefined; // when creating a component // this callback can be defined // so that you can alter the // componetized options for // defining the component. // the callback injects // the config object, and the // area configuration. expects // and object to be returned // containing the config object. // basically this method does // the heavy liften then lets // you tweak controller names // paths and urls. this.onComponetize = undefined; // when not false instance // add $app to window. this.globalize = true; // if you wish to change the name of the // global you can do so here. // NOTE: this will not change the name // of the Angualar factory that is // exposed this will still be "$basap" this.globalizeAs = '$basap'; // global based controller. this.BaseCtrl = BaseCtrl; // extends BaseCtrl with // custom methods/properties. this.baseExtend = undefined; // the element the base controller // should be bound to, must be // unique html tag or id. this.baseElement = 'html'; // the element to bind the RouteCtrl to // only used with ngNewRouter, must be // unique html tag or id. this.routerElement = 'body'; // Default logger configuration. // Http requests are made via a // simple API. The object accepts // the following properties. //var logger = { // globalize undefined, // when NOT false logger is added to window as $log. // console: undefined, // when NOT false messages are logged to the console. // remote: false, // when true logs are posted to server, otherwise only console. // level: 'error', // current log level supports 'error', 'warn', 'info', 'debug'. // method: 'POST', // method to use to post to server. // path: '/api/log/client' // the endpoint on the server. // headers: {} // ex: contentType (IMPORTANT keys must be in camelcase). // async: true // default for XMLHttpRequests is true for asynchronous. // username: undefined // username to be use with credentials. // password: undefined // same as above. //}; // whether or not to enabled // logger can either be set to // true, false a config object // or a function that provides // context to basap. // if true the default config // is used. // essentially this is merely a // wrapper to the browser's console // its usefulness is in sending // the log payload to the server // in a convenient way. this.logger = undefined; // the key name added to $rootScope. this.areaKey = '$area'; // The property name used when // attaching security lists to area routes. this.aclKey = 'acl'; // extend options. angular.extend(this, options); // ensure we have a name for display. this.name = this.name || this.ns; // check if template/componentBase // have values if not inherit from // viewBase. if(!this.templateBase) this.templateBase = this.viewBase; if(!this.componentBase) this.componentBase = this.viewBase; // check if logger is enabled. if(this.logger !== false) this.logger = this.logger || true; // create logger instance. this.log = new Logger(this.logger, this.module); // globalize logger if enabled. if(this.log.options.globalize !== false) window.$log = this.log; // if globalized add to window // prefixed by $ (default: $app) if(this.globalize) window[this.globalizeAs] = this; //`${this.ns}`; // add base controller attr if defined. if(this.BaseCtrl){ let elem = this.query(this.baseElement); if(elem) elem.setAttribute('ng-controller', 'BaseCtrl as ' + this.ns); } // lookup the router this.routerName = lookupRouter(this.dependencies); // show ngNewRouter warning. if(this.routerName === 'ngNewRouter'){ if(console && console.warn){ console.warn(`${this.routerName} is supported only as preview, should not be used in` + `production. until more stable.`); } } // if ngNewRouter add controller // expression to routerElement. if(this.routerName === 'ngNewRouter' && this.routerElement){ let elem = this.query(this.routerElement); if(elem) elem.setAttribute('ng-controller', 'RouteCtrl as router'); } // get router config. this.routerConfig = angular.extend(configs[this.routerName], this.routerConfig); // ensure controllerSuffix this.controllerSuffix = this.controllerSuffix || 'Ctrl'; // private properties. Object.defineProperties(this, { _menu: { value: undefined, writable: true } }); return this; } /** * Enables setting options after initializing basap. * This method however has no concept of timing. * For example you may wish to set a base path. * You must do this prior to calling app.when() * or the setting will have no effect. This likewise * applies to components and so on. * @param {String} key - the key anme to be updated. * @param {*} [value] - the value to be set. * @returns {Base} */ set(key, value){ if(angular.isObject(key)) angular.extend(this, key); else this[key] = value !== undefined ? value : this[key]; return this; } /** * Simple check if value is * a boolean object. * @param val * @returns {boolean} */ isBoolean(val) { return val === true || val === false; } /** * Try catch get provider. * make errors clear. * @param injector - instance of $injector. * @param provider - string value representing * @returns {function|boolean} */ tryInject(injector, provider){ try { return injector.get(provider); } catch(ex) { ex.message = `Failed to inject ${provider}: ${ex.message}`; throw ex; } } /** * Checks if value is containted in an Array. * @param arr - the array to parse. * @param values - an or Array or single value. * @param bool - if true (default) returns boolean otherwise value. * @returns {boolean|*} */ contains(arr, values, bool) { var self = this; bool = bool === undefined ? true : bool; if(!(values instanceof Array)){ var idx = arr.indexOf(values); if(bool) return idx !== -1; return arr[idx]; } else { let result; values.forEach((v) => { if(!result){ result = self.contains(arr, v, bool); } }); return result; } } /** * Helper using document.querySelector * @param elem - element to find. * @param all - weather to use selectorAll * @returns {*} */ query(elem, all) { var selector = all ? 'querySelectorAll' : 'querySelector'; return document[selector](elem); } /** * Async helper for looping arrays * this function is NOT a promise. * @param arr - the array to loop. * @param fn - the fn to call for each iteration. * @param pre - a fn to preprocess params injected into fn. * @param done - callback upon completion of iteration. */ async(arr, fn, pre, done){ var i = 0, delay = 0; done = done || function () {}; if(typeof pre !== 'function'){ delay = pre; pre = undefined; } return setTimeout(function iter(){ let params; if(i===arr.length) return done(); // injects item, index. params = [arr[i], i++]; // if pre call to get params. if(typeof pre === 'function') params = pre.apply(arr, params); // if not array convert apply to fn. if(!(params instanceof Array)) params = [params]; fn.apply(arr, params); setTimeout(iter, delay); }, 0); } /** * Returns collection of routes * by area name or all. * if mapped returns url/path * with configuration object. * @param [area] - the area you wish to retrieve routes for. * @returns {Array} */ routes(area) { var self = this, routes = []; Object.keys(this.areas).forEach((a) => { let _routes = self.areas[a]._routes; if(!area || a === area) routes = routes.concat(_routes); }); // flatten routes. if(this.routerName === 'ngNewRouter'){ routes = [].concat.apply([], routes); } else { // convert to array of objects. angular.forEach(routes, function (r,i) { routes[i] = r[1]; }); } return routes; } /** * Returns list of providers for app. * @param injector * @returns {{}} */ providers(injector) { if(!injector) return; var providers = { location: this.tryInject(injector, '$locationProvider'), controller: this.tryInject(injector, '$controllerProvider').register, factory: this.tryInject(injector, '$provide').factory, service: this.tryInject(injector, '$provide').service, directive: this.tryInject(injector, '$compileProvider').directive, filter: this.tryInject(injector, '$filterProvider').register, value: this.tryInject(injector, '$provide').value, constant: this.tryInject(injector, '$provide').constant, decorator: this.tryInject(injector, '$provide').decorator }; if(this.routerName === 'ngRoute' || this.routerName === 'uiRouter') { providers.route = this.tryInject(injector, this.routerConfig.provider); providers.otherwise = this.tryInject(injector, this.routerConfig.otherwiseProvider); } else { providers.loader = this.tryInject(injector, '$componentLoaderProvider'); } return providers; } /** * Register's an area with app. * @param name - the name of the Area. * @param deps - area dependencies. * @param options - object of options. * @returns {Base} */ area(name, deps, options) { var self = this, area; // if only area name provided get area. if(angular.isString(name) && arguments.length === 1){ if(!this.areas[name]) throw new Error(`The area ${name} could not be found.`); return this.areas[name]; } // allow namespace and // options only as params. if(arguments.length === 2 && angular.isObject(deps) && !angular.isArray(deps)){ options = deps; deps = undefined; } var regex = new RegExp('^(app|' + this.ns + ')', 'i'); if(angular.isString(name)) name = name.replace(regex,''); options = options || {}; // check for duplicates. if(this.areas[name]) throw new Error(`Failed to register area ${name} duplicate detected.`); // create area instance. area = new Area(name, options); // get area namespace. area.ns = area.ns || (`${this.ns}.${name}`); // add the area as a dependency. // only if not deactivated. // area is created but will not // be loaded. if(!area.inactive) this.dependencies.push(area.ns); // create the module. area.module = angular.module(area.ns, area.dependencies); // expose app instance to area. area.basap = this; // set base paths. area.reBase(); // get area routes or all routes. area.getRoutes = function getRoutes (all) { let area = all ? area.name : undefined; return self.routes(area); }; // add the area to the collection. this.areas[area.name] = area; // return the area. return area; } /** * Gets & filters routes for menu. * If no filter is provided returns all * routes which contain a truthy "menu" property. * when a string is passed menu must match the string, * if an array is passed will match any value in the * array. * * The "menu" property may also be comma separated string, * when true the string is converted to an array amd then * matched against this enables a single route to have multiple * menus. * * ex: app.menu('public') where app is the namespace of your app. * the above would return all routes where "menu" equals 'public'. * * @param [filter] - filters routes with "menu" property accepts undefined, string or function. * @returns {array} */ menu(filter) { var _filter; // filter function. function filterRoutes(route) { if(!route) return; // if menu convert to object if string. if(route.menu && angular.isString(route.menu)) route.menu = { name: route.menu }; route.menu = route.menu || {}; // ensure valid route name. // title - is used as the header title in page. // label - the name displayed as the link text in your menu. route.menu.label = route.menu.label || route.title || route.name; // check for ui-router state name if(/\./g.test(route.menu.label)){ // split and pop last key in state name. let tmp = route.menu.label.split('.'); route.menu.label = tmp.pop(); } // if filter is undefined // return all where menu is truthy. if(filter === undefined) return route.menu; // ensure route.menu is defined. if(Object.keys(route.menu).length){ // if string split to array // after trimming whitespace. if(angular.isString(route.menu.name)){ let found = false, menu; menu = route.menu.name.replace(/\s/g, ''); menu = menu.split(','); menu.forEach(m => { if(!found) found = (m === filter); }); return found; } return filter === route.menu.name; } return false; } _filter = filterRoutes; if(angular.isFunction(filter)) _filter = filter; // filter routes where "menu" property is present. this._menu = this.routes().filter(_filter); return this._menu; } /** * Adds custom configure function to module. * this is merely a convenience wrapper * @param [fn] - the function to exec or array containing dependencies. * @returns {Area} */ config(fn) { if(fn) this.module.config.apply(this, arguments); return this; } /** * Adds custom run function to module. * this is merely a convenience wrapper. * @param [fn] - the function to exec or array including dependencies. * @returns {Base} */ run(fn) { if(fn) this.module.run.apply(this, arguments); return this; } /** * Boostraps Angular app. * @param element - the element to bootstrap app. */ bootstrap(element) { var self = this, _module = this.module; // check override of bound element. element = element || this.element; // main module config. function config($injector) { var providers = self.providers($injector); var mode = self.html5Mode !== undefined ? self.html5Mode : true; // set html5Mode. providers.location.html5Mode(mode); } config.$inject = ['$injector']; // add main router controller. // you can add nested routers // the below is simply the base router. if(self.routerName === 'ngNewRouter'){ _module.controller('RouteCtrl', RouteCtrl); } // expose basap as factory function BasapFact () { var _instance = self; // need to extend w/methods. _instance.routes = self.routes.bind(self); _instance.tryInject = self.tryInject.bind(self); _instance.contains = self.contains.bind(self); _instance.query = self.query.bind(self); _instance.providers = self.providers.bind(self); _instance.async = self.async.bind(self); _instance.menu = self.menu.bind(self); _instance.log = self.log; return _instance; } BasapFact.$inject = []; _module.factory('$basap', BasapFact); // expose base controller. if(this.BaseCtrl) _module.controller('BaseCtrl', this.BaseCtrl); var promise = new Promise(function(resolve) { var areaKeys = Object.keys(self.areas); function fn(item) { var area = self.areas[item]; if(area) area.init(); } self.async(areaKeys, fn, null, resolve); }); promise.then(function() { // exec config block. _module.config(config); // bootstrap to element. angular.element(document).ready(function () { angular.bootstrap(element, [self.ns]); }); }); } } // Singleton instance of Base. Base.instance = undefined; /** * Gets singleton instance of Base. * @param [ns] - the app namespace * @param [deps] - the main module dependencies. * @param [options] - base default options. * @private * @returns {Base} */ function get(ns, deps, options) { if(!Base.instance){ Base.instance = new Base(ns, deps, options); Base.constructor = null; } return Base.instance; } export default get;