-
Notifications
You must be signed in to change notification settings - Fork 3k
/
Copy pathviews.ts
167 lines (144 loc) · 6.83 KB
/
views.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
/** @module ng1 */ /** */
import { ng as angular } from "../../angular";
import {
State, Obj, pick, forEach, anyTrueR, unnestR, tail, extend, kebobString,
isArray, isInjectable, isDefined, isString, isObject, services, trace,
ViewConfig, ViewService, ViewConfigFactory, PathNode, ResolveContext, Resolvable, RawParams, IInjectable
} from "ui-router-core";
import { Ng1ViewDeclaration } from "../interface";
import { TemplateFactory } from "../templateFactory";
import IInjectorService = angular.auto.IInjectorService;
export const ng1ViewConfigFactory: ViewConfigFactory = (path, view) =>
[new Ng1ViewConfig(path, 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: { [key: string]: Ng1ViewDeclaration } = {},
viewsObject = state.views || { "$default": pick(state, allKeys) };
forEach(viewsObject, function (config: Ng1ViewDeclaration, name: string) {
// 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: IInjectorService) {
const resolveFor = (key: string) =>
config.bindings && config.bindings[key] || key;
const prefix = angular.version.minor >= 3 ? "::" : "";
const attributeTpl = (input: BindingTuple) => {
var attrName = kebobString(input.name);
var resolveName = resolveFor(input.name);
if (input.type === '@')
return `${attrName}='{{${prefix}$resolve.${resolveName}}}'`;
return `${attrName}='${prefix}$resolve.${resolveName}'`;
};
let attrs = getComponentInputs($injector, config.component).map(attributeTpl).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;
}
interface BindingTuple {
name: string;
type: string;
}
// for ng 1.2 style, process the scope: { input: "=foo" }
// for ng 1.3 through ng 1.5, process the component's bindToController: { input: "=foo" } object
const scopeBindings = (bindingsObj: Obj) => Object.keys(bindingsObj || {})
// [ 'input', [ '=foo', '=', 'foo' ] ]
.map(key => [key, /^([=<@])[?]?(.*)/.exec(bindingsObj[key])])
// skip malformed values
.filter(tuple => isDefined(tuple) && isArray(tuple[1]))
// { name: ('foo' || 'input'), type: '=' }
.map(tuple => ({ name: tuple[1][2] || tuple[0], type: tuple[1][1] } as BindingTuple));
// Given a directive definition, find its object input attributes
// Use different properties, depending on the type of directive (component, bindToController, normal)
const getBindings = (def: any) => {
if (isObject(def.bindToController)) return scopeBindings(def.bindToController);
return scopeBindings(def.scope);
};
// Gets all the directive(s)' inputs ('@', '=', and '<')
function getComponentInputs($injector: IInjectorService, name: string) {
let cmpDefs = <any[]> $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, []);
}
let id = 0;
export class Ng1ViewConfig implements ViewConfig {
$id = id++;
loaded: boolean = false;
controller: Function; // actually IInjectable|string
template: string;
locals: any; // TODO: delete me
constructor(public path: PathNode[], 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 context = new ResolveContext(this.path);
let params = this.path.reduce((acc, node) => extend(acc, node.paramValues), {});
let promises: any = {
template: $q.when(this.getTemplate(params, new TemplateFactory(), context)),
controller: $q.when(this.getController(context))
};
return $q.all(promises).then((results) => {
trace.traceViewServiceEvent("Loaded", this);
this.controller = results.controller;
this.template = results.template;
return this;
});
}
/**
* 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: RawParams, $factory: TemplateFactory, context: ResolveContext) {
return $factory.fromConfig(this.viewDecl, params, context);
}
/**
* Gets the controller for a view configuration.
*
* @returns {Function|Promise.<Function>} Returns a controller, or a promise that resolves to a controller.
*/
getController(context: ResolveContext): (IInjectable|string|Promise<IInjectable|string>) {
let provider = this.viewDecl.controllerProvider;
if (!isInjectable(provider)) return this.viewDecl.controller;
let deps = services.$injector.annotate(provider);
let providerFn = isArray(provider) ? tail(<any> provider) : provider;
let resolvable = new Resolvable("", <any> providerFn, deps);
return resolvable.get(context);
}
}