feat(core): allow to add precompiled tokens via a provider

Introduces the new `ANALYZE_FOR_PRECOMPILE` token. This token can be used to
create a virtual provider that will populate the `precompile` fields of
components and app modules based on its
`useValue`. All components that are referenced in the `useValue`
value (either directly or in a nested array or map) will be added
to the `precompile` property.

closes #9874
related to #9726
This commit is contained in:
Tobias Bosch 2016-07-07 10:05:55 -07:00
parent 9d265b6f61
commit 7073cf74fe
12 changed files with 215 additions and 30 deletions

View File

@ -11,13 +11,16 @@ import {BrowserModule} from '@angular/platform-browser';
import {AnimateCmp} from './animate';
import {BasicComp} from './basic';
import {CompWithPrecompile} from './precompile';
import {CompWithAnalyzePrecompileProvider, CompWithPrecompile} from './precompile';
import {ProjectingComp} from './projection';
import {CompWithChildQuery} from './queries';
@AppModule({
modules: [BrowserModule],
precompile: [AnimateCmp, BasicComp, CompWithPrecompile, ProjectingComp, CompWithChildQuery]
precompile: [
AnimateCmp, BasicComp, CompWithPrecompile, CompWithAnalyzePrecompileProvider, ProjectingComp,
CompWithChildQuery
]
})
export class MainModule {
constructor(public appRef: ApplicationRef) {}

View File

@ -7,7 +7,7 @@
*/
import {LowerCasePipe, NgIf} from '@angular/common';
import {AppModule, Component, ComponentFactoryResolver, Injectable} from '@angular/core';
import {ANALYZE_FOR_PRECOMPILE, AppModule, Component, ComponentFactoryResolver, Inject, Injectable, OpaqueToken} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
@Injectable()
@ -53,3 +53,17 @@ export class SomeModule {
})
export class SomeModuleUsingParentComp {
}
export const SOME_TOKEN = new OpaqueToken('someToken');
export function provideValueWithPrecompile(value: any) {
return [
{provide: SOME_TOKEN, useValue: value},
{provide: ANALYZE_FOR_PRECOMPILE, useValue: value, multi: true},
];
}
@AppModule({providers: [provideValueWithPrecompile([{a: 'b', component: SomeComp}])]})
export class SomeModuleWithAnalyzePrecompileProvider {
constructor(@Inject(SOME_TOKEN) public providedValue: any) {}
}

View File

@ -6,10 +6,30 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, ComponentFactoryResolver, Inject, OpaqueToken} from '@angular/core';
import {ANALYZE_FOR_PRECOMPILE, Component, ComponentFactoryResolver, Inject, OpaqueToken} from '@angular/core';
import {BasicComp} from './basic';
@Component({selector: 'cmp-precompile', template: '', precompile: [BasicComp]})
export class CompWithPrecompile {
constructor(public cfr: ComponentFactoryResolver) {}
}
export const SOME_TOKEN = new OpaqueToken('someToken');
export function provideValueWithPrecompile(value: any) {
return [
{provide: SOME_TOKEN, useValue: value},
{provide: ANALYZE_FOR_PRECOMPILE, useValue: value, multi: true},
];
}
@Component({
selector: 'comp-precompile-provider',
template: '',
providers: [provideValueWithPrecompile([{a: 'b', component: BasicComp}])]
})
export class CompWithAnalyzePrecompileProvider {
constructor(public cfr: ComponentFactoryResolver, @Inject(SOME_TOKEN) public providedValue: any) {
}
}

View File

@ -7,7 +7,7 @@
*/
import './init';
import {NestedModule, NestedService, ParentComp, SomeComp, SomeModule, SomeService} from '../src/module_fixtures';
import {SomeModuleNgFactory, SomeModuleUsingParentCompNgFactory} from '../src/module_fixtures.ngfactory';
import {SomeModuleNgFactory, SomeModuleUsingParentCompNgFactory, SomeModuleWithAnalyzePrecompileProviderNgFactory} from '../src/module_fixtures.ngfactory';
import {createComponent, createModule} from './util';
describe('AppModule', () => {
@ -26,6 +26,15 @@ describe('AppModule', () => {
expect(compRef.instance instanceof SomeComp).toBe(true);
});
it('should support precompile via the ANALYZE_FOR_PRECOMPILE provider and function providers in components',
() => {
const moduleRef = createModule(SomeModuleWithAnalyzePrecompileProviderNgFactory);
const cf = moduleRef.componentFactoryResolver.resolveComponentFactory(SomeComp);
expect(cf.componentType).toBe(SomeComp);
// check that the function call that created the provider for ANALYZE_FOR_PRECOMPILE worked.
expect(moduleRef.instance.providedValue).toEqual([{a: 'b', component: SomeComp}]);
});
it('should support module directives and pipes', () => {
var compFixture = createComponent(SomeComp, SomeModuleNgFactory);
var debugElement = compFixture.debugElement;

View File

@ -7,14 +7,27 @@
*/
import './init';
import {BasicComp} from '../src/basic';
import {CompWithPrecompile} from '../src/precompile';
import {CompWithAnalyzePrecompileProvider, CompWithPrecompile} from '../src/precompile';
import {createComponent} from './util';
describe('content projection', () => {
it('should support basic content projection', () => {
it('should support precompile in components', () => {
var compFixture = createComponent(CompWithPrecompile);
var cf = compFixture.componentInstance.cfr.resolveComponentFactory(BasicComp);
expect(cf.componentType).toBe(BasicComp);
});
it('should support precompile via the ANALYZE_FOR_PRECOMPILE provider and function providers in components',
() => {
const compFixture = createComponent(CompWithAnalyzePrecompileProvider);
const cf = compFixture.componentInstance.cfr.resolveComponentFactory(BasicComp);
expect(cf.componentType).toBe(BasicComp);
// check that the function call that created the provider for ANALYZE_FOR_PRECOMPILE worked.
expect(compFixture.componentInstance.providedValue).toEqual([
{a: 'b', component: BasicComp}
]);
});
});

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AppModuleFactory, ChangeDetectionStrategy, ChangeDetectorRef, ComponentFactory, ComponentFactoryResolver, ElementRef, Injector, QueryList, RenderComponentType, Renderer, SecurityContext, SimpleChange, TemplateRef, ViewContainerRef, ViewEncapsulation} from '@angular/core';
import {ANALYZE_FOR_PRECOMPILE, AppModuleFactory, ChangeDetectionStrategy, ChangeDetectorRef, ComponentFactory, ComponentFactoryResolver, ElementRef, Injector, QueryList, RenderComponentType, Renderer, SecurityContext, SimpleChange, TemplateRef, ViewContainerRef, ViewEncapsulation} from '@angular/core';
import {AnimationGroupPlayer as AnimationGroupPlayer_, AnimationKeyframe as AnimationKeyframe_, AnimationSequencePlayer as AnimationSequencePlayer_, AnimationStyles as AnimationStyles_, AppElement, AppModuleInjector, AppView, ChangeDetectorStatus, CodegenComponentFactoryResolver, DebugAppView, DebugContext, EMPTY_ARRAY, EMPTY_MAP, NoOpAnimationPlayer as NoOpAnimationPlayer_, StaticNodeDebugInfo, TemplateRef_, ValueUnwrapper, ViewType, ViewUtils, balanceAnimationKeyframes as impBalanceAnimationKeyframes, castByValue, checkBinding, clearStyles as impClearStyles, collectAndResolveStyles as impCollectAndResolveStyles, devModeEqual, flattenNestedViewRenderNodes, interpolate, prepareFinalAnimationStyles as impBalanceAnimationStyles, pureProxy1, pureProxy10, pureProxy2, pureProxy3, pureProxy4, pureProxy5, pureProxy6, pureProxy7, pureProxy8, pureProxy9, renderStyles as impRenderStyles, uninitialized} from '../core_private';
@ -58,6 +58,11 @@ var impNoOpAnimationPlayer = NoOpAnimationPlayer_;
var ANIMATION_STYLE_UTIL_ASSET_URL = assetUrl('core', 'animation/animation_style_util');
export class Identifiers {
static ANALYZE_FOR_PRECOMPILE = new CompileIdentifierMetadata({
name: 'ANALYZE_FOR_PRECOMPILE',
moduleUrl: assetUrl('core', 'metadata/di'),
runtime: ANALYZE_FOR_PRECOMPILE
});
static ViewUtils = new CompileIdentifierMetadata(
{name: 'ViewUtils', moduleUrl: assetUrl('core', 'linker/view_utils'), runtime: impViewUtils});
static AppView = new CompileIdentifierMetadata(

View File

@ -18,6 +18,7 @@ import * as cpl from './compile_metadata';
import {CompilerConfig} from './config';
import {hasLifecycleHook} from './directive_lifecycle_reflector';
import {DirectiveResolver} from './directive_resolver';
import {Identifiers, identifierToken} from './identifiers';
import {PipeResolver} from './pipe_resolver';
import {getUrlScheme} from './url_resolver';
import {MODULE_SUFFIX, ValueTransformer, sanitizeIdentifier, visitValue} from './util';
@ -137,7 +138,7 @@ export class CompileMetadataResolver {
changeDetectionStrategy = cmpMeta.changeDetection;
if (isPresent(dirMeta.viewProviders)) {
viewProviders = this.getProvidersMetadata(
verifyNonBlankProviders(directiveType, dirMeta.viewProviders, 'viewProviders'));
verifyNonBlankProviders(directiveType, dirMeta.viewProviders, 'viewProviders'), []);
}
moduleUrl = componentModuleUrl(this._reflector, directiveType, cmpMeta);
if (cmpMeta.precompile) {
@ -149,10 +150,11 @@ export class CompileMetadataResolver {
var providers: any[] /** TODO #9100 */ = [];
if (isPresent(dirMeta.providers)) {
providers = this.getProvidersMetadata(
verifyNonBlankProviders(directiveType, dirMeta.providers, 'providers'));
verifyNonBlankProviders(directiveType, dirMeta.providers, 'providers'),
precompileTypes);
}
var queries: any[] /** TODO #9100 */ = [];
var viewQueries: any[] /** TODO #9100 */ = [];
var queries: cpl.CompileQueryMetadata[] = [];
var viewQueries: cpl.CompileQueryMetadata[] = [];
if (isPresent(dirMeta.queries)) {
queries = this.getQueriesMetadata(dirMeta.queries, false, directiveType);
viewQueries = this.getQueriesMetadata(dirMeta.queries, true, directiveType);
@ -214,7 +216,7 @@ export class CompileMetadataResolver {
}
if (meta.providers) {
providers.push(...this.getProvidersMetadata(meta.providers));
providers.push(...this.getProvidersMetadata(meta.providers, precompile));
}
if (meta.directives) {
directives.push(...flattenArray(meta.directives)
@ -410,23 +412,54 @@ export class CompileMetadataResolver {
return compileToken;
}
getProvidersMetadata(providers: any[]):
getProvidersMetadata(providers: any[], targetPrecompileComponents: cpl.CompileTypeMetadata[]):
Array<cpl.CompileProviderMetadata|cpl.CompileTypeMetadata|any[]> {
return providers.map((provider) => {
const compileProviders: Array<cpl.CompileProviderMetadata|cpl.CompileTypeMetadata|any[]> = [];
providers.forEach((provider) => {
provider = resolveForwardRef(provider);
if (isProviderLiteral(provider)) {
provider = createProvider(provider);
}
let compileProvider: cpl.CompileProviderMetadata|cpl.CompileTypeMetadata|any[];
if (isArray(provider)) {
return this.getProvidersMetadata(provider);
compileProvider = this.getProvidersMetadata(provider, targetPrecompileComponents);
} else if (provider instanceof Provider) {
return this.getProviderMetadata(provider);
} else if (isProviderLiteral(provider)) {
return this.getProviderMetadata(createProvider(provider));
let tokenMeta = this.getTokenMetadata(provider.token);
if (tokenMeta.equalsTo(identifierToken(Identifiers.ANALYZE_FOR_PRECOMPILE))) {
targetPrecompileComponents.push(...this.getPrecompileComponentsFromProvider(provider));
} else {
compileProvider = this.getProviderMetadata(provider);
}
} else if (isValidType(provider)) {
return this.getTypeMetadata(provider, staticTypeModuleUrl(provider));
compileProvider = this.getTypeMetadata(provider, staticTypeModuleUrl(provider));
} else {
throw new BaseException(
`Invalid provider - only instances of Provider and Type are allowed, got: ${stringify(provider)}`);
}
if (compileProvider) {
compileProviders.push(compileProvider);
}
});
return compileProviders;
}
getPrecompileComponentsFromProvider(provider: Provider): cpl.CompileTypeMetadata[] {
let components: cpl.CompileTypeMetadata[] = [];
let collectedIdentifiers: cpl.CompileIdentifierMetadata[] = [];
if (provider.useFactory || provider.useExisting || provider.useClass) {
throw new BaseException(`The ANALYZE_FOR_PRECOMPILE token only supports useValue!`);
}
if (!provider.multi) {
throw new BaseException(`The ANALYZE_FOR_PRECOMPILE token only supports 'multi = true'!`);
}
convertToCompileValue(provider.useValue, collectedIdentifiers);
collectedIdentifiers.forEach((identifier) => {
let dirMeta = this.maybeGetDirectiveMetadata(identifier.runtime);
if (dirMeta) {
components.push(dirMeta.type);
}
});
return components;
}
getProviderMetadata(provider: Provider): cpl.CompileProviderMetadata {
@ -447,7 +480,7 @@ export class CompileMetadataResolver {
return new cpl.CompileProviderMetadata({
token: this.getTokenMetadata(provider.token),
useClass: compileTypeMetadata,
useValue: convertToCompileValue(provider.useValue),
useValue: convertToCompileValue(provider.useValue, []),
useFactory: compileFactoryMetadata,
useExisting: isPresent(provider.useExisting) ? this.getTokenMetadata(provider.useExisting) :
null,
@ -566,17 +599,21 @@ function componentModuleUrl(
return reflector.importUri(type);
}
// Only fill CompileIdentifierMetadata.runtime if needed...
function convertToCompileValue(value: any): any {
return visitValue(value, new _CompileValueConverter(), null);
function convertToCompileValue(
value: any, targetIdentifiers: cpl.CompileIdentifierMetadata[]): any {
return visitValue(value, new _CompileValueConverter(), targetIdentifiers);
}
class _CompileValueConverter extends ValueTransformer {
visitOther(value: any, context: any): any {
visitOther(value: any, targetIdentifiers: cpl.CompileIdentifierMetadata[]): any {
let identifier: cpl.CompileIdentifierMetadata;
if (cpl.isStaticSymbol(value)) {
return new cpl.CompileIdentifierMetadata({name: value.name, moduleUrl: value.filePath});
identifier = new cpl.CompileIdentifierMetadata(
{name: value.name, moduleUrl: value.filePath, runtime: value});
} else {
return new cpl.CompileIdentifierMetadata({runtime: value});
identifier = new cpl.CompileIdentifierMetadata({runtime: value});
}
targetIdentifiers.push(identifier);
return identifier;
}
}

View File

@ -20,7 +20,7 @@ import {ComponentMetadata, DirectiveMetadata, HostBindingMetadata, HostListenerM
import {ViewEncapsulation, ViewMetadata} from './metadata/view';
export {AppModuleMetadata} from './metadata/app_module';
export {AttributeMetadata, ContentChildMetadata, ContentChildrenMetadata, QueryMetadata, ViewChildMetadata, ViewChildrenMetadata, ViewQueryMetadata} from './metadata/di';
export {ANALYZE_FOR_PRECOMPILE, AttributeMetadata, ContentChildMetadata, ContentChildrenMetadata, QueryMetadata, ViewChildMetadata, ViewChildrenMetadata, ViewQueryMetadata} from './metadata/di';
export {ComponentMetadata, DirectiveMetadata, HostBindingMetadata, HostListenerMetadata, InputMetadata, OutputMetadata, PipeMetadata} from './metadata/directives';
export {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, OnChanges, OnDestroy, OnInit} from './metadata/lifecycle_hooks';
export {ViewEncapsulation, ViewMetadata} from './metadata/view';

View File

@ -8,8 +8,44 @@
import {resolveForwardRef} from '../di/forward_ref';
import {DependencyMetadata} from '../di/metadata';
import {OpaqueToken} from '../di/opaque_token';
import {StringWrapper, Type, isString, stringify} from '../facade/lang';
/**
* This token can be used to create a virtual provider that will populate the
* `precompile` fields of components and app modules based on its `useValue`.
* All components that are referenced in the `useValue` value (either directly
* or in a nested array or map) will be added to the `precompile` property.
*
* ### Example
* The following example shows how the router can populate the `precompile`
* field of an AppModule based on the router configuration which refers
* to components.
*
* ```typescript
* // helper function inside the router
* function provideRoutes(routes) {
* return [
* {provide: ROUTES, useValue: routes},
* {provide: ANALYZE_FOR_PRECOMPILE, useValue: routes, multi: true}
* ];
* }
*
* // user code
* let routes = [
* {path: '/root', component: RootComp},
* {path: /teams', component: TeamsComp}
* ];
*
* @AppModule({
* providers: [provideRoutes(routes)]
* })
* class ModuleWithRoutes {}
* ```
*
* @experimental
*/
export const ANALYZE_FOR_PRECOMPILE = new OpaqueToken('AnalyzeForPrecompile');
/**
* Specifies that a constant attribute value should be injected.

View File

@ -1,6 +1,6 @@
import {LowerCasePipe, NgIf} from '@angular/common';
import {CompilerConfig} from '@angular/compiler';
import {AppModule, AppModuleMetadata, Compiler, Component, ComponentFactoryResolver, ComponentRef, ComponentResolver, DebugElement, Host, Inject, Injectable, Injector, OpaqueToken, Optional, Provider, ReflectiveInjector, SelfMetadata, SkipSelf, SkipSelfMetadata, forwardRef, getDebugNode, provide} from '@angular/core';
import {ANALYZE_FOR_PRECOMPILE, AppModule, AppModuleMetadata, Compiler, Component, ComponentFactoryResolver, ComponentRef, ComponentResolver, DebugElement, Host, Inject, Injectable, Injector, OpaqueToken, Optional, Provider, ReflectiveInjector, SelfMetadata, SkipSelf, SkipSelfMetadata, forwardRef, getDebugNode, provide} from '@angular/core';
import {ComponentFixture, configureCompiler} from '@angular/core/testing';
import {AsyncTestCompleter, beforeEach, beforeEachProviders, ddescribe, describe, expect, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal';
@ -87,6 +87,14 @@ class SomeComp {
class ModuleWithPrecompile {
}
@AppModule({
providers:
[{provide: ANALYZE_FOR_PRECOMPILE, multi: true, useValue: [{a: 'b', component: SomeComp}]}]
})
class ModuleWithAnalyzePrecompileProvider {
}
@Component({
selector: 'comp',
template: `<div [title]="'HELLO' | lowercase"></div><div *ngIf="true"></div>`
@ -137,6 +145,16 @@ function declareTests({useJit}: {useJit: boolean}) {
.toBe(SomeComp);
});
it('should resolve ComponentFactories via ANALYZE_FOR_PRECOMPILE', () => {
let appModule = compiler.compileAppModuleSync(ModuleWithAnalyzePrecompileProvider).create();
expect(appModule.componentFactoryResolver.resolveComponentFactory(SomeComp).componentType)
.toBe(SomeComp);
expect(appModule.injector.get(ComponentFactoryResolver)
.resolveComponentFactory(SomeComp)
.componentType)
.toBe(SomeComp);
});
it('should resolve ComponentFactories for nested modules', () => {
let appModule =
compiler

View File

@ -10,7 +10,7 @@ import {beforeEach, ddescribe, xdescribe, describe, expect, iit, inject, beforeE
import {TestComponentBuilder} from '@angular/compiler/testing';
import {AsyncTestCompleter} from '@angular/core/testing/testing_internal';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {Component, ViewChild, ViewContainerRef, ComponentFactoryResolver, NoComponentFactoryError, ComponentRef, forwardRef} from '@angular/core';
import {Component, ViewChild, ViewContainerRef, ComponentFactoryResolver, NoComponentFactoryError, ComponentRef, forwardRef, ANALYZE_FOR_PRECOMPILE} from '@angular/core';
import {CompilerConfig} from '@angular/compiler';
export function main() {
@ -34,6 +34,17 @@ function declareTests({useJit}: {useJit: boolean}) {
});
}));
it('should resolve ComponentFactories via ANALYZE_FOR_PRECOMPILE',
inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
let compFixture = tcb.createSync(CompWithAnalyzePrecompileProvider);
let mainComp: CompWithAnalyzePrecompileProvider = compFixture.componentInstance;
let cfr: ComponentFactoryResolver =
compFixture.debugElement.injector.get(ComponentFactoryResolver);
expect(cfr.resolveComponentFactory(ChildComp).componentType).toBe(ChildComp);
expect(cfr.resolveComponentFactory(NestedChildComp).componentType).toBe(NestedChildComp);
}));
it('should be able to get a component form a parent component (view hiearchy)',
inject(
[TestComponentBuilder, AsyncTestCompleter],
@ -70,6 +81,7 @@ function declareTests({useJit}: {useJit: boolean}) {
async.done();
});
}));
});
}
@ -98,3 +110,18 @@ class ChildComp {
class MainComp {
constructor(public cfr: ComponentFactoryResolver) {}
}
@Component({
selector: 'comp-with-analyze',
template: '',
providers: [{
provide: ANALYZE_FOR_PRECOMPILE,
multi: true,
useValue: [
{a: 'b', component: ChildComp},
{b: 'c', anotherComponent: NestedChildComp},
]
}]
})
class CompWithAnalyzePrecompileProvider {
}

View File

@ -25,6 +25,9 @@ export declare abstract class AfterViewInit {
abstract ngAfterViewInit(): any;
}
/** @experimental */
export declare const ANALYZE_FOR_PRECOMPILE: OpaqueToken;
/** @experimental */
export declare function animate(timing: string | number, styles?: AnimationStyleMetadata | AnimationKeyframesSequenceMetadata): AnimationAnimateMetadata;