Skip to content

Commit b59caaa

Browse files
feat(view): Route a view to a directive using componentProvider
State definitions can dynamically load a component's template by using componentProvider.
1 parent b5c731d commit b59caaa

File tree

6 files changed

+149
-54
lines changed

6 files changed

+149
-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/templateFactorySpec.js

+22
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ describe('templateFactory', function () {
55

66
beforeEach(module('ui.router.util'));
77

8+
beforeEach(function() {
9+
app = angular.module('foo', []);
10+
var component = {
11+
bindings: { bar: '<' },
12+
template: '{{$ctrl.cmpdata}}',
13+
};
14+
15+
ctrl = function controller() {
16+
this.data = "DATA";
17+
};
18+
19+
app.component('foo', angular.extend({}, component, {controller: ctrl}));
20+
21+
});
22+
23+
beforeEach(angular['mock'].module('foo'));
24+
825
it('exists', inject(function ($templateFactory) {
926
expect($templateFactory).toBeDefined();
1027
}));
@@ -16,4 +33,9 @@ describe('templateFactory', function () {
1633
$templateFactory.fromUrl('views/view.html');
1734
$httpBackend.flush();
1835
}));
36+
37+
it('should get template from component', inject(function($templateFactory) {
38+
var test = $templateFactory.fromComponent('foo', {test: test});
39+
expect(test).toEqual("<foo bar='::$resolve.bar'></foo>");
40+
}));
1941
});

test/viewDirectiveSpec.js

+25
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,10 @@ describe('angular 1.5+ style .component()', function() {
837837
bindings: { evt: '&' },
838838
template: 'eventCmp',
839839
});
840+
841+
app.component('dynamicComponent', {
842+
template: 'dynamicComponent'
843+
})
840844
}
841845
}));
842846

@@ -1188,4 +1192,25 @@ describe('angular 1.5+ style .component()', function() {
11881192
});
11891193
}
11901194
});
1195+
1196+
describe('componentProvider', function() {
1197+
it('should load correct component when using componentProvider', function() {
1198+
$stateProvider.state('dynamicComponent', {
1199+
url: '/dynamicComponent/:type',
1200+
componentProvider: ['$stateParams', function($stateParams) {
1201+
return $stateParams.type;
1202+
}]
1203+
});
1204+
1205+
var $state = svcs.$state, $httpBackend = svcs.$httpBackend, $q = svcs.$q;
1206+
1207+
$state.transitionTo('dynamicComponent', {type: 'dynamicComponent'});
1208+
$q.flush();
1209+
1210+
directiveEl = el[0].querySelector('div ui-view component-provider');
1211+
expect(directiveEl).toBeDefined();
1212+
expect($state.current.name).toBe('dynamicComponent');
1213+
expect(el.text()).toBe('dynamicComponent');
1214+
});
1215+
});
11911216
});

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)