Skip to content

Commit 090d2e3

Browse files
sarahpadschristopherthielen
authored andcommitted
feat(view): Route a view to a directive using componentProvider (#3165)
State definitions can dynamically load a component's template by using componentProvider.
1 parent 3984f9b commit 090d2e3

File tree

5 files changed

+154
-54
lines changed

5 files changed

+154
-54
lines changed

src/ng1/interface.ts

+22
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,28 @@ export interface Ng1ViewDeclaration extends _ViewDeclaration {
369369
*/
370370
bindings?: { [key: string]: string };
371371

372+
/**
373+
* Dynamic component provider function.
374+
*
375+
* A property of [[Ng1StateDeclaration]] or [[Ng1ViewDeclaration]]:
376+
*
377+
* This is an injectable provider function which returns the component's wrapper template.
378+
* The provider will invoked during a Transition in which the view's state is
379+
* entered. The provider is called after the resolve data is fetched.
380+
*
381+
* #### Example:
382+
* ```js
383+
* componentProvider: function(MyResolveData, $transition$) {
384+
* if (MyResolveData.foo) {
385+
* return "fooComponent"
386+
* } else if ($transition$.to().name === 'bar') {
387+
* return "barComponent";
388+
* }
389+
* }
390+
* ```
391+
*/
392+
componentProvider?: IInjectable;
393+
372394
/**
373395
* The view's controller function or name
374396
*

src/ng1/statebuilders/views.ts

+4-51
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/** @module ng1 */ /** */
22
import { ng as angular } from "../../angular";
33
import {
4-
State, Obj, pick, forEach, anyTrueR, unnestR, tail, extend, kebobString,
5-
isArray, isInjectable, isDefined, isString, isObject, services, trace,
4+
State, pick, forEach, anyTrueR, tail, extend,
5+
isArray, isInjectable, isDefined, isString, services, trace,
66
ViewConfig, ViewService, ViewConfigFactory, PathNode, ResolveContext, Resolvable, RawParams, IInjectable
77
} from "ui-router-core";
88
import { Ng1ViewDeclaration } from "../interface";
@@ -24,7 +24,7 @@ export const ng1ViewConfigFactory: ViewConfigFactory = (path, view) =>
2424
export function ng1ViewsBuilder(state: State) {
2525
let tplKeys = ['templateProvider', 'templateUrl', 'template', 'notify', 'async'],
2626
ctrlKeys = ['controller', 'controllerProvider', 'controllerAs', 'resolveAs'],
27-
compKeys = ['component', 'bindings'],
27+
compKeys = ['component', 'bindings', 'componentProvider'],
2828
nonCompKeys = tplKeys.concat(ctrlKeys),
2929
allKeys = compKeys.concat(nonCompKeys);
3030

@@ -43,24 +43,6 @@ export function ng1ViewsBuilder(state: State) {
4343
if (nonCompKeys.map(key => isDefined(config[key])).reduce(anyTrueR, false)) {
4444
throw new Error(`Cannot combine: ${compKeys.join("|")} with: ${nonCompKeys.join("|")} in stateview: 'name@${state.name}'`);
4545
}
46-
47-
// Dynamically build a template like "<component-name input1='::$resolve.foo'></component-name>"
48-
config.templateProvider = ['$injector', function ($injector: IInjectorService) {
49-
const resolveFor = (key: string) =>
50-
config.bindings && config.bindings[key] || key;
51-
const prefix = angular.version.minor >= 3 ? "::" : "";
52-
const attributeTpl = (input: BindingTuple) => {
53-
var attrName = kebobString(input.name);
54-
var resolveName = resolveFor(input.name);
55-
if (input.type === '@')
56-
return `${attrName}='{{${prefix}$resolve.${resolveName}}}'`;
57-
return `${attrName}='${prefix}$resolve.${resolveName}'`;
58-
};
59-
60-
let attrs = getComponentInputs($injector, config.component).map(attributeTpl).join(" ");
61-
let kebobName = kebobString(config.component);
62-
return `<${kebobName} ${attrs}></${kebobName}>`;
63-
}];
6446
}
6547

6648
config.resolveAs = config.resolveAs || '$resolve';
@@ -77,35 +59,6 @@ export function ng1ViewsBuilder(state: State) {
7759
return views;
7860
}
7961

80-
interface BindingTuple {
81-
name: string;
82-
type: string;
83-
}
84-
85-
// for ng 1.2 style, process the scope: { input: "=foo" }
86-
// for ng 1.3 through ng 1.5, process the component's bindToController: { input: "=foo" } object
87-
const scopeBindings = (bindingsObj: Obj) => Object.keys(bindingsObj || {})
88-
// [ 'input', [ '=foo', '=', 'foo' ] ]
89-
.map(key => [key, /^([=<@])[?]?(.*)/.exec(bindingsObj[key])])
90-
// skip malformed values
91-
.filter(tuple => isDefined(tuple) && isArray(tuple[1]))
92-
// { name: ('foo' || 'input'), type: '=' }
93-
.map(tuple => ({ name: tuple[1][2] || tuple[0], type: tuple[1][1] } as BindingTuple));
94-
95-
// Given a directive definition, find its object input attributes
96-
// Use different properties, depending on the type of directive (component, bindToController, normal)
97-
const getBindings = (def: any) => {
98-
if (isObject(def.bindToController)) return scopeBindings(def.bindToController);
99-
return scopeBindings(def.scope);
100-
};
101-
102-
// Gets all the directive(s)' inputs ('@', '=', and '<')
103-
function getComponentInputs($injector: IInjectorService, name: string) {
104-
let cmpDefs = <any[]> $injector.get(name + "Directive"); // could be multiple
105-
if (!cmpDefs || !cmpDefs.length) throw new Error(`Unable to find component named '${name}'`);
106-
return cmpDefs.map(getBindings).reduce(unnestR, []);
107-
}
108-
10962
let id = 0;
11063
export class Ng1ViewConfig implements ViewConfig {
11164
$id = id++;
@@ -144,7 +97,7 @@ export class Ng1ViewConfig implements ViewConfig {
14497
* @return {boolean} Returns `true` if the configuration contains a valid template, otherwise `false`.
14598
*/
14699
hasTemplate() {
147-
return !!(this.viewDecl.template || this.viewDecl.templateUrl || this.viewDecl.templateProvider);
100+
return !!(this.viewDecl.template || this.viewDecl.templateUrl || this.viewDecl.templateProvider || this.viewDecl.component || this.viewDecl.componentProvider);
148101
}
149102

150103
getTemplate(params: RawParams, $factory: TemplateFactory, context: ResolveContext) {

src/ng1/templateFactory.ts

+75-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import { ng as angular } from "../angular";
12
/** @module view */ /** for typedoc */
23
import {
3-
isArray, isDefined, isFunction, services, IInjectable, tail, ResolveContext, Resolvable, RawParams
4+
isArray, isDefined, isFunction, isObject, services, Obj, IInjectable, tail, kebobString, unnestR, ResolveContext, Resolvable, RawParams
45
} from "ui-router-core";
56
import { Ng1ViewDeclaration } from "./interface";
67

@@ -26,6 +27,8 @@ export class TemplateFactory {
2627
isDefined(config.template) ? this.fromString(config.template, params) :
2728
isDefined(config.templateUrl) ? this.fromUrl(config.templateUrl, params) :
2829
isDefined(config.templateProvider) ? this.fromProvider(config.templateProvider, params, context) :
30+
isDefined(config.component) ? this.fromComponent(config.component, config.bindings) :
31+
isDefined(config.componentProvider) ? this.fromComponentProvider(config.componentProvider, params, context) :
2932
null
3033
);
3134
};
@@ -72,4 +75,74 @@ export class TemplateFactory {
7275
let resolvable = new Resolvable("", <Function> providerFn, deps);
7376
return resolvable.get(context);
7477
};
75-
}
78+
79+
/**
80+
* Creates a template from a component's name
81+
*
82+
* @param component {string} Component's name in camel case.
83+
* @param bindings An object defining the component's bindings: {foo: '<'}
84+
* @return {string} The template as a string: "<component-name input1='::$resolve.foo'></component-name>".
85+
*/
86+
fromComponent(component: string, bindings?: any) {
87+
const resolveFor = (key: string) =>
88+
bindings && bindings[key] || key;
89+
const prefix = angular.version.minor >= 3 ? "::" : "";
90+
const attributeTpl = (input: BindingTuple) => {
91+
var attrName = kebobString(input.name);
92+
var resolveName = resolveFor(input.name);
93+
if (input.type === '@')
94+
return `${attrName}='{{${prefix}$resolve.${resolveName}}}'`;
95+
return `${attrName}='${prefix}$resolve.${resolveName}'`;
96+
};
97+
98+
let attrs = getComponentInputs(component).map(attributeTpl).join(" ");
99+
let kebobName = kebobString(component);
100+
return `<${kebobName} ${attrs}></${kebobName}>`;
101+
};
102+
103+
/**
104+
* Creates a component's template by invoking an injectable provider function.
105+
*
106+
* @param provider Function to invoke via `locals`
107+
* @param {Function} injectFn a function used to invoke the template provider
108+
* @return {string} The template html as a string: "<component-name input1='::$resolve.foo'></component-name>".
109+
*/
110+
fromComponentProvider(provider: IInjectable, params: any, context: ResolveContext) {
111+
let deps = services.$injector.annotate(provider);
112+
let providerFn = isArray(provider) ? tail(<any[]> provider) : provider;
113+
let resolvable = new Resolvable("", <Function> providerFn, deps);
114+
return resolvable.get(context).then((componentName) => {
115+
return this.fromComponent(componentName);
116+
});
117+
};
118+
}
119+
120+
// Gets all the directive(s)' inputs ('@', '=', and '<')
121+
function getComponentInputs(name: string) {
122+
let cmpDefs = <any[]> services.$injector.get(name + "Directive"); // could be multiple
123+
if (!cmpDefs || !cmpDefs.length) throw new Error(`Unable to find component named '${name}'`);
124+
return cmpDefs.map(getBindings).reduce(unnestR, []);
125+
}
126+
127+
// Given a directive definition, find its object input attributes
128+
// Use different properties, depending on the type of directive (component, bindToController, normal)
129+
const getBindings = (def: any) => {
130+
if (isObject(def.bindToController)) return scopeBindings(def.bindToController);
131+
return scopeBindings(def.scope);
132+
};
133+
134+
interface BindingTuple {
135+
name: string;
136+
type: string;
137+
}
138+
139+
// for ng 1.2 style, process the scope: { input: "=foo" }
140+
// for ng 1.3 through ng 1.5, process the component's bindToController: { input: "=foo" } object
141+
const scopeBindings = (bindingsObj: Obj) => Object.keys(bindingsObj || {})
142+
// [ 'input', [ '=foo', '=', 'foo' ] ]
143+
.map(key => [key, /^([=<@])[?]?(.*)/.exec(bindingsObj[key])])
144+
// skip malformed values
145+
.filter(tuple => isDefined(tuple) && isArray(tuple[1]))
146+
// { name: ('foo' || 'input'), type: '=' }
147+
.map(tuple => ({ name: tuple[1][2] || tuple[0], type: tuple[1][1] } as BindingTuple));
148+

test/viewDirectiveSpec.js

+52
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,13 @@ describe('angular 1.5+ style .component()', function() {
806806
}
807807
});
808808

809+
app.directive('ng12DynamicDirective', function() {
810+
return {
811+
restrict: 'E',
812+
template: 'dynamic directive'
813+
}
814+
});
815+
809816
// ng 1.5+ component
810817
if (angular.version.minor >= 5) {
811818
app.component('ngComponent', {
@@ -837,6 +844,10 @@ describe('angular 1.5+ style .component()', function() {
837844
bindings: { evt: '&' },
838845
template: 'eventCmp',
839846
});
847+
848+
app.component('dynamicComponent', {
849+
template: 'dynamicComponent'
850+
})
840851
}
841852
}));
842853

@@ -1188,4 +1199,45 @@ describe('angular 1.5+ style .component()', function() {
11881199
});
11891200
}
11901201
});
1202+
1203+
describe('componentProvider', function() {
1204+
it('should work with angular 1.2+ directives', function () {
1205+
$stateProvider.state('ng12-dynamic-directive', {
1206+
url: '/ng12dynamicDirective/:type',
1207+
componentProvider: ['$stateParams', function($stateParams) {
1208+
return $stateParams.type;
1209+
}]
1210+
});
1211+
1212+
var $state = svcs.$state, $q = svcs.$q;
1213+
1214+
$state.transitionTo('ng12-dynamic-directive', {type: 'ng12DynamicDirective'}); $q.flush();
1215+
1216+
directiveEl = el[0].querySelector('div ui-view ng12-dynamic-directive');
1217+
expect(directiveEl).toBeDefined();
1218+
expect($state.current.name).toBe('ng12-dynamic-directive');
1219+
expect(el.text()).toBe('dynamic directive');
1220+
});
1221+
1222+
if (angular.version.minor >= 5) {
1223+
it('should load correct component when using componentProvider', function() {
1224+
$stateProvider.state('dynamicComponent', {
1225+
url: '/dynamicComponent/:type',
1226+
componentProvider: ['$stateParams', function($stateParams) {
1227+
return $stateParams.type;
1228+
}]
1229+
});
1230+
1231+
var $state = svcs.$state, $httpBackend = svcs.$httpBackend, $q = svcs.$q;
1232+
1233+
$state.transitionTo('dynamicComponent', {type: 'dynamicComponent'});
1234+
$q.flush();
1235+
1236+
directiveEl = el[0].querySelector('div ui-view dynamic-component');
1237+
expect(directiveEl).toBeDefined();
1238+
expect($state.current.name).toBe('dynamicComponent');
1239+
expect(el.text()).toBe('dynamicComponent');
1240+
});
1241+
}
1242+
});
11911243
});

test/viewHookSpec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,4 @@ describe("view hooks", () => {
167167
});
168168

169169
});
170-
});
170+
});

0 commit comments

Comments
 (0)