Skip to content

Commit af95206

Browse files
feat(ui-view): Route-to-component: Wire component "&" bindings
When using route-to-component, this feature allows "&" component bindings to be wired to either 1) a function returned by a resolve or 2) a function in the parent component. Closes #3239 Closes #3111
1 parent 3a8fb11 commit af95206

File tree

5 files changed

+232
-55
lines changed

5 files changed

+232
-55
lines changed

karma.conf.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ webpackConfig.plugins = [];
3535
webpackConfig.devtool = 'inline-source-map';
3636

3737
module.exports = function(config) {
38-
var ngVersion = config.ngversion || "1.2.28";
38+
var ngVersion = config.ngversion || "1.6.0";
3939

4040
config.set({
4141
singleRun: true,

src/directives/viewDirective.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { ng as angular } from "../angular";
66
import {
77
IInterpolateService, IScope, ITranscludeFunction, IAugmentedJQuery,
8-
ICompileService, IControllerService, ITimeoutService
8+
ICompileService, IControllerService, ITimeoutService, noop
99
} from "angular";
1010

1111
import {
@@ -357,15 +357,15 @@ function $ViewDirectiveFill ($compile: ICompileService, $controller: IController
357357
return;
358358
}
359359

360-
let cfg: Ng1ViewConfig = data.$cfg || <any> { viewDecl: {} };
361-
$element.html(cfg.template || initial);
360+
let cfg: Ng1ViewConfig = data.$cfg || <any> { viewDecl: {}, getTemplate: noop };
361+
let resolveCtx: ResolveContext = cfg.path && new ResolveContext(cfg.path);
362+
$element.html(cfg.getTemplate($element, resolveCtx) || initial);
362363
trace.traceUIViewFill(data.$uiView, $element.html());
363364

364365
let link = $compile($element.contents());
365366
let controller = cfg.controller;
366367
let controllerAs: string = getControllerAs(cfg);
367368
let resolveAs: string = getResolveAs(cfg);
368-
let resolveCtx: ResolveContext = cfg.path && new ResolveContext(cfg.path);
369369
let locals = resolveCtx && getLocals(resolveCtx);
370370

371371
scope[resolveAs] = locals;

src/statebuilders/views.ts

+6-15
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ export class Ng1ViewConfig implements ViewConfig {
6767
loaded: boolean = false;
6868
controller: Function; // actually IInjectable|string
6969
template: string;
70+
component: string;
7071
locals: any; // TODO: delete me
72+
factory = new TemplateFactory();
7173

7274
constructor(public path: PathNode[], public viewDecl: Ng1ViewDeclaration) {
7375
}
@@ -78,31 +80,20 @@ export class Ng1ViewConfig implements ViewConfig {
7880
let params = this.path.reduce((acc, node) => extend(acc, node.paramValues), {});
7981

8082
let promises: any = {
81-
template: $q.when(this.getTemplate(params, new TemplateFactory(), context)),
83+
template: $q.when(this.factory.fromConfig(this.viewDecl, params, context)),
8284
controller: $q.when(this.getController(context))
8385
};
8486

8587
return $q.all(promises).then((results) => {
8688
trace.traceViewServiceEvent("Loaded", this);
8789
this.controller = results.controller;
88-
this.template = results.template;
90+
extend(this, results.template); // Either { template: "tpl" } or { component: "cmpName" }
8991
return this;
9092
});
9193
}
9294

93-
/**
94-
* Checks a view configuration to ensure that it specifies a template.
95-
*
96-
* @return {boolean} Returns `true` if the configuration contains a valid template, otherwise `false`.
97-
*/
98-
hasTemplate() {
99-
var templateKeys = ['template', 'templateUrl', 'templateProvider', 'component', 'componentProvider'];
100-
return hasAnyKey(templateKeys, this.viewDecl);
101-
}
102-
103-
getTemplate(params: RawParams, $factory: TemplateFactory, context: ResolveContext) {
104-
return $factory.fromConfig(this.viewDecl, params, context);
105-
}
95+
getTemplate = (uiView, context: ResolveContext) =>
96+
this.component ? this.factory.makeComponentTemplate(uiView, context, this.component, this.viewDecl.bindings) : this.template;
10697

10798
/**
10899
* Gets the controller for a view configuration.

src/templateFactory.ts

+63-34
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
/** @module view */ /** for typedoc */
1+
/** @module view */
2+
/** for typedoc */
23
import { ng as angular } from "./angular";
4+
import { IAugmentedJQuery } from "angular";
35
import {
4-
isArray, isDefined, isFunction, isObject, services, Obj, IInjectable, tail, kebobString, unnestR, ResolveContext, Resolvable, RawParams
6+
isArray, isDefined, isFunction, isObject, services, Obj, IInjectable, tail, kebobString, unnestR, ResolveContext,
7+
Resolvable, RawParams, identity
58
} from "ui-router-core";
69
import { Ng1ViewDeclaration } from "./interface";
710

@@ -25,13 +28,16 @@ export class TemplateFactory {
2528
fromConfig(config: Ng1ViewDeclaration, params: any, context: ResolveContext) {
2629
const defaultTemplate = "<ui-view></ui-view>";
2730

31+
const asTemplate = (result) => services.$q.when(result).then(str => ({ template: str }));
32+
const asComponent = (result) => services.$q.when(result).then(str => ({ component: str }));
33+
2834
return (
29-
isDefined(config.template) ? this.fromString(config.template, params) :
30-
isDefined(config.templateUrl) ? this.fromUrl(config.templateUrl, params) :
31-
isDefined(config.templateProvider) ? this.fromProvider(config.templateProvider, params, context) :
32-
isDefined(config.component) ? this.fromComponent(config.component, config.bindings) :
33-
isDefined(config.componentProvider) ? this.fromComponentProvider(config.componentProvider, params, context) :
34-
defaultTemplate
35+
isDefined(config.template) ? asTemplate(this.fromString(config.template, params)) :
36+
isDefined(config.templateUrl) ? asTemplate(this.fromUrl(config.templateUrl, params)) :
37+
isDefined(config.templateProvider) ? asTemplate(this.fromProvider(config.templateProvider, params, context)) :
38+
isDefined(config.component) ? asComponent(config.component) :
39+
isDefined(config.componentProvider) ? asComponent(this.fromComponentProvider(config.componentProvider, params, context)) :
40+
asTemplate(defaultTemplate)
3541
);
3642
};
3743

@@ -78,50 +84,73 @@ export class TemplateFactory {
7884
return resolvable.get(context);
7985
};
8086

87+
/**
88+
* Creates a component's template by invoking an injectable provider function.
89+
*
90+
* @param provider Function to invoke via `locals`
91+
* @param {Function} injectFn a function used to invoke the template provider
92+
* @return {string} The template html as a string: "<component-name input1='::$resolve.foo'></component-name>".
93+
*/
94+
fromComponentProvider(provider: IInjectable, params: any, context: ResolveContext) {
95+
let deps = services.$injector.annotate(provider);
96+
let providerFn = isArray(provider) ? tail(<any[]> provider) : provider;
97+
let resolvable = new Resolvable("", <Function> providerFn, deps);
98+
return resolvable.get(context);
99+
};
100+
81101
/**
82102
* Creates a template from a component's name
83103
*
104+
* @param uiView {object} The parent ui-view (for binding outputs to callbacks)
105+
* @param context The ResolveContext (for binding outputs to callbacks returned from resolves)
84106
* @param component {string} Component's name in camel case.
85107
* @param bindings An object defining the component's bindings: {foo: '<'}
86108
* @return {string} The template as a string: "<component-name input1='::$resolve.foo'></component-name>".
87109
*/
88-
fromComponent(component: string, bindings?: any) {
89-
const resolveFor = (key: string) =>
90-
bindings && bindings[key] || key;
110+
makeComponentTemplate(uiView: IAugmentedJQuery, context: ResolveContext, component: string, bindings?: any) {
111+
bindings = bindings || {};
112+
91113
// Bind once prefix
92114
const prefix = angular.version.minor >= 3 ? "::" : "";
115+
93116
const attributeTpl = (input: BindingTuple) => {
94-
var attrName = kebobString(input.name);
95-
var resolveName = resolveFor(input.name);
96-
if (input.type === '@')
117+
let {name, type } = input;
118+
let attrName = kebobString(name);
119+
// If the ui-view has an attribute which matches a binding on the routed component
120+
// then pass that attribute through to the routed component template.
121+
// Prefer ui-view wired mappings to resolve data, unless the resolve was explicitly bound using `bindings:`
122+
if (uiView.attr(attrName) && !bindings[name])
123+
return `${attrName}='${uiView.attr(attrName)}'`;
124+
125+
let resolveName = bindings[name] || name;
126+
// Pre-evaluate the expression for "@" bindings by enclosing in {{ }}
127+
// some-attr="{{ ::$resolve.someResolveName }}"
128+
if (type === '@')
97129
return `${attrName}='{{${prefix}$resolve.${resolveName}}}'`;
130+
131+
// Wire "&" callbacks to resolves that return a callback function
132+
// Get the result of the resolve (should be a function) and annotate it to get its arguments.
133+
// some-attr="$resolve.someResolveResultName(foo, bar)"
134+
if (type === '&') {
135+
let res = context.getResolvable(resolveName);
136+
let fn = res && res.data;
137+
let args = fn && services.$injector.annotate(fn) || [];
138+
let arrayIdxStr = isArray(fn) ? `[${fn.length - 1}]` : '';
139+
return `${attrName}='$resolve.${resolveName}${arrayIdxStr}(${args.join(",")})'`;
140+
}
141+
142+
// some-attr="::$resolve.someResolveName"
98143
return `${attrName}='${prefix}$resolve.${resolveName}'`;
99144
};
100145

101-
let attrs = getComponentInputs(component).map(attributeTpl).join(" ");
146+
let attrs = getComponentBindings(component).map(attributeTpl).join(" ");
102147
let kebobName = kebobString(component);
103148
return `<${kebobName} ${attrs}></${kebobName}>`;
104149
};
105-
106-
/**
107-
* Creates a component's template by invoking an injectable provider function.
108-
*
109-
* @param provider Function to invoke via `locals`
110-
* @param {Function} injectFn a function used to invoke the template provider
111-
* @return {string} The template html as a string: "<component-name input1='::$resolve.foo'></component-name>".
112-
*/
113-
fromComponentProvider(provider: IInjectable, params: any, context: ResolveContext) {
114-
let deps = services.$injector.annotate(provider);
115-
let providerFn = isArray(provider) ? tail(<any[]> provider) : provider;
116-
let resolvable = new Resolvable("", <Function> providerFn, deps);
117-
return resolvable.get(context).then((componentName) => {
118-
return this.fromComponent(componentName);
119-
});
120-
};
121150
}
122151

123-
// Gets all the directive(s)' inputs ('@', '=', and '<')
124-
function getComponentInputs(name: string) {
152+
// Gets all the directive(s)' inputs ('@', '=', and '<') and outputs ('&')
153+
function getComponentBindings(name: string) {
125154
let cmpDefs = <any[]> services.$injector.get(name + "Directive"); // could be multiple
126155
if (!cmpDefs || !cmpDefs.length) throw new Error(`Unable to find component named '${name}'`);
127156
return cmpDefs.map(getBindings).reduce(unnestR, []);
@@ -143,7 +172,7 @@ interface BindingTuple {
143172
// for ng 1.3 through ng 1.5, process the component's bindToController: { input: "=foo" } object
144173
const scopeBindings = (bindingsObj: Obj) => Object.keys(bindingsObj || {})
145174
// [ 'input', [ '=foo', '=', 'foo' ] ]
146-
.map(key => [key, /^([=<@])[?]?(.*)/.exec(bindingsObj[key])])
175+
.map(key => [key, /^([=<@&])[?]?(.*)/.exec(bindingsObj[key])])
147176
// skip malformed values
148177
.filter(tuple => isDefined(tuple) && isArray(tuple[1]))
149178
// { name: ('foo' || 'input'), type: '=' }

0 commit comments

Comments
 (0)