Skip to content

Commit f6c552d

Browse files
feat(resolve): auto-map resolve values to matching component inputs in AoT
When angular started stripping reflect metadata for AoT, it broke our ability to introspect a component to determine what inputs it had. In this commit angular/angular@bd64ab5 the inputs/outputs are added to the component factory, and we now have a public API to introspect component inputs. Closes #94 Closes #89 Closes #36
1 parent d13ed47 commit f6c552d

File tree

9 files changed

+112
-74
lines changed

9 files changed

+112
-74
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@uirouter/angular",
3-
"description": "State-based routing for Angular 2",
4-
"version": "1.0.0-beta.5",
3+
"description": "State-based routing for Angular",
4+
"version": "1.0.0-beta.6",
55
"scripts": {
66
"clean": "shx rm -rf lib lib-esm _bundles _dec",
77
"compile": "npm run clean && ngc",

src/directives/uiView.ts

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
/** */
33
import {
44
Component, ComponentFactoryResolver, ViewContainerRef, Input, ComponentRef, Type, ReflectiveInjector, ViewChild,
5-
Injector, Inject
5+
Injector, Inject, ComponentFactory
66
} from '@angular/core';
7-
import { ɵReflectorReader as ReflectorReader } from '@angular/core';
7+
88
import {
99
UIRouter, isFunction, Transition, parse, TransitionHookFn, StateDeclaration, inArray, trace, ViewContext, ViewConfig,
1010
ActiveUIView, ResolveContext, NATIVE_INJECTOR_TOKEN, flattenR
@@ -35,29 +35,8 @@ interface InputMapping {
3535
*
3636
* @internalapi
3737
*/
38-
const ng2ComponentInputs = (reflector: ReflectorReader, ng2CompClass: Type<any>, component: any) => {
39-
/** Get "@Input('foo') _foo" inputs */
40-
let props = reflector.propMetadata(ng2CompClass);
41-
let _props = Object.keys(props || {})
42-
// -> [ { key: string, anno: annotations[] } ] tuples
43-
.map(key => ({ key, annoArr: props[key] }))
44-
// -> flattened to [ { key: string, anno: annotation } ] tuples
45-
.reduce((acc, tuple) => acc.concat(tuple.annoArr.map(anno => ({ key: tuple.key, anno }))), [])
46-
// Only Inputs
47-
.filter(tuple => tuple.anno instanceof Input)
48-
// If they have a bindingPropertyName, i.e. "@Input('foo') _foo", then foo, else _foo
49-
.map(tuple => ({ token: tuple.anno.bindingPropertyName || tuple.key, prop: tuple.key }));
50-
51-
/** Get "inputs: ['foo']" inputs */
52-
let inputs = reflector.annotations(ng2CompClass)
53-
// Find the ComponentMetadata class annotation
54-
.filter(x => x instanceof Component && !!x.inputs)
55-
// Get the .inputs string array
56-
.map(x => x.inputs)
57-
.reduce(flattenR, [])
58-
.map(input => ({ token: input, prop: input }));
59-
60-
return _props.concat(inputs) as InputMapping[];
38+
const ng2ComponentInputs = (factory: ComponentFactory<any>): InputMapping[] => {
39+
return factory.inputs.map(input => ({ prop: input.propName, token: input.templateName }));
6140
};
6241

6342
/**
@@ -149,7 +128,6 @@ export class UIView {
149128
public router: UIRouter,
150129
@Inject(UIView.PARENT_INJECT) parent,
151130
public viewContainerRef: ViewContainerRef,
152-
private reflector: ReflectorReader
153131
) {
154132
this.parent = parent;
155133
}
@@ -189,7 +167,7 @@ export class UIView {
189167
const state: StateDeclaration = parse("uiViewData.config.viewDecl.$context.self")(this);
190168

191169
if (trans.exiting().indexOf(state) !== -1) {
192-
trans.onStart({}, function(trans) {
170+
trans.onStart({}, function() {
193171
return uiCanExitFn.call(instance, trans);
194172
});
195173
}
@@ -244,7 +222,7 @@ export class UIView {
244222
this.componentRef = this.componentTarget.createComponent(compFactory, undefined, componentInjector);
245223

246224
// Wire resolves to @Input()s
247-
this.applyInputBindings(this.componentRef, context, componentClass);
225+
this.applyInputBindings(compFactory, this.componentRef, context, componentClass);
248226
}
249227

250228
/**
@@ -262,7 +240,7 @@ export class UIView {
262240
let resolvables = context.getTokens().map(token => context.getResolvable(token)).filter(r => r.resolved);
263241
let newProviders = resolvables.map(r => ({ provide: r.token, useValue: r.data }));
264242

265-
var parentInject = { context: this.uiViewData.config.viewDecl.$context, fqn: this.uiViewData.fqn };
243+
let parentInject = { context: this.uiViewData.config.viewDecl.$context, fqn: this.uiViewData.fqn };
266244
newProviders.push({ provide: UIView.PARENT_INJECT, useValue: parentInject });
267245

268246
let parentComponentInjector = this.viewContainerRef.injector;
@@ -278,16 +256,24 @@ export class UIView {
278256
* Finds component inputs which match resolves (by name) and sets the input value
279257
* to the resolve data.
280258
*/
281-
applyInputBindings(ref: ComponentRef<any>, context: ResolveContext, componentClass) {
259+
applyInputBindings(factory: ComponentFactory<any>, ref: ComponentRef<any>, context: ResolveContext, componentClass) {
282260
const component = ref.instance;
283261
const bindings = this.uiViewData.config.viewDecl['bindings'] || {};
284262
const explicitBoundProps = Object.keys(bindings);
285263

286-
// Supply resolve data to matching @Input('prop') or inputs: ['prop']
264+
// Returns the actual component property for a renamed an input renamed using `@Input('foo') _foo`.
265+
// return the `_foo` property
266+
const renamedInputProp = (prop: string) => {
267+
const input = factory.inputs.find(i => i.templateName === prop);
268+
return input && input.propName || prop;
269+
};
270+
271+
// Supply resolve data to component as specified in the state's `bindings: {}`
287272
const explicitInputTuples = explicitBoundProps
288-
.reduce((acc, key) => acc.concat([{ prop: key, token: bindings[key] }]), []);
273+
.reduce((acc, key) => acc.concat([{ prop: renamedInputProp(key), token: bindings[key] }]), []);
289274

290-
const implicitInputTuples = ng2ComponentInputs(this.reflector, componentClass, component)
275+
// Supply resolve data to matching @Input('prop') or inputs: ['prop']
276+
const implicitInputTuples = ng2ComponentInputs(factory)
291277
.filter(tuple => !inArray(explicitBoundProps, tuple.prop));
292278

293279
const addResolvable = (tuple: InputMapping) => ({
@@ -298,7 +284,7 @@ export class UIView {
298284
explicitInputTuples.concat(implicitInputTuples)
299285
.map(addResolvable)
300286
.filter(tuple => tuple.resolvable && tuple.resolvable.resolved)
301-
.forEach(tuple => { component[tuple.prop] = tuple.resolvable.data });
287+
.forEach(tuple => { component[tuple.prop] = tuple.resolvable.data; });
302288

303289
// Initiate change detection for the newly created component
304290
ref.changeDetectorRef.detectChanges();

src/private_import_core.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

test/matchers.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/// <reference path='../typings/jasmine/jasmine.d.ts' />
21
import {equals} from "@uirouter/core";
32
declare var testablePromise;
43

test/ngModule/lazyModule.spec.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { UIView } from '../../src/directives/uiView';
44
import { memoryLocationPlugin, UIRouter } from '@uirouter/core';
55
import { NgModuleFactoryLoader, SystemJsNgModuleLoader } from '@angular/core';
66

7-
declare var System;
7+
declare let System;
88

99
let futureFoo = {
1010
name: 'foo.**',
@@ -16,19 +16,18 @@ function configFn(router: UIRouter) {
1616
router.plugin(memoryLocationPlugin);
1717
}
1818

19-
beforeEach(() => {
20-
let routerModule = UIRouterModule.forRoot({ useHash: true, states: [futureFoo], config: configFn });
21-
22-
TestBed.configureTestingModule({
23-
declarations: [],
24-
imports: [routerModule],
25-
providers: [
26-
{ provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader },
27-
]
28-
});
29-
});
30-
3119
describe('lazy loading', () => {
20+
beforeEach(() => {
21+
let routerModule = UIRouterModule.forRoot({ useHash: true, states: [futureFoo], config: configFn });
22+
23+
TestBed.configureTestingModule({
24+
declarations: [],
25+
imports: [routerModule],
26+
providers: [
27+
{ provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader },
28+
]
29+
});
30+
});
3231

3332
it('should lazy load a module', async(inject([UIRouter], (router: UIRouter) => {
3433
const fixture = TestBed.createComponent(UIView);
@@ -46,7 +45,7 @@ describe('lazy loading', () => {
4645
names = stateRegistry.get().map(state => state.name).sort();
4746
expect(names.length).toBe(4);
4847
expect(names).toEqual(['', 'foo', 'foo.child1', 'foo.child2']);
49-
})
48+
});
5049
})));
5150

5251

test/tsconfig.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,5 @@
1515
"skipTemplateCodegen": true,
1616
"strictMetadataEmit": true
1717
},
18-
"files": [
19-
"./index.js"
20-
]
18+
"include": ["./**/*.ts"]
2119
}

test/uiSref.spec.ts renamed to test/uiSref/uiSref.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { Component, DebugElement } from '@angular/core';
22
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
33
import { By } from '@angular/platform-browser';
44

5-
import { UIRouterModule } from '../src/uiRouterNgModule';
6-
import { UISref } from '../src/directives/uiSref';
5+
import { UIRouterModule } from '../../src/uiRouterNgModule';
6+
import { UISref } from '../../src/directives/uiSref';
77
import { UIRouter } from '@uirouter/core';
88
import { Subject } from 'rxjs/Subject';
99

@@ -34,7 +34,7 @@ describe('uiSref', () => {
3434
beforeEach(() => {
3535
fixture = TestBed.configureTestingModule({
3636
declarations: [TestComponent],
37-
imports: [UIRouterModule]
37+
imports: [UIRouterModule.forRoot({ useHash: true })]
3838
}).createComponent(TestComponent);
3939
fixture.detectChanges();
4040
des = fixture.debugElement.queryAll(By.directive(UISref));
@@ -61,7 +61,7 @@ describe('uiSref', () => {
6161
} as any;
6262
TestBed.configureTestingModule({
6363
declarations: [TestComponent],
64-
imports: [UIRouterModule]
64+
imports: [UIRouterModule.forRoot({ useHash: true })]
6565
}).overrideComponent(TestComponent, {
6666
set: {
6767
providers: [

test/uiView.spec.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

test/uiView/resolveBinding.spec.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Component, DebugElement, Input, NgModuleFactoryLoader, SystemJsNgModuleLoader } from '@angular/core';
2+
import { ComponentFixture, TestBed } from '@angular/core/testing';
3+
import { UIRouterModule } from '../../src/uiRouterNgModule';
4+
import { Ng2StateDeclaration } from '../../src/interface';
5+
import { UIView } from '../../src/directives/uiView';
6+
import { By } from '@angular/platform-browser';
7+
import { UIRouter } from '@uirouter/core';
8+
9+
describe('uiView', () => {
10+
11+
describe('should map resolve data to inputs', () => {
12+
@Component({ template: `<h3>hey</h3> ` })
13+
class ManyResolvesComponent {
14+
@Input() resolve1;
15+
@Input() resolve2;
16+
@Input('resolve3') _resolve3;
17+
@Input('resolve4') _resolve4;
18+
}
19+
20+
let comp: ManyResolvesComponent;
21+
let fixture: ComponentFixture<UIView>;
22+
let router: UIRouter;
23+
24+
beforeEach((done) => {
25+
const manyResolves: Ng2StateDeclaration = {
26+
name: 'manyResolves',
27+
component: ManyResolvesComponent,
28+
bindings: {
29+
// component_input: 'resolve name'
30+
resolve2: 'Resolve2',
31+
resolve4: 'Resolve4',
32+
},
33+
resolve: [
34+
{ token: 'resolve1', resolveFn: () => 'resolve1' },
35+
{ token: 'Resolve2', resolveFn: () => 'resolve2' },
36+
{ token: 'resolve3', resolveFn: () => 'resolve3' },
37+
{ token: 'Resolve4', resolveFn: () => 'resolve4' },
38+
],
39+
};
40+
41+
let routerModule = UIRouterModule.forRoot({ useHash: true, states: [manyResolves] });
42+
43+
fixture = TestBed.configureTestingModule({
44+
declarations: [ManyResolvesComponent],
45+
imports: [routerModule],
46+
}).createComponent(UIView);
47+
48+
fixture.detectChanges();
49+
router = fixture.debugElement.injector.get(UIRouter);
50+
router.stateService.go('manyResolves').then(() => {
51+
comp = fixture.debugElement.query(By.directive(ManyResolvesComponent)).componentInstance;
52+
done();
53+
});
54+
});
55+
56+
it('should bind resolve by name to component input name', () => {
57+
expect(comp.resolve1).toBe('resolve1');
58+
});
59+
60+
it('should bind resolve by name to the component input specified by `bindings`', () => {
61+
expect(comp.resolve2).toBe('resolve2');
62+
});
63+
64+
it('should bind resolve by name to component input templateName', () => {
65+
expect(comp._resolve3).toBe('resolve3');
66+
});
67+
68+
it('should bind resolve by name to the component input templateName specified in state `bindings`', () => {
69+
expect(comp._resolve4).toBe('resolve4');
70+
});
71+
});
72+
});

0 commit comments

Comments
 (0)