basap
Version:
Angular 1.x ES6 base app register module.
1,026 lines (868 loc) • 34.7 kB
JavaScript
import angular from 'angular';
/**
* Represents Area module containing its components.
* @class
*/
class Area {
/**
* Area constructor.
* @contructor
* @param name - the area name.
* @param [deps] - module dependencies.
* @param [options] - area options.
*/
constructor(name, deps, options) {
if(!angular.isArray(deps) && angular.isObject(deps)){
options = deps;
deps = undefined;
}
// allow passing deps in options.
if(options.dependencies){
deps = options.dependencies;
delete options.dependencies;
}
// store original options
// this simple provides a way
// to pass any option the user
// may wish and access by
// app.$area.current.options[ /* YOUR_OPTION */ ]
this.options = angular.copy(options || {});
/* Public properties
***********************************/
// the area name
this.name = name;
// enables baseCtrl titles
// to display a name other than
// the area name.
this.title = undefined;
// the namespace for the area.
this.ns = undefined;
// area module dependencies.
this.dependencies = deps || [];
// area base by default is set to
// the area name. areaBase
// prefixes routeBase, templateBase
// and componentBase if defined
// set to false to ignore.
this.areaBase = undefined;
// prefix routes with this string.
this.routeBase = undefined;
// prefix template and component paths.
this.viewBase = undefined;
// prefix template url with this string.
this.templateBase = undefined;
// base path for components.
this.componentBase = undefined;
// expression name used when
// defining route controllers with
// controller as syntax.
// this is only used for ngRoute
// & uiRouter where the route
// options contain a key called
// "component". results in an
// auto generated controller
// ex: { controller:
// 'SomeController as ctrl' }
this.controllerAs = undefined;
// 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;
// disable the area.
this.inactive = false;
// The areas security access levels.
// The property is attached to each route
// in the area. it is used for filtering
// and preventing access to routes based on
// access level.
this.acl = undefined;
// do not allow "name" to be
// passed within options.
delete options.name;
// extend w/ options.
if(options)
angular.extend(this, options);
// 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;
// ensure the display name.
this.title = this.title !== undefined ? this.title : this.name;
// check if areaBase is enabled.
if(this.areaBase !== false){
// if areaBase is string use
// it otherwise set to area name.
if(!angular.isString(this.areaBase))
this.areaBase = `/${this.name}`;
}
/* Components
***********************************/
// wrapper function for calling
// this.component by a type.
function componentWrapper(type) {
return function(...args) {
args.unshift(type);
this.component.apply(this, args);
};
}
// add helper methods.
this.controller = componentWrapper('controller');
this.factory = componentWrapper('factory');
this.service = componentWrapper('service');
this.directive = componentWrapper('directive');
this.filter = componentWrapper('filter');
this.constant = componentWrapper('constant');
this.value = componentWrapper('value');
this.decorator = componentWrapper('decorator');
/* Private properties
***********************************/
Object.defineProperties(this, {
// track area initialization
_initialized: {
value: false,
writable: true
},
// collection of route configs.
_routes: {
value: [],
writable: true
},
// ngNewRouter component, controller
// and template mapping for components.
_mappings: {
value: [],
writable: true
},
// collection of component configs.
_components: {
value: [],
writable: true
},
// contains list of controllers,
// for comparing to required.
_controllers: {
value: [],
writable: true
}
});
return this;
}
/**
* Iterate object setting base paths.
* @param base - the base path to prefix routes.
* @param key - the property to update or array of property strings.
* @param obj - the route configuration object
*/
setBase(base, key, obj){
var self = this;
// allows passing two strs
// the base path and the route path.
if(arguments.length === 2){
obj = key;
key = base;
base = undefined;
}
// checks if val is in array.
function contains(arr, val){
return arr.indexOf(val) !== -1;
}
// joins two strings.
// Todo: test make sure works in all scenarios.
function join(b,p) {
let result;
b = b || '';
// ensure . or / as first char.
if(b && !/^(\.|\/)/.test(p))
p = '/' + p;
// remove last char if backslash.
if(/\/$/.test(p) && p.length > 1)
p = p.slice(0, -1);
// ensure no double backslashes
result = b + p;
result = result.replace(/\\/g, '/');
return result;
}
// iterate object's properties and
// nested properties.
function iterateConfig(o) {
for(var prop in o){
if(o.hasOwnProperty(prop)){
if(angular.isObject(o[prop]) && !angular.isArray(0[prop])){
iterateConfig(o[prop]);
}
else {
if(contains(key, prop)){
// check if path starts with mount path
// if true and is view path check if
// should be static, saves some aggrevation.
if(self.mount && self.mount.length){
let mount = self.mount;
if(mount.charAt(0) === '/')
mount = mount.replace(/^\//, '');
let regexp = new RegExp(`(/${mount}|${mount})`);
if(prop === 'templateUrl' && regexp.test(o[prop])){
o.staticView = true;
}
}
// don't join static routes or views.
if(!o.staticView && !o.staticRoute)
o[prop] = join(base, o[prop]);
if(o[prop] !== '/')
o[prop] = o[prop].replace(/\/$/, '');
}
}
}
}
return o;
}
// if object is string (uiRouter)
// then join with base.
if(angular.isString(obj)){
return join(base, obj);
}
// if typeof object iterate
// and return the result.
else {
if(!angular.isArray(key))
key = [key];
return iterateConfig(obj);
}
}
/**
* Enables mapping of template &
* name within components.
* NOTE: valid only for ngNewRouter
* @see https://angular.github.io/router/$componentLoaderProvider
* @param type - the type of component loader to run.
* @param mapping - the mapping function to be called.
*/
setMapping(type, mapping) {
if(this.routerName !== 'ngNewRouter')
throw new Error(`Method setMapping is not implemented for ${this.routerName}`);
var map = {
controller: 'setCtrlNameMapping',
template: 'setTemplateMapping',
component: 'setComponentFromCtrlMapping'
};
// if not in map try reverse
// lookup by values.
if(!map[type]){
let values = Object.keys(map).map((k) => {
return map[k];
});
type = this.basap.contains(values, type, false);
if(!type)
throw new Error(`Failed to set mapping of type ${type}, the type is invalid.`);
} else {
type = map[type];
}
this._mappings.push([type, mapping]);
}
/**
* Updates base paths after area has been created.
* This is useful when you want to set options in
* basap and you want changes to be reflected in
* @returns {Area}
*/
reBase() {
var baseKeys = [
'routerName', 'routerConfig',
'access', 'inherit', 'componentBase', 'onComponetize',
'routeBase', 'templateBase', 'controllerSuffix',
'controllerAs', 'areaKey', 'aclKey', 'onControllerName', 'mount'
],
area = this,
basap = this.basap;
baseKeys.forEach((k) => {
// if not a base type
// and area's key has no
// value, simply update
// from app options property.
if(!basap.contains(['routeBase', 'templateBase', 'componentBase'], k)){
if(area[k] === undefined)
area[k] = basap[k];
}
// if key is a base type
// check for prepends and
// overrides.
else {
// prepend base paths to area paths.
let tmpBase = area[k] !== undefined ? area[k] : '';
// if tmpBase is false
// set to empty string and return.
if(tmpBase === false){
area[k] = '';
return;
}
// routes usually need to be
// as defined if basap and
// area routeBase both undefined
// set to empty string and return.
if(k === 'routeBase' && basap[k] === undefined && area[k] === undefined){
area[k] = '';
return;
}
// set area base.
if(area.areaBase)
tmpBase = `${area.areaBase}/${tmpBase}`;
// ensure first char is backslash.
if(tmpBase.charAt(0) !== '/')
tmpBase = `/${tmpBase}`;
// if no tmpBase but base options
// contain value of same base
// type allow it to populate
// the base path.
if(!tmpBase || !tmpBase.length && (basap[k] && basap[k].length))
tmpBase = basap[k];
// check for mount point.
// routeBase should NOT be
// prepended with mount point.
if(k !== 'routeBase')
tmpBase = `${basap.mount || ''}/${tmpBase}`;
// ensure no double backslashes.
tmpBase = tmpBase.replace(/\/\//g, '/');
// remove trailing slash.
tmpBase = tmpBase.replace(/\/$/, '');
// finally update the base type
// with the tmpBase value.
area[k] = tmpBase || '';
}
});
return this;
}
/**
* Normalizes controller names to prevent
* casing issues or invalid suffix when
* using component feature.
* @param name
* @returns {string}
*/
normalizeCtrlName(name){
var key = name,
suffix = this.controllerSuffix;
// if string starts with "/" remove.
if(/^\//.test(key))
key = key.slice(1);
// check for user normalize func.
if(angular.isFunction(this.onControllerName)){
key = this.onControllerName(key);
}
else {
// make sure suffix is cap.
suffix = suffix.charAt(0).toUpperCase() + suffix.slice(1);
// split key
key = key.split('/');
key = key.map(function(k) {
return k.charAt(0).toUpperCase() + k.slice(1);
});
key = key.join('');
if(!suffix)
return key;
// attempt to normalize controller
// name to prevent mis-namiing &
// casing issues. when used with
// components.
var normExp =
new RegExp('(Controller|Ctrl|Con|Ctrls|' + this.controllerSuffix + ')$', 'gi');
key = key.replace(normExp, '');
// combine key & suffix.
key = `${key}${suffix}`;
}
return key;
}
/**
* Registers Angular component by type.
* Register multiple by passing object of
* components as second param.
* @param type - controller, directive, factory,
* service, filter, constant, value or decorator.
* @param name - the name of the component.
* @param component - the component itself.
* @returns {Area}
*/
component(type, name, component){
var self = this;
if(!type) return this;
function addComponent(t, c) {
// iterate components add to collection.
Object.keys(c).forEach((k) => {
let key = k;
// componentize key if type controller
//if(t === 'controller' && self.componentBase){
if(t === 'controller'){
key = self.normalizeCtrlName(key);
self._controllers.push(k);
}
self._components.push([t, key, c[k]]);
});
}
// ensure module is loaded
if(!this.module && !this.module.config)
throw new Error(`Failed to register component ${name}, the module is not loaded.`);
// if only one argument assume
// object containing collection
// of component types.
if(arguments.length === 1) {
if(!angular.isObject(type) && !angular.isArray(type))
throw new Error(`Failed to load component collection of type ${typeof type}.`);
Object.keys(type).forEach(function (k){
addComponent(k, type[k]);
});
} else {
// lower and strip plural.
type = type.toLowerCase();
if(type === 'factories')
type = 'factory';
type = type.replace(/s$/, '');
// allow component object as
// second argument.
if(angular.isObject(name) && !angular.isArray(name)){
component = name;
name = undefined;
}
// normalize single component to object.
if(angular.isString(name)){
var orig = component;
component = {};
component[name] = orig;
}
addComponent(type, component);
}
return this;
}
/**
* Simply calls component.
* @returns {Area}
*/
components(){
return this.component.apply(this, arguments);
}
/**
* Add route to routes collection. Accepts path/state &
* options object or object containing keys representing
* states/paths whose values contain route configuration
* objects. You may also pass an array of objects in this
* case the Array containing configuration objects must
* have a property named "path" for ngRoute/ngRouterNew and a
* property named "state" for ui.router.
*
* NOTE: when using routeBase which is set to true by
* default you should set your main or root route
* to "root:true" in the route config. This tells Basap
* NOT to prefix this route with a base. To disable
* set routeBase to false. You may also set root:true
* on the area config to apply to all routes in the area.
*
* [ngRoute]
* ex: Single Route
* area.when('/route/path', { // options });
*
* ex: Object of Routes.
* area.when({
* '/home': { templateUrl: '/home.html' },
*
* NOTE: since "contacts" here is not a path Basap will
* look to the "path" key in the options as show below.
* contacts: { path: '/contact', templateUrl: '/contact.html' },
*
* '/about: { templateUrl: '/about.html' }
* });
*
* ex: Array of Objects
* area.when([
* { path: '/home', templateUrl: '/path/to/template' },
* { path: '/about', templateUrl: '/path/to/template' }
* ]);
*
* [ui.router]
* ex: Single Route
* area.when('state_name', { // options });
*
* ex: Object of Routes.
* area.when({
* home: { url: '/home', templateUrl: '/home.html' },
*
* NOTE: since "state" is specified the property key
* of "users" is overriden in this cause using the singular
* "user" instead of the default "users".
* users: { state: 'user', url: '/user', templateUrl: '/user.html' },
*
* about: { url: '/about', templateUrl: '/about.html' }
* });
*
* ex: Array of Objects
* area.when([
* { state: 'home', url: '/home', templateUrl: '/path/to/template' },
* { state: 'about', url: '/about', templateUrl: '/path/to/template' }
* ]);
*
* [ngRouterNew]
* ex: Single Route
* area.when('/route/path', { // options });
*
* ex: Object of Routes.
* area.when({
* home: { path: '/home', component: 'public' },
* about: { path: '/about', component: 'public' }
* ]);
*
* ex: Array of Objects
* area.when([
* { path: '/home', component: 'public' },
* { path: '/about', component: 'public' }
* ]);
*
* @param path - the path or state for the route.
* @param options - the route configuration object.
* @returns {Area}
*/
when(path, options) {
var self = this,
routerName = this.routerName,
key;
// get the path or overridden path.
function getPath(key, route) {
if(routerName === 'ngRoute' || routerName === 'ngNewRouter'){
route.path = route.path || key;
key = route.path || key;
key = self.setBase(self.routeBase, key);
}
if(routerName === 'uiRouter' && route.state !== undefined){
route.url = route.url || route.path;
route.path = route.url;
key = route.state || key;
delete route.state;
}
return key;
}
// when router is ngRoute or uiRouter
// if options contains "component"
// componentize the configuration
// options for the route.
function generateComponent(base, opts) {
let templateUrl;
templateUrl = opts.component;
// set the genrated templateUrl
// and the generated controller.
if(templateUrl) {
// if string starts with "/" remove.
if(/^\//.test(templateUrl))
templateUrl = templateUrl.slice(1);
let name = templateUrl.split('/').pop();
opts.templateUrl = `${templateUrl}/${name}.html`;
// if controller exists
// assume user defined.
if(!opts.controller){
opts.controllerName = self.normalizeCtrlName(templateUrl);
opts.controller = opts.controllerName;
if(self.controllerAs !== false && opts.controllerAs !== false)
opts.controller = `${opts.controller} as ${self.controllerAs}`;
}
if(self.basap.lowerPaths !== false)
opts.templateUrl = opts.templateUrl.toLowerCase();
// check for user componetized callback.
if(self.onComponetize){
// create clone in case undefined is returned.
let clone = angular.copy(opts);
opts = self.onComponetize.call(opts, self);
if(!opts)
opts = clone;
}
opts = self.setBase(base, ['templateUrl'], opts);
}
return opts;
}
// iterate the views or children object
// generating the templateUrls and controllers
function iterateUiComponents(base, obj) {
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
if(prop === 'children'){
obj[prop].forEach((c, i) => {
obj[prop][i] = iterateUiComponents(base, c);
});
}
if(angular.isObject(obj[prop])){
iterateUiComponents(base, obj[prop]);
}
}
}
if(obj.component) {
obj = generateComponent(base, obj);
}
if(obj.templateUrl && !obj.component){
obj = self.setBase(self.templateBase, ['templateUrl'], obj);
}
return obj;
}
// normalize paths where base
// path or template has been
// provided as prefix.
function normalizeOptions(opts){
opts = self.setBase(self.routeBase, ['url'], opts);
if(routerName !== 'ngNewRouter') {
// componentize uiRouter and ngRoute.
if(!self.basap.contains(Object.keys(opts), ['views', 'children', 'component'])){
if(!opts.staticView)
opts = self.setBase(self.templateBase, ['templateUrl'], opts);
}
else {
if(!angular.isString(self.componentBase))
throw new Error(`To use components with ${routerName}`+
` componentBase must be string/empty string.`);
if(routerName === 'ngRoute'){
opts = generateComponent(self.componentBase, opts);
} else {
opts = iterateUiComponents(self.componentBase, opts);
}
}
}
return opts;
}
// normalizes array which is used
// to apply when calling provider or $router.config.
function normalizeRouteArray(key, options){
if(routerName === 'ngNewRouter'){
options.path = key;
return [options];
} else {
return [key, options];
}
}
// iterate an object of
// route configurations.
if(angular.isObject(path) && !angular.isArray(path)){
Object.keys(path).forEach((r) => {
let route = path[r];
key = getPath(r, route);
if(self.basap.lowerPaths !== false)
key = key.toLowerCase();
if(r !== 'otherwise' && !angular.isFunction(route)){
route[self.areaKey] = self.name;
route[self.aclKey] = route[self.aclKey] || self.acl;
self._routes.push(normalizeRouteArray(key, normalizeOptions(route)));
} else {
self._routes.push(['otherwise', route]);
}
});
}
// iterate array of route configurations.
else if(angular.isArray(path)){
path.forEach((route) => {
key = getPath(null, route);
if(key){
route[self.areaKey] = self.name;
route[self.aclKey] = route[self.aclKey] || self.acl;
if(self.basap.lowerPaths !== false)
key = key.toLowerCase();
self._routes.push(normalizeRouteArray(key, normalizeOptions(route)));
}
});
}
// process single route w/ options.
else {
let isValid = angular.isString(options) || angular.isObject(options);
if(arguments.length !== 2 || !isValid){
throw new Error(`Route ${path} could not be registered, the configuration invalid.`);
}
else {
// if options is string
// assume redirect.
if(angular.isString(options)){
options = { redirectTo: options };
}
if(angular.isObject(options)){
key = getPath(path, options);
options[self.areaKey] = self.name;
options[self.aclKey] = self.acl;
options[self.aclKey] = options[self.aclKey] || self.acl;
if(self.basap.lowerPaths !== false)
key = key.toLowerCase();
self._routes.push(normalizeRouteArray(key, normalizeOptions(options)));
}
}
}
return this;
}
/**
* Add otherwise to routes collection.
* if path starts with "." or object
* contains "static:true" the path
* is considered static and is not
* relative to the area within it
* resides. The full path will be
* used.
* @param path - path, object or function.
* @returns {Area}
*/
otherwise(path) {
if(angular.isString(path) && !/^\./.test(path)){
path = this.setBase(this.routeBase, path);
path = path.replace(/^\./, '');
}
if(angular.isObject(path) && path.redirectTo && !/^\./.test(path.redirectTo) && !path.static){
path.redirectTo = this.setBase(this.routeBase, path.redirectTo);
path.redirectTo = path.redirectTo.replace(/^\./, '');
}
this._routes.push(['otherwise', path]);
return this;
}
/**
* 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 {Area}
*/
run(fn) {
if(fn)
this.module.run.apply(this, arguments);
return this;
}
/**
* Initialize the area.
* @returns {Area}
*/
init() {
// prevent duplicate initializations.
if(this._initialized)
throw new Error(`${this.ns} attempted to init but has already been initialized.`);
var self = this,
_module = this.module;
function DummyCtrl() {}
function normalizeComponentCtrls(obj, providers){
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
if(prop === 'children'){
obj[prop].forEach((c, i) => {
obj[prop][i] = normalizeComponentCtrls(c, providers);
});
}
if(angular.isObject(obj[prop])){
normalizeComponentCtrls(obj[prop], providers);
}
}
}
// Add dummy controlller when
// defined controller not found.
if(obj.component) {
if(!self.basap.contains(self._controllers, obj.controllerName))
providers.controller(obj.controllerName, DummyCtrl);
}
return obj;
}
// expose provider register methods.
function config($injector) {
// get all providers from app instance.
let providers = self.basap.providers($injector);
// set any mappings that are required.
if(self.routerName === 'ngNewRouter'){
self._mappings.forEach((m) => {
let type = m.shift();
providers.loader[type].apply(null, m);
});
}
// register components.
self._components.forEach((k) => {
let type = k[0];
if(angular.isString(type) && providers[type]){
k.shift();
providers[type].apply(null, k);
} else {
throw new Error(`Component type ${type} invalid configuration or not supported.`);
}
});
// if ngRoute or uiRouter use injector
// to get route/otherwise providers
// then inject routes.
if(self.routerName === 'ngRoute' || self.routerName === 'uiRouter'){
self._routes.forEach((r) => {
let key = r[0],
opts = r[1],
reqCtrl;
if(key === 'otherwise'){
// strip first element.
r.shift();
providers.otherwise[self.routerConfig.otherwiseMethod].apply(providers.otherwise, r);
} else {
// if component, check for
// valid controller if not exists
// inject noop dummy controller.
if(opts){
if(opts.component){
//self.normalizeCtrlName(opts.component);
if(!self.basap.contains(self._controllers, opts.controllerName))
providers.controller(opts.controllerName, DummyCtrl);
}
// interate for nested ui-router
// controller components.
else {
normalizeComponentCtrls(opts, providers);
}
}
if(self.routerName === 'uiRouter' && (opts && opts.redirectTo))
providers.otherwise.when.call(providers.otherwise, opts.url, opts.redirectTo);
else
providers.route[self.routerConfig.whenMethod].apply(providers.route, r);
}
});
}
}
config.$inject = ['$injector'];
// add configuration block.
_module.config(config);
// set initialized to true
// prevent duplicate inits.
this._initialized = true;
return this;
}
}
export default Area;