UNPKG

ui-router

Version:

State-based routing for Javascript

151 lines (129 loc) 6.52 kB
/** @module ng1 */ /** */ import {State} from "../state/stateObject"; import {pick, forEach, anyTrueR, unnestR} from "../common/common"; import {kebobString} from "../common/strings"; import {ViewConfig} from "../view/interface"; import {Ng1ViewDeclaration} from "./interface"; import {ViewService} from "../view/view"; import {isInjectable, isDefined, isString, isObject} from "../common/predicates"; import {services} from "../common/coreservices"; import {trace} from "../common/trace"; import {Node} from "../path/node"; import {TemplateFactory} from "./templateFactory"; import {ResolveContext} from "../resolve/resolveContext"; export const ng1ViewConfigFactory = (node, view) => new Ng1ViewConfig(node, view); /** * This is a [[StateBuilder.builder]] function for angular1 `views`. * * When the [[StateBuilder]] builds a [[State]] object from a raw [[StateDeclaration]], this builder * handles the `views` property with logic specific to angular-ui-router (ng1). * * If no `views: {}` property exists on the [[StateDeclaration]], then it creates the `views` object * and applies the state-level configuration to a view named `$default`. */ export function ng1ViewsBuilder(state: State) { let tplKeys = ['templateProvider', 'templateUrl', 'template', 'notify', 'async'], ctrlKeys = ['controller', 'controllerProvider', 'controllerAs', 'resolveAs'], compKeys = ['component', 'bindings'], nonCompKeys = tplKeys.concat(ctrlKeys), allKeys = compKeys.concat(nonCompKeys); let views = {}, viewsObject = state.views || {"$default": pick(state, allKeys)}; forEach(viewsObject, function (config: Ng1ViewDeclaration, name) { // Account for views: { "": { template... } } name = name || "$default"; // Account for views: { header: "headerComponent" } if (isString(config)) config = { component: <string> config }; if (!Object.keys(config).length) return; // Configure this view for routing to an angular 1.5+ style .component (or any directive, really) if (config.component) { if (nonCompKeys.map(key => isDefined(config[key])).reduce(anyTrueR, false)) { throw new Error(`Cannot combine: ${compKeys.join("|")} with: ${nonCompKeys.join("|")} in stateview: 'name@${state.name}'`); } // Dynamically build a template like "<component-name input1='$resolve.foo'></component-name>" config.templateProvider = ['$injector', function($injector) { const resolveFor = key => config.bindings && config.bindings[key] || key; const prefix = angular.version.minor >= 3 ? "::" : ""; let attrs = getComponentInputs($injector, config.component) .map(key => `${kebobString(key)}='${prefix}$resolve.${resolveFor(key)}'`).join(" "); let kebobName = kebobString(config.component); return `<${kebobName} ${attrs}></${kebobName}>`; }]; } config.resolveAs = config.resolveAs || '$resolve'; config.$type = "ng1"; config.$context = state; config.$name = name; let normalized = ViewService.normalizeUiViewTarget(config.$context, config.$name); config.$uiViewName = normalized.uiViewName; config.$uiViewContextAnchor = normalized.uiViewContextAnchor; views[name] = config; }); return views; } // for ng 1.2 style, process the scope: { input: "=foo" } object const scopeBindings = bindingsObj => Object.keys(bindingsObj || {}) .map(key => [key, /^[=<](.*)/.exec(bindingsObj[key])]) .filter(tuple => isDefined(tuple[1])) .map(tuple => tuple[1][1] || tuple[0]); // for ng 1.3+ bindToController or 1.5 component style, process a $$bindings object const bindToCtrlBindings = bindingsObj => Object.keys(bindingsObj || {}) .filter(key => !!/[=<]/.exec(bindingsObj[key].mode)) .map(key => bindingsObj[key].attrName); // Given a directive definition, find its object input attributes // Use different properties, depending on the type of directive (component, bindToController, normal) const getBindings = def => { if (isObject(def.bindToController)) return scopeBindings(def.bindToController); if (def.$$bindings && def.$$bindings.bindToController) return bindToCtrlBindings(def.$$bindings.bindToController); if (def.$$isolateBindings) return bindToCtrlBindings(def.$$isolateBindings); return <any> scopeBindings(def.scope); }; // Gets all the directive(s)' inputs ('=' and '<') function getComponentInputs($injector, name) { let cmpDefs = $injector.get(name + "Directive"); // could be multiple if (!cmpDefs || !cmpDefs.length) throw new Error(`Unable to find component named '${name}'`); return cmpDefs.map(getBindings).reduce(unnestR, []); } export class Ng1ViewConfig implements ViewConfig { loaded: boolean = false; controller: Function; template: string; locals: any; // TODO: delete me constructor(public node: Node, public viewDecl: Ng1ViewDeclaration) { } load() { let $q = services.$q; if (!this.hasTemplate()) throw new Error(`No template configuration specified for '${this.viewDecl.$uiViewName}@${this.viewDecl.$uiViewContextAnchor}'`); let injector = this.node.resolveContext; let params = this.node.paramValues; let promises: any = { template: $q.when(this.getTemplate(params, new TemplateFactory(), injector)), controller: $q.when(this.getController(injector)) }; return $q.all(promises).then((results) => { trace.traceViewServiceEvent("Loaded", this); this.controller = results.controller; this.template = results.template; }); } /** * Checks a view configuration to ensure that it specifies a template. * * @return {boolean} Returns `true` if the configuration contains a valid template, otherwise `false`. */ hasTemplate() { return !!(this.viewDecl.template || this.viewDecl.templateUrl || this.viewDecl.templateProvider); } getTemplate(params, $factory, injector: ResolveContext) { return $factory.fromConfig(this.viewDecl, params, injector.invokeLater.bind(injector)); } /** * Gets the controller for a view configuration. * * @returns {Function|Promise.<Function>} Returns a controller, or a promise that resolves to a controller. */ getController(injector: ResolveContext) { //* @param {Object} locals A context object from transition.context() to invoke a function in the correct context let provider = this.viewDecl.controllerProvider; return isInjectable(provider) ? injector.invokeLater(provider, {}) : this.viewDecl.controller; } }