ui-router
Version:
State-based routing for Javascript
151 lines (129 loc) • 6.52 kB
text/typescript
/** @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;
}
}