diff --git a/packages/upgrade/src/common/constants.ts b/packages/upgrade/src/common/constants.ts index 3f4cb03ff9..207d3867be 100644 --- a/packages/upgrade/src/common/constants.ts +++ b/packages/upgrade/src/common/constants.ts @@ -24,6 +24,7 @@ export const $$TESTABILITY = '$$testability'; export const COMPILER_KEY = '$$angularCompiler'; export const GROUP_PROJECTABLE_NODES_KEY = '$$angularGroupProjectableNodes'; export const INJECTOR_KEY = '$$angularInjector'; +export const LAZY_MODULE_REF = '$$angularLazyModuleRef'; export const NG_ZONE_KEY = '$$angularNgZone'; export const REQUIRE_INJECTOR = '?^^' + INJECTOR_KEY; diff --git a/packages/upgrade/src/common/downgrade_component.ts b/packages/upgrade/src/common/downgrade_component.ts index 62d90fc2b7..8968ba5dd4 100644 --- a/packages/upgrade/src/common/downgrade_component.ts +++ b/packages/upgrade/src/common/downgrade_component.ts @@ -9,9 +9,14 @@ import {ComponentFactory, ComponentFactoryResolver, Injector, Type} from '@angular/core'; import * as angular from './angular1'; -import {$COMPILE, $INJECTOR, $PARSE, INJECTOR_KEY, REQUIRE_INJECTOR, REQUIRE_NG_MODEL} from './constants'; +import {$COMPILE, $INJECTOR, $PARSE, INJECTOR_KEY, LAZY_MODULE_REF, REQUIRE_INJECTOR, REQUIRE_NG_MODEL} from './constants'; import {DowngradeComponentAdapter} from './downgrade_component_adapter'; -import {controllerKey, getComponentName} from './util'; +import {LazyModuleRef, controllerKey, getComponentName, isFunction} from './util'; + + +interface Thenable { + then(callback: (value: T) => any): any; +} let downgradeCount = 0; @@ -50,6 +55,8 @@ let downgradeCount = 0; */ export function downgradeComponent(info: { component: Type; + /** @experimental */ + propagateDigest?: boolean; /** @deprecated since v4. This parameter is no longer used */ inputs?: string[]; /** @deprecated since v4. This parameter is no longer used */ @@ -76,9 +83,14 @@ export function downgradeComponent(info: { // triggered by `UpgradeNg1ComponentAdapterBuilder`, before the Angular templates have // been compiled. - const parentInjector: Injector|ParentInjectorPromise = - required[0] || $injector.get(INJECTOR_KEY); const ngModel: angular.INgModelController = required[1]; + let parentInjector: Injector|Thenable|undefined = required[0]; + let ranAsync = false; + + if (!parentInjector) { + const lazyModuleRef = $injector.get(LAZY_MODULE_REF) as LazyModuleRef; + parentInjector = lazyModuleRef.injector || lazyModuleRef.promise; + } const downgradeFn = (injector: Injector) => { const componentFactoryResolver: ComponentFactoryResolver = @@ -98,18 +110,26 @@ export function downgradeComponent(info: { const projectableNodes = facade.compileContents(); facade.createComponent(projectableNodes); - facade.setupInputs(); + facade.setupInputs(info.propagateDigest); facade.setupOutputs(); facade.registerCleanup(); injectorPromise.resolve(facade.getInjector()); + + if (ranAsync) { + // If this is run async, it is possible that it is not run inside a + // digest and initial input values will not be detected. + scope.$evalAsync(() => {}); + } }; - if (parentInjector instanceof ParentInjectorPromise) { + if (isThenable(parentInjector)) { parentInjector.then(downgradeFn); } else { downgradeFn(parentInjector); } + + ranAsync = true; } }; }; @@ -155,3 +175,7 @@ class ParentInjectorPromise { this.callbacks.length = 0; } } + +function isThenable(obj: object): obj is Thenable { + return isFunction((obj as any).then); +} diff --git a/packages/upgrade/src/common/downgrade_component_adapter.ts b/packages/upgrade/src/common/downgrade_component_adapter.ts index 1d05dc151e..af9dad0e12 100644 --- a/packages/upgrade/src/common/downgrade_component_adapter.ts +++ b/packages/upgrade/src/common/downgrade_component_adapter.ts @@ -18,8 +18,9 @@ const INITIAL_VALUE = { }; export class DowngradeComponentAdapter { + private implementsOnChanges = false; private inputChangeCount: number = 0; - private inputChanges: SimpleChanges|null = null; + private inputChanges: SimpleChanges = {}; private componentScope: angular.IScope; private componentRef: ComponentRef|null = null; private component: any = null; @@ -64,7 +65,7 @@ export class DowngradeComponentAdapter { hookupNgModel(this.ngModel, this.component); } - setupInputs(): void { + setupInputs(propagateDigest = true): void { const attrs = this.attrs; const inputs = this.componentFactory.inputs || []; for (let i = 0; i < inputs.length; i++) { @@ -114,17 +115,29 @@ export class DowngradeComponentAdapter { } } + // Invoke `ngOnChanges()` and Change Detection (when necessary) + const detectChanges = () => this.changeDetector && this.changeDetector.detectChanges(); const prototype = this.componentFactory.componentType.prototype; - if (prototype && (prototype).ngOnChanges) { - // Detect: OnChanges interface - this.inputChanges = {}; - this.componentScope.$watch(() => this.inputChangeCount, () => { + this.implementsOnChanges = !!(prototype && (prototype).ngOnChanges); + + this.componentScope.$watch(() => this.inputChangeCount, () => { + // Invoke `ngOnChanges()` + if (this.implementsOnChanges) { const inputChanges = this.inputChanges; this.inputChanges = {}; (this.component).ngOnChanges(inputChanges !); - }); + } + + // If opted out of propagating digests, invoke change detection when inputs change + if (!propagateDigest) { + detectChanges(); + } + }); + + // If not opted out of propagating digests, invoke change detection on every digest + if (propagateDigest) { + this.componentScope.$watch(detectChanges); } - this.componentScope.$watch(() => this.changeDetector && this.changeDetector.detectChanges()); } setupOutputs() { @@ -181,11 +194,11 @@ export class DowngradeComponentAdapter { getInjector(): Injector { return this.componentRef ! && this.componentRef !.injector; } private updateInput(prop: string, prevValue: any, currValue: any) { - if (this.inputChanges) { - this.inputChangeCount++; + if (this.implementsOnChanges) { this.inputChanges[prop] = new SimpleChange(prevValue, currValue, prevValue === currValue); } + this.inputChangeCount++; this.component[prop] = currValue; } diff --git a/packages/upgrade/src/common/util.ts b/packages/upgrade/src/common/util.ts index 8d4f30eaa0..849f6dd4dc 100644 --- a/packages/upgrade/src/common/util.ts +++ b/packages/upgrade/src/common/util.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Type} from '@angular/core'; +import {Injector, Type} from '@angular/core'; import * as angular from './angular1'; const DIRECTIVE_PREFIX_REGEXP = /^(?:x|data)[:\-_]/i; @@ -67,6 +67,11 @@ export class Deferred { } } +export interface LazyModuleRef { + injector?: Injector; + promise: Promise; +} + /** * @return Whether the passed-in component implements the subset of the * `ControlValueAccessor` interface needed for AngularJS `ng-model` diff --git a/packages/upgrade/src/dynamic/upgrade_adapter.ts b/packages/upgrade/src/dynamic/upgrade_adapter.ts index fbddc22f35..038503acfd 100644 --- a/packages/upgrade/src/dynamic/upgrade_adapter.ts +++ b/packages/upgrade/src/dynamic/upgrade_adapter.ts @@ -10,7 +10,7 @@ import {Compiler, CompilerOptions, Directive, Injector, NgModule, NgModuleRef, N import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import * as angular from '../common/angular1'; -import {$$TESTABILITY, $COMPILE, $INJECTOR, $ROOT_SCOPE, COMPILER_KEY, INJECTOR_KEY, NG_ZONE_KEY} from '../common/constants'; +import {$$TESTABILITY, $COMPILE, $INJECTOR, $ROOT_SCOPE, COMPILER_KEY, INJECTOR_KEY, LAZY_MODULE_REF, NG_ZONE_KEY} from '../common/constants'; import {downgradeComponent} from '../common/downgrade_component'; import {downgradeInjectable} from '../common/downgrade_injectable'; import {Deferred, controllerKey, onError} from '../common/util'; @@ -495,6 +495,12 @@ export class UpgradeAdapter { this.ngZone = new NgZone({enableLongStackTrace: Zone.hasOwnProperty('longStackTraceZoneSpec')}); this.ng2BootstrapDeferred = new Deferred(); ng1Module.factory(INJECTOR_KEY, () => this.moduleRef !.injector.get(Injector)) + .factory( + LAZY_MODULE_REF, + [ + INJECTOR_KEY, + (injector: Injector) => ({injector, promise: Promise.resolve(injector)}) + ]) .constant(NG_ZONE_KEY, this.ngZone) .factory(COMPILER_KEY, () => this.moduleRef !.injector.get(Compiler)) .config([ diff --git a/packages/upgrade/src/static/downgrade_module.ts b/packages/upgrade/src/static/downgrade_module.ts new file mode 100644 index 0000000000..f54de29d4b --- /dev/null +++ b/packages/upgrade/src/static/downgrade_module.ts @@ -0,0 +1,60 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Injector, NgModuleFactory, NgModuleRef, Provider} from '@angular/core'; +import {platformBrowser} from '@angular/platform-browser'; + +import * as angular from '../common/angular1'; +import {$INJECTOR, INJECTOR_KEY, LAZY_MODULE_REF, UPGRADE_MODULE_NAME} from '../common/constants'; +import {LazyModuleRef, isFunction} from '../common/util'; + +import {angular1Providers, setTempInjectorRef} from './angular1_providers'; +import {NgAdapterInjector} from './util'; + + +/** @experimental */ +export function downgradeModule( + moduleFactoryOrBootstrapFn: NgModuleFactory| + ((extraProviders: Provider[]) => Promise>)): string { + const LAZY_MODULE_NAME = UPGRADE_MODULE_NAME + '.lazy'; + const bootstrapFn = isFunction(moduleFactoryOrBootstrapFn) ? + moduleFactoryOrBootstrapFn : + (extraProviders: Provider[]) => + platformBrowser(extraProviders).bootstrapModuleFactory(moduleFactoryOrBootstrapFn); + + let injector: Injector; + + // Create an ng1 module to bootstrap. + angular.module(LAZY_MODULE_NAME, []) + .factory( + INJECTOR_KEY, + () => { + if (!injector) { + throw new Error('The Angular module has not been bootstrapped yet.'); + } + return injector; + }) + .factory(LAZY_MODULE_REF, [ + $INJECTOR, + ($injector: angular.IInjectorService) => { + const result: LazyModuleRef = { + promise: bootstrapFn(angular1Providers).then(ref => { + setTempInjectorRef($injector); + + injector = result.injector = new NgAdapterInjector(ref.injector); + injector.get($INJECTOR); + + return injector; + }) + }; + return result; + } + ]); + + return LAZY_MODULE_NAME; +} diff --git a/packages/upgrade/src/static/upgrade_module.ts b/packages/upgrade/src/static/upgrade_module.ts index 9ff10c8265..0eeb69fc70 100644 --- a/packages/upgrade/src/static/upgrade_module.ts +++ b/packages/upgrade/src/static/upgrade_module.ts @@ -6,13 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injector, NgModule, NgZone, Testability, ɵNOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from '@angular/core'; +import {Injector, NgModule, NgZone, Testability} from '@angular/core'; import * as angular from '../common/angular1'; -import {$$TESTABILITY, $DELEGATE, $INJECTOR, $INTERVAL, $PROVIDE, INJECTOR_KEY, UPGRADE_MODULE_NAME} from '../common/constants'; +import {$$TESTABILITY, $DELEGATE, $INJECTOR, $INTERVAL, $PROVIDE, INJECTOR_KEY, LAZY_MODULE_REF, UPGRADE_MODULE_NAME} from '../common/constants'; import {controllerKey} from '../common/util'; import {angular1Providers, setTempInjectorRef} from './angular1_providers'; +import {NgAdapterInjector} from './util'; /** @@ -163,6 +164,13 @@ export class UpgradeModule { .value(INJECTOR_KEY, this.injector) + .factory( + LAZY_MODULE_REF, + [ + INJECTOR_KEY, + (injector: Injector) => ({injector, promise: Promise.resolve(injector)}) + ]) + .config([ $PROVIDE, $INJECTOR, ($provide: angular.IProvideService, $injector: angular.IInjectorService) => { @@ -265,19 +273,3 @@ export class UpgradeModule { } } } - -class NgAdapterInjector implements Injector { - constructor(private modInjector: Injector) {} - - // When Angular locate a service in the component injector tree, the not found value is set to - // `NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR`. In such a case we should not walk up to the module - // injector. - // AngularJS only supports a single tree and should always check the module injector. - get(token: any, notFoundValue?: any): any { - if (notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) { - return notFoundValue; - } - - return this.modInjector.get(token, notFoundValue); - } -} diff --git a/packages/upgrade/src/static/util.ts b/packages/upgrade/src/static/util.ts new file mode 100644 index 0000000000..20f237fa5d --- /dev/null +++ b/packages/upgrade/src/static/util.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Injector, ɵNOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from '@angular/core'; + + +export class NgAdapterInjector implements Injector { + constructor(private modInjector: Injector) {} + + // When Angular locate a service in the component injector tree, the not found value is set to + // `NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR`. In such a case we should not walk up to the module + // injector. + // AngularJS only supports a single tree and should always check the module injector. + get(token: any, notFoundValue?: any): any { + if (notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) { + return notFoundValue; + } + + return this.modInjector.get(token, notFoundValue); + } +} diff --git a/packages/upgrade/static/public_api.ts b/packages/upgrade/static/public_api.ts index 9d28d1c554..b8057bb535 100644 --- a/packages/upgrade/static/public_api.ts +++ b/packages/upgrade/static/public_api.ts @@ -16,6 +16,7 @@ export {getAngularLib, setAngularLib} from './src/common/angular1'; export {downgradeComponent} from './src/common/downgrade_component'; export {downgradeInjectable} from './src/common/downgrade_injectable'; export {VERSION} from './src/common/version'; +export {downgradeModule} from './src/static/downgrade_module'; export {UpgradeComponent} from './src/static/upgrade_component'; export {UpgradeModule} from './src/static/upgrade_module'; diff --git a/packages/upgrade/test/static/integration/downgrade_component_spec.ts b/packages/upgrade/test/static/integration/downgrade_component_spec.ts index 3a9a9c7872..12e674e7d9 100644 --- a/packages/upgrade/test/static/integration/downgrade_component_spec.ts +++ b/packages/upgrade/test/static/integration/downgrade_component_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Compiler, Component, ComponentFactoryResolver, EventEmitter, Injector, Input, NgModule, NgModuleRef, OnChanges, OnDestroy, SimpleChanges, destroyPlatform} from '@angular/core'; +import {ChangeDetectorRef, Compiler, Component, ComponentFactoryResolver, EventEmitter, Injector, Input, NgModule, NgModuleRef, OnChanges, OnDestroy, SimpleChanges, destroyPlatform} from '@angular/core'; import {async} from '@angular/core/testing'; import {BrowserModule} from '@angular/platform-browser'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; @@ -148,6 +148,132 @@ export function main() { }); })); + it('should run change-detection on every digest (by default)', async(() => { + let ng2Component: Ng2Component; + + @Component({selector: 'ng2', template: '{{ value1 }} | {{ value2 }}'}) + class Ng2Component { + @Input() value1 = -1; + @Input() value2 = -1; + + constructor() { ng2Component = this; } + } + + @NgModule({ + imports: [BrowserModule, UpgradeModule], + declarations: [Ng2Component], + entryComponents: [Ng2Component] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + const ng1Module = angular.module('ng1', []) + .directive('ng2', downgradeComponent({component: Ng2Component})) + .run(($rootScope: angular.IRootScopeService) => { + $rootScope.value1 = 0; + $rootScope.value2 = 0; + }); + + const element = html(''); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { + const $rootScope = upgrade.$injector.get('$rootScope') as angular.IRootScopeService; + + expect(element.textContent).toBe('0 | 0'); + + // Digest should invoke CD + $rootScope.$digest(); + $rootScope.$digest(); + expect(element.textContent).toBe('0 | 0'); + + // Internal changes should be detected on digest + ng2Component.value1 = 1; + ng2Component.value2 = 2; + $rootScope.$digest(); + expect(element.textContent).toBe('1 | 2'); + + // Digest should propagate change in prop-bound input + $rootScope.$apply('value1 = 3'); + expect(element.textContent).toBe('3 | 2'); + + // Digest should propagate change in attr-bound input + ng2Component.value1 = 4; + $rootScope.$apply('value2 = 5'); + expect(element.textContent).toBe('4 | 5'); + + // Digest should propagate changes that happened before the digest + $rootScope.value1 = 6; + expect(element.textContent).toBe('4 | 5'); + + $rootScope.$digest(); + expect(element.textContent).toBe('6 | 5'); + }); + })); + + it('should not run change-detection on every digest when opted out', async(() => { + let ng2Component: Ng2Component; + + @Component({selector: 'ng2', template: '{{ value1 }} | {{ value2 }}'}) + class Ng2Component { + @Input() value1 = -1; + @Input() value2 = -1; + + constructor() { ng2Component = this; } + } + + @NgModule({ + imports: [BrowserModule, UpgradeModule], + declarations: [Ng2Component], + entryComponents: [Ng2Component] + }) + class Ng2Module { + ngDoBootstrap() {} + } + + const ng1Module = + angular.module('ng1', []) + .directive( + 'ng2', downgradeComponent({component: Ng2Component, propagateDigest: false})) + .run(($rootScope: angular.IRootScopeService) => { + $rootScope.value1 = 0; + $rootScope.value2 = 0; + }); + + const element = html(''); + + bootstrap(platformBrowserDynamic(), Ng2Module, element, ng1Module).then(upgrade => { + const $rootScope = upgrade.$injector.get('$rootScope') as angular.IRootScopeService; + + expect(element.textContent).toBe('0 | 0'); + + // Digest should not invoke CD + $rootScope.$digest(); + $rootScope.$digest(); + expect(element.textContent).toBe('0 | 0'); + + // Digest should not invoke CD, even if component values have changed (internally) + ng2Component.value1 = 1; + ng2Component.value2 = 2; + $rootScope.$digest(); + expect(element.textContent).toBe('0 | 0'); + + // Digest should invoke CD, if prop-bound input has changed + $rootScope.$apply('value1 = 3'); + expect(element.textContent).toBe('3 | 2'); + + // Digest should invoke CD, if attr-bound input has changed + ng2Component.value1 = 4; + $rootScope.$apply('value2 = 5'); + expect(element.textContent).toBe('4 | 5'); + + // Digest should invoke CD, if input has changed before the digest + $rootScope.value1 = 6; + $rootScope.$digest(); + expect(element.textContent).toBe('6 | 5'); + }); + })); + it('should initialize inputs in time for `ngOnChanges`', async(() => { @Component({ selector: 'ng2', diff --git a/packages/upgrade/test/static/integration/downgrade_module_spec.ts b/packages/upgrade/test/static/integration/downgrade_module_spec.ts new file mode 100644 index 0000000000..f506c77ddc --- /dev/null +++ b/packages/upgrade/test/static/integration/downgrade_module_spec.ts @@ -0,0 +1,136 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Component, Inject, Input, NgModule, Provider, destroyPlatform} from '@angular/core'; +import {async} from '@angular/core/testing'; +import {BrowserModule} from '@angular/platform-browser'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import * as angular from '@angular/upgrade/src/common/angular1'; +import {$ROOT_SCOPE, INJECTOR_KEY} from '@angular/upgrade/src/common/constants'; +import {downgradeComponent, downgradeModule} from '@angular/upgrade/static'; + +import {html} from '../test_helpers'; + +export function main() { + describe('lazy-load ng2 module', () => { + + beforeEach(() => destroyPlatform()); + afterEach(() => destroyPlatform()); + + it('should support downgrading a component and propagate inputs', async(() => { + @Component({selector: 'ng2A', template: 'a({{ value }}) | '}) + class Ng2AComponent { + @Input() value = -1; + } + + @Component({selector: 'ng2B', template: 'b({{ value }})'}) + class Ng2BComponent { + @Input() value = -2; + } + + @NgModule({ + declarations: [Ng2AComponent, Ng2BComponent], + entryComponents: [Ng2AComponent], + imports: [BrowserModule], + }) + class Ng2Module { + ngDoBootstrap() {} + } + + const bootstrapFn = (extraProviders: Provider[]) => + platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); + const lazyModuleName = downgradeModule(bootstrapFn); + const ng1Module = + angular.module('ng1', [lazyModuleName]) + .directive( + 'ng2', downgradeComponent({component: Ng2AComponent, propagateDigest: false})) + .run(($rootScope: angular.IRootScopeService) => $rootScope.value = 0); + + const element = html('
'); + const $injector = angular.bootstrap(element, [ng1Module.name]); + const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService; + + expect(element.textContent).toBe(''); + expect(() => $injector.get(INJECTOR_KEY)).toThrowError(); + + $rootScope.$apply('value = 1'); + expect(element.textContent).toBe(''); + expect(() => $injector.get(INJECTOR_KEY)).toThrowError(); + + $rootScope.$apply('loadNg2 = true'); + expect(element.textContent).toBe(''); + expect(() => $injector.get(INJECTOR_KEY)).toThrowError(); + + // Wait for the module to be bootstrapped. + setTimeout(() => { + expect(() => $injector.get(INJECTOR_KEY)).not.toThrow(); + + // Wait for `$evalAsync()` to propagate inputs. + setTimeout(() => expect(element.textContent).toBe('a(1) | b(1)')); + }); + })); + + it('should support using an upgraded service', async(() => { + class Ng2Service { + constructor(@Inject('ng1Value') private ng1Value: string) {} + getValue = () => `${this.ng1Value}-bar`; + } + + @Component({selector: 'ng2', template: '{{ value }}'}) + class Ng2Component { + value: string; + constructor(ng2Service: Ng2Service) { this.value = ng2Service.getValue(); } + } + + @NgModule({ + declarations: [Ng2Component], + entryComponents: [Ng2Component], + imports: [BrowserModule], + providers: [ + Ng2Service, + { + provide: 'ng1Value', + useFactory: (i: angular.IInjectorService) => i.get('ng1Value'), + deps: ['$injector'], + }, + ], + }) + class Ng2Module { + ngDoBootstrap() {} + } + + const bootstrapFn = (extraProviders: Provider[]) => + platformBrowserDynamic(extraProviders).bootstrapModule(Ng2Module); + const lazyModuleName = downgradeModule(bootstrapFn); + const ng1Module = + angular.module('ng1', [lazyModuleName]) + .directive( + 'ng2', downgradeComponent({component: Ng2Component, propagateDigest: false})) + .value('ng1Value', 'foo'); + + const element = html('
'); + const $injector = angular.bootstrap(element, [ng1Module.name]); + const $rootScope = $injector.get($ROOT_SCOPE) as angular.IRootScopeService; + + expect(element.textContent).toBe(''); + expect(() => $injector.get(INJECTOR_KEY)).toThrowError(); + + $rootScope.$apply('loadNg2 = true'); + expect(element.textContent).toBe(''); + expect(() => $injector.get(INJECTOR_KEY)).toThrowError(); + + // Wait for the module to be bootstrapped. + setTimeout(() => { + expect(() => $injector.get(INJECTOR_KEY)).not.toThrow(); + + // Wait for `$evalAsync()` to propagate inputs. + setTimeout(() => expect(element.textContent).toBe('foo-bar')); + }); + })); + }); +} diff --git a/tools/public_api_guard/upgrade/static.d.ts b/tools/public_api_guard/upgrade/static.d.ts index 2eedf38587..75b541cad7 100644 --- a/tools/public_api_guard/upgrade/static.d.ts +++ b/tools/public_api_guard/upgrade/static.d.ts @@ -1,6 +1,7 @@ /** @experimental */ export declare function downgradeComponent(info: { component: Type; + /** @experimental */ propagateDigest?: boolean; /** @deprecated */ inputs?: string[]; /** @deprecated */ outputs?: string[]; /** @deprecated */ selectors?: string[]; @@ -9,6 +10,9 @@ export declare function downgradeComponent(info: { /** @experimental */ export declare function downgradeInjectable(token: any): Function; +/** @experimental */ +export declare function downgradeModule(moduleFactoryOrBootstrapFn: NgModuleFactory | ((extraProviders: Provider[]) => Promise>)): string; + /** @stable */ export declare function getAngularLib(): any;