diff --git a/modules/@angular/compiler-cli/integrationtest/src/module.ts b/modules/@angular/compiler-cli/integrationtest/src/module.ts index 77c6c62e38..0a0962b83b 100644 --- a/modules/@angular/compiler-cli/integrationtest/src/module.ts +++ b/modules/@angular/compiler-cli/integrationtest/src/module.ts @@ -14,7 +14,7 @@ import {AnimateCmp} from './animate'; import {BasicComp} from './basic'; import {CompWithAnalyzeEntryComponentsProvider, CompWithEntryComponents} from './entry_components'; import {CompWithProviders, CompWithReferences} from './features'; -import {CompUsingRootModuleDirectiveAndPipe, SomeDirectiveInRootModule, SomeLibModule, SomePipeInRootModule, SomeService} from './module_fixtures'; +import {CompUsingRootModuleDirectiveAndPipe, SomeDirectiveInRootModule, someLibModuleWithProviders, SomePipeInRootModule, SomeService} from './module_fixtures'; import {ProjectingComp} from './projection'; import {CompWithChildQuery, CompWithDirectiveChild} from './queries'; @@ -25,7 +25,7 @@ import {CompWithChildQuery, CompWithDirectiveChild} from './queries'; CompWithDirectiveChild, CompUsingRootModuleDirectiveAndPipe, CompWithProviders, CompWithReferences ], - imports: [BrowserModule, FormsModule, SomeLibModule], + imports: [BrowserModule, FormsModule, someLibModuleWithProviders()], providers: [SomeService], entryComponents: [ AnimateCmp, BasicComp, CompWithEntryComponents, CompWithAnalyzeEntryComponentsProvider, diff --git a/modules/@angular/compiler-cli/integrationtest/src/module_fixtures.ts b/modules/@angular/compiler-cli/integrationtest/src/module_fixtures.ts index 0c5af2cdbb..a1891e157e 100644 --- a/modules/@angular/compiler-cli/integrationtest/src/module_fixtures.ts +++ b/modules/@angular/compiler-cli/integrationtest/src/module_fixtures.ts @@ -7,7 +7,7 @@ */ import {LowerCasePipe, NgIf} from '@angular/common'; -import {ANALYZE_FOR_ENTRY_COMPONENTS, Component, ComponentFactoryResolver, Directive, Inject, Injectable, Input, NgModule, OpaqueToken, Pipe} from '@angular/core'; +import {ANALYZE_FOR_ENTRY_COMPONENTS, Component, ComponentFactoryResolver, Directive, Inject, Injectable, Input, ModuleWithProviders, NgModule, OpaqueToken, Pipe} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; @Injectable() @@ -60,11 +60,21 @@ export function provideValueWithEntryComponents(value: any) { @NgModule({ declarations: [SomeDirectiveInLibModule, SomePipeInLibModule, CompUsingLibModuleDirectiveAndPipe], + exports: [CompUsingLibModuleDirectiveAndPipe], entryComponents: [CompUsingLibModuleDirectiveAndPipe], - providers: [ - ServiceUsingLibModule, - provideValueWithEntryComponents([{a: 'b', component: CompUsingLibModuleDirectiveAndPipe}]) - ], }) export class SomeLibModule { } + +// TODO(tbosch): Make this a static method in `SomeLibModule` once +// our static reflector supports it. +// See https://github.com/angular/angular/issues/10266. +export function someLibModuleWithProviders(): ModuleWithProviders { + return { + ngModule: SomeLibModule, + providers: [ + ServiceUsingLibModule, + provideValueWithEntryComponents([{a: 'b', component: CompUsingLibModuleDirectiveAndPipe}]) + ] + }; +} diff --git a/modules/@angular/compiler/src/metadata_resolver.ts b/modules/@angular/compiler/src/metadata_resolver.ts index 74c6706a39..4b5f4e41e6 100644 --- a/modules/@angular/compiler/src/metadata_resolver.ts +++ b/modules/@angular/compiler/src/metadata_resolver.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AnimationAnimateMetadata, AnimationEntryMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationStateDeclarationMetadata, AnimationStateMetadata, AnimationStateTransitionMetadata, AnimationStyleMetadata, AnimationWithStepsMetadata, AttributeMetadata, ChangeDetectionStrategy, ComponentMetadata, HostMetadata, Inject, InjectMetadata, Injectable, NgModule, NgModuleMetadata, Optional, OptionalMetadata, Provider, QueryMetadata, SelfMetadata, SkipSelfMetadata, ViewMetadata, ViewQueryMetadata, resolveForwardRef} from '@angular/core'; +import {AnimationAnimateMetadata, AnimationEntryMetadata, AnimationGroupMetadata, AnimationKeyframesSequenceMetadata, AnimationMetadata, AnimationStateDeclarationMetadata, AnimationStateMetadata, AnimationStateTransitionMetadata, AnimationStyleMetadata, AnimationWithStepsMetadata, AttributeMetadata, ChangeDetectionStrategy, ComponentMetadata, HostMetadata, Inject, InjectMetadata, Injectable, ModuleWithProviders, NgModule, NgModuleMetadata, Optional, OptionalMetadata, Provider, QueryMetadata, SelfMetadata, SkipSelfMetadata, ViewMetadata, ViewQueryMetadata, resolveForwardRef} from '@angular/core'; import {Console, LIFECYCLE_HOOKS_VALUES, ReflectorReader, createProvider, isProviderLiteral, reflector} from '../core_private'; import {MapWrapper, StringMapWrapper} from '../src/facade/collection'; @@ -206,16 +206,24 @@ export class CompileMetadataResolver { const exportedPipes: cpl.CompilePipeMetadata[] = []; const importedModules: cpl.CompileNgModuleMetadata[] = []; const exportedModules: cpl.CompileNgModuleMetadata[] = []; + const providers: any[] = []; + const entryComponents: cpl.CompileTypeMetadata[] = []; if (meta.imports) { flattenArray(meta.imports).forEach((importedType) => { - if (!isValidType(importedType)) { - throw new BaseException( - `Unexpected value '${stringify(importedType)}' imported by the module '${stringify(moduleType)}'`); + let importedModuleType: Type; + if (isValidType(importedType)) { + importedModuleType = importedType; + } else if (importedType && importedType.ngModule) { + const moduleWithProviders: ModuleWithProviders = importedType; + importedModuleType = moduleWithProviders.ngModule; + if (moduleWithProviders.providers) { + providers.push( + ...this.getProvidersMetadata(moduleWithProviders.providers, entryComponents)); + } } - let importedModuleMeta: cpl.CompileNgModuleMetadata; - if (importedModuleMeta = this.getNgModuleMetadata(importedType, false)) { - importedModules.push(importedModuleMeta); + if (importedModuleType) { + importedModules.push(this.getNgModuleMetadata(importedModuleType, false)); } else { throw new BaseException( `Unexpected value '${stringify(importedType)}' imported by the module '${stringify(moduleType)}'`); @@ -274,8 +282,6 @@ export class CompileMetadataResolver { }); } - const providers: any[] = []; - const entryComponents: cpl.CompileTypeMetadata[] = []; if (meta.providers) { providers.push(...this.getProvidersMetadata(meta.providers, entryComponents)); } diff --git a/modules/@angular/core/src/metadata.ts b/modules/@angular/core/src/metadata.ts index 0660a84bfe..8bffa69842 100644 --- a/modules/@angular/core/src/metadata.ts +++ b/modules/@angular/core/src/metadata.ts @@ -16,13 +16,13 @@ import {ChangeDetectionStrategy} from '../src/change_detection/change_detection' import {AnimationEntryMetadata} from './animation/metadata'; import {AttributeMetadata, ContentChildMetadata, ContentChildrenMetadata, QueryMetadata, ViewChildMetadata, ViewChildrenMetadata, ViewQueryMetadata} from './metadata/di'; import {ComponentMetadata, DirectiveMetadata, HostBindingMetadata, HostListenerMetadata, InputMetadata, OutputMetadata, PipeMetadata} from './metadata/directives'; -import {NgModuleMetadata} from './metadata/ng_module'; +import {ModuleWithProviders, NgModuleMetadata} from './metadata/ng_module'; import {ViewEncapsulation, ViewMetadata} from './metadata/view'; export {ANALYZE_FOR_ENTRY_COMPONENTS, 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 {NgModuleMetadata} from './metadata/ng_module'; +export {ModuleWithProviders, NgModuleMetadata} from './metadata/ng_module'; export {ViewEncapsulation, ViewMetadata} from './metadata/view'; import {makeDecorator, makeParamDecorator, makePropDecorator, TypeDecorator,} from './util/decorators'; @@ -498,7 +498,7 @@ export interface NgModuleMetadataFactory { (obj?: { providers?: any[], declarations?: Array, - imports?: Array, + imports?: Array, exports?: Array, entryComponents?: Array }): NgModuleDecorator; diff --git a/modules/@angular/core/src/metadata/ng_module.ts b/modules/@angular/core/src/metadata/ng_module.ts index d87068e2fc..50b75bbbb3 100644 --- a/modules/@angular/core/src/metadata/ng_module.ts +++ b/modules/@angular/core/src/metadata/ng_module.ts @@ -9,6 +9,16 @@ import {InjectableMetadata} from '../di/metadata'; import {Type} from '../facade/lang'; +/** + * A wrapper around a module that also includes the providers. + * + * @experimental + */ +export interface ModuleWithProviders { + ngModule: Type; + providers?: any[]; +} + /** * Declares an Angular Module. * @experimental @@ -65,6 +75,7 @@ export class NgModuleMetadata extends InjectableMetadata { /** * Specifies a list of modules whose exported directives/pipes * should be available to templates in this module. + * This can also contain {@link ModuleWithProviders}. * * ### Example * @@ -76,7 +87,7 @@ export class NgModuleMetadata extends InjectableMetadata { * } * ``` */ - imports: Array; + imports: Array; /** * Specifies a list of directives/pipes/module that can be used within the template diff --git a/modules/@angular/core/test/linker/ng_module_integration_spec.ts b/modules/@angular/core/test/linker/ng_module_integration_spec.ts index 064aa68eec..9b44a039bd 100644 --- a/modules/@angular/core/test/linker/ng_module_integration_spec.ts +++ b/modules/@angular/core/test/linker/ng_module_integration_spec.ts @@ -9,7 +9,7 @@ import {LowerCasePipe, NgIf} from '@angular/common'; import {CompilerConfig, NgModuleResolver, ViewResolver} from '@angular/compiler'; import {MockNgModuleResolver, MockViewResolver} from '@angular/compiler/testing'; -import {ANALYZE_FOR_ENTRY_COMPONENTS, Compiler, Component, ComponentFactoryResolver, ComponentRef, ComponentResolver, DebugElement, Directive, Host, HostBinding, Inject, Injectable, Injector, Input, NgModule, NgModuleMetadata, NgModuleRef, OpaqueToken, Optional, Pipe, Provider, ReflectiveInjector, SelfMetadata, SkipSelf, SkipSelfMetadata, ViewMetadata, forwardRef, getDebugNode, provide} from '@angular/core'; +import {ANALYZE_FOR_ENTRY_COMPONENTS, Compiler, Component, ComponentFactoryResolver, ComponentRef, ComponentResolver, DebugElement, Directive, Host, HostBinding, Inject, Injectable, Injector, Input, ModuleWithProviders, NgModule, NgModuleMetadata, NgModuleRef, OpaqueToken, Optional, Pipe, Provider, ReflectiveInjector, SelfMetadata, SkipSelf, SkipSelfMetadata, ViewMetadata, forwardRef, getDebugNode, provide} from '@angular/core'; import {Console} from '@angular/core/src/console'; import {ComponentFixture, configureCompiler} from '@angular/core/testing'; import {AsyncTestCompleter, beforeEach, beforeEachProviders, ddescribe, describe, iit, inject, it, xdescribe, xit} from '@angular/core/testing/testing_internal'; @@ -449,6 +449,28 @@ function declareTests({useJit}: {useJit: boolean}) { .toBe('transformed someValue'); }); + it('should support exported directives and pipes if the module is wrapped into an `ModuleWithProviders`', + () => { + @NgModule( + {declarations: [SomeDirective, SomePipe], exports: [SomeDirective, SomePipe]}) + class SomeImportedModule { + } + + @NgModule({ + declarations: [CompUsingModuleDirectiveAndPipe], + imports: [{ngModule: SomeImportedModule}], + entryComponents: [CompUsingModuleDirectiveAndPipe] + }) + class SomeModule { + } + + + const compFixture = createComp(CompUsingModuleDirectiveAndPipe, SomeModule); + compFixture.detectChanges(); + expect(compFixture.debugElement.children[0].properties['title']) + .toBe('transformed someValue'); + }); + it('should support reexported modules', () => { @NgModule({declarations: [SomeDirective, SomePipe], exports: [SomeDirective, SomePipe]}) class SomeReexportedModule { @@ -876,6 +898,26 @@ function declareTests({useJit}: {useJit: boolean}) { expect(injector.get('token1')).toBe('imported'); }); + it('should add the providers of imported ModuleWithProviders', () => { + @NgModule() + class ImportedModule { + } + + @NgModule({ + imports: [ + {ngModule: ImportedModule, providers: [{provide: 'token1', useValue: 'imported'}]} + ] + }) + class SomeModule { + } + + const injector = createModule(SomeModule).injector; + + expect(injector.get(SomeModule)).toBeAnInstanceOf(SomeModule); + expect(injector.get(ImportedModule)).toBeAnInstanceOf(ImportedModule); + expect(injector.get('token1')).toBe('imported'); + }); + it('should overwrite the providers of imported modules', () => { @NgModule({providers: [{provide: 'token1', useValue: 'imported'}]}) class ImportedModule {