feat(upgrade): support lazy-loading Angular module into AngularJS app
This commit is contained in:

committed by
Alex Rickabaugh

parent
44b50427d9
commit
30e76fcd80
@ -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('<ng2 [value1]="value1" value2="{{ value2 }}"></ng2>');
|
||||
|
||||
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('<ng2 [value1]="value1" value2="{{ value2 }}"></ng2>');
|
||||
|
||||
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',
|
||||
|
@ -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 }}) | <ng2B [value]="value"></ng2B>'})
|
||||
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<Ng2Module>(bootstrapFn);
|
||||
const ng1Module =
|
||||
angular.module('ng1', [lazyModuleName])
|
||||
.directive(
|
||||
'ng2', downgradeComponent({component: Ng2AComponent, propagateDigest: false}))
|
||||
.run(($rootScope: angular.IRootScopeService) => $rootScope.value = 0);
|
||||
|
||||
const element = html('<div><ng2 [value]="value" ng-if="loadNg2"></ng2></div>');
|
||||
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<Ng2Module>(bootstrapFn);
|
||||
const ng1Module =
|
||||
angular.module('ng1', [lazyModuleName])
|
||||
.directive(
|
||||
'ng2', downgradeComponent({component: Ng2Component, propagateDigest: false}))
|
||||
.value('ng1Value', 'foo');
|
||||
|
||||
const element = html('<div><ng2 ng-if="loadNg2"></ng2></div>');
|
||||
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'));
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user