feat(upgrade): support lazy-loading Angular module into AngularJS app
This commit is contained in:
parent
44b50427d9
commit
30e76fcd80
@ -24,6 +24,7 @@ export const $$TESTABILITY = '$$testability';
|
|||||||
export const COMPILER_KEY = '$$angularCompiler';
|
export const COMPILER_KEY = '$$angularCompiler';
|
||||||
export const GROUP_PROJECTABLE_NODES_KEY = '$$angularGroupProjectableNodes';
|
export const GROUP_PROJECTABLE_NODES_KEY = '$$angularGroupProjectableNodes';
|
||||||
export const INJECTOR_KEY = '$$angularInjector';
|
export const INJECTOR_KEY = '$$angularInjector';
|
||||||
|
export const LAZY_MODULE_REF = '$$angularLazyModuleRef';
|
||||||
export const NG_ZONE_KEY = '$$angularNgZone';
|
export const NG_ZONE_KEY = '$$angularNgZone';
|
||||||
|
|
||||||
export const REQUIRE_INJECTOR = '?^^' + INJECTOR_KEY;
|
export const REQUIRE_INJECTOR = '?^^' + INJECTOR_KEY;
|
||||||
|
@ -9,9 +9,14 @@
|
|||||||
import {ComponentFactory, ComponentFactoryResolver, Injector, Type} from '@angular/core';
|
import {ComponentFactory, ComponentFactoryResolver, Injector, Type} from '@angular/core';
|
||||||
|
|
||||||
import * as angular from './angular1';
|
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 {DowngradeComponentAdapter} from './downgrade_component_adapter';
|
||||||
import {controllerKey, getComponentName} from './util';
|
import {LazyModuleRef, controllerKey, getComponentName, isFunction} from './util';
|
||||||
|
|
||||||
|
|
||||||
|
interface Thenable<T> {
|
||||||
|
then(callback: (value: T) => any): any;
|
||||||
|
}
|
||||||
|
|
||||||
let downgradeCount = 0;
|
let downgradeCount = 0;
|
||||||
|
|
||||||
@ -50,6 +55,8 @@ let downgradeCount = 0;
|
|||||||
*/
|
*/
|
||||||
export function downgradeComponent(info: {
|
export function downgradeComponent(info: {
|
||||||
component: Type<any>;
|
component: Type<any>;
|
||||||
|
/** @experimental */
|
||||||
|
propagateDigest?: boolean;
|
||||||
/** @deprecated since v4. This parameter is no longer used */
|
/** @deprecated since v4. This parameter is no longer used */
|
||||||
inputs?: string[];
|
inputs?: string[];
|
||||||
/** @deprecated since v4. This parameter is no longer used */
|
/** @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
|
// triggered by `UpgradeNg1ComponentAdapterBuilder`, before the Angular templates have
|
||||||
// been compiled.
|
// been compiled.
|
||||||
|
|
||||||
const parentInjector: Injector|ParentInjectorPromise =
|
|
||||||
required[0] || $injector.get(INJECTOR_KEY);
|
|
||||||
const ngModel: angular.INgModelController = required[1];
|
const ngModel: angular.INgModelController = required[1];
|
||||||
|
let parentInjector: Injector|Thenable<Injector>|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 downgradeFn = (injector: Injector) => {
|
||||||
const componentFactoryResolver: ComponentFactoryResolver =
|
const componentFactoryResolver: ComponentFactoryResolver =
|
||||||
@ -98,18 +110,26 @@ export function downgradeComponent(info: {
|
|||||||
|
|
||||||
const projectableNodes = facade.compileContents();
|
const projectableNodes = facade.compileContents();
|
||||||
facade.createComponent(projectableNodes);
|
facade.createComponent(projectableNodes);
|
||||||
facade.setupInputs();
|
facade.setupInputs(info.propagateDigest);
|
||||||
facade.setupOutputs();
|
facade.setupOutputs();
|
||||||
facade.registerCleanup();
|
facade.registerCleanup();
|
||||||
|
|
||||||
injectorPromise.resolve(facade.getInjector());
|
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<Injector>(parentInjector)) {
|
||||||
parentInjector.then(downgradeFn);
|
parentInjector.then(downgradeFn);
|
||||||
} else {
|
} else {
|
||||||
downgradeFn(parentInjector);
|
downgradeFn(parentInjector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ranAsync = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -155,3 +175,7 @@ class ParentInjectorPromise {
|
|||||||
this.callbacks.length = 0;
|
this.callbacks.length = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isThenable<T>(obj: object): obj is Thenable<T> {
|
||||||
|
return isFunction((obj as any).then);
|
||||||
|
}
|
||||||
|
@ -18,8 +18,9 @@ const INITIAL_VALUE = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class DowngradeComponentAdapter {
|
export class DowngradeComponentAdapter {
|
||||||
|
private implementsOnChanges = false;
|
||||||
private inputChangeCount: number = 0;
|
private inputChangeCount: number = 0;
|
||||||
private inputChanges: SimpleChanges|null = null;
|
private inputChanges: SimpleChanges = {};
|
||||||
private componentScope: angular.IScope;
|
private componentScope: angular.IScope;
|
||||||
private componentRef: ComponentRef<any>|null = null;
|
private componentRef: ComponentRef<any>|null = null;
|
||||||
private component: any = null;
|
private component: any = null;
|
||||||
@ -64,7 +65,7 @@ export class DowngradeComponentAdapter {
|
|||||||
hookupNgModel(this.ngModel, this.component);
|
hookupNgModel(this.ngModel, this.component);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupInputs(): void {
|
setupInputs(propagateDigest = true): void {
|
||||||
const attrs = this.attrs;
|
const attrs = this.attrs;
|
||||||
const inputs = this.componentFactory.inputs || [];
|
const inputs = this.componentFactory.inputs || [];
|
||||||
for (let i = 0; i < inputs.length; i++) {
|
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;
|
const prototype = this.componentFactory.componentType.prototype;
|
||||||
if (prototype && (<OnChanges>prototype).ngOnChanges) {
|
this.implementsOnChanges = !!(prototype && (<OnChanges>prototype).ngOnChanges);
|
||||||
// Detect: OnChanges interface
|
|
||||||
this.inputChanges = {};
|
this.componentScope.$watch(() => this.inputChangeCount, () => {
|
||||||
this.componentScope.$watch(() => this.inputChangeCount, () => {
|
// Invoke `ngOnChanges()`
|
||||||
|
if (this.implementsOnChanges) {
|
||||||
const inputChanges = this.inputChanges;
|
const inputChanges = this.inputChanges;
|
||||||
this.inputChanges = {};
|
this.inputChanges = {};
|
||||||
(<OnChanges>this.component).ngOnChanges(inputChanges !);
|
(<OnChanges>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() {
|
setupOutputs() {
|
||||||
@ -181,11 +194,11 @@ export class DowngradeComponentAdapter {
|
|||||||
getInjector(): Injector { return this.componentRef ! && this.componentRef !.injector; }
|
getInjector(): Injector { return this.componentRef ! && this.componentRef !.injector; }
|
||||||
|
|
||||||
private updateInput(prop: string, prevValue: any, currValue: any) {
|
private updateInput(prop: string, prevValue: any, currValue: any) {
|
||||||
if (this.inputChanges) {
|
if (this.implementsOnChanges) {
|
||||||
this.inputChangeCount++;
|
|
||||||
this.inputChanges[prop] = new SimpleChange(prevValue, currValue, prevValue === currValue);
|
this.inputChanges[prop] = new SimpleChange(prevValue, currValue, prevValue === currValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.inputChangeCount++;
|
||||||
this.component[prop] = currValue;
|
this.component[prop] = currValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* 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';
|
import * as angular from './angular1';
|
||||||
|
|
||||||
const DIRECTIVE_PREFIX_REGEXP = /^(?:x|data)[:\-_]/i;
|
const DIRECTIVE_PREFIX_REGEXP = /^(?:x|data)[:\-_]/i;
|
||||||
@ -67,6 +67,11 @@ export class Deferred<R> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LazyModuleRef {
|
||||||
|
injector?: Injector;
|
||||||
|
promise: Promise<Injector>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Whether the passed-in component implements the subset of the
|
* @return Whether the passed-in component implements the subset of the
|
||||||
* `ControlValueAccessor` interface needed for AngularJS `ng-model`
|
* `ControlValueAccessor` interface needed for AngularJS `ng-model`
|
||||||
|
@ -10,7 +10,7 @@ import {Compiler, CompilerOptions, Directive, Injector, NgModule, NgModuleRef, N
|
|||||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||||
|
|
||||||
import * as angular from '../common/angular1';
|
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 {downgradeComponent} from '../common/downgrade_component';
|
||||||
import {downgradeInjectable} from '../common/downgrade_injectable';
|
import {downgradeInjectable} from '../common/downgrade_injectable';
|
||||||
import {Deferred, controllerKey, onError} from '../common/util';
|
import {Deferred, controllerKey, onError} from '../common/util';
|
||||||
@ -495,6 +495,12 @@ export class UpgradeAdapter {
|
|||||||
this.ngZone = new NgZone({enableLongStackTrace: Zone.hasOwnProperty('longStackTraceZoneSpec')});
|
this.ngZone = new NgZone({enableLongStackTrace: Zone.hasOwnProperty('longStackTraceZoneSpec')});
|
||||||
this.ng2BootstrapDeferred = new Deferred();
|
this.ng2BootstrapDeferred = new Deferred();
|
||||||
ng1Module.factory(INJECTOR_KEY, () => this.moduleRef !.injector.get(Injector))
|
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)
|
.constant(NG_ZONE_KEY, this.ngZone)
|
||||||
.factory(COMPILER_KEY, () => this.moduleRef !.injector.get(Compiler))
|
.factory(COMPILER_KEY, () => this.moduleRef !.injector.get(Compiler))
|
||||||
.config([
|
.config([
|
||||||
|
60
packages/upgrade/src/static/downgrade_module.ts
Normal file
60
packages/upgrade/src/static/downgrade_module.ts
Normal file
@ -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<T>(
|
||||||
|
moduleFactoryOrBootstrapFn: NgModuleFactory<T>|
|
||||||
|
((extraProviders: Provider[]) => Promise<NgModuleRef<T>>)): 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;
|
||||||
|
}
|
@ -6,13 +6,14 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 * 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 {controllerKey} from '../common/util';
|
||||||
|
|
||||||
import {angular1Providers, setTempInjectorRef} from './angular1_providers';
|
import {angular1Providers, setTempInjectorRef} from './angular1_providers';
|
||||||
|
import {NgAdapterInjector} from './util';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -163,6 +164,13 @@ export class UpgradeModule {
|
|||||||
|
|
||||||
.value(INJECTOR_KEY, this.injector)
|
.value(INJECTOR_KEY, this.injector)
|
||||||
|
|
||||||
|
.factory(
|
||||||
|
LAZY_MODULE_REF,
|
||||||
|
[
|
||||||
|
INJECTOR_KEY,
|
||||||
|
(injector: Injector) => ({injector, promise: Promise.resolve(injector)})
|
||||||
|
])
|
||||||
|
|
||||||
.config([
|
.config([
|
||||||
$PROVIDE, $INJECTOR,
|
$PROVIDE, $INJECTOR,
|
||||||
($provide: angular.IProvideService, $injector: angular.IInjectorService) => {
|
($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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
26
packages/upgrade/src/static/util.ts
Normal file
26
packages/upgrade/src/static/util.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@ export {getAngularLib, setAngularLib} from './src/common/angular1';
|
|||||||
export {downgradeComponent} from './src/common/downgrade_component';
|
export {downgradeComponent} from './src/common/downgrade_component';
|
||||||
export {downgradeInjectable} from './src/common/downgrade_injectable';
|
export {downgradeInjectable} from './src/common/downgrade_injectable';
|
||||||
export {VERSION} from './src/common/version';
|
export {VERSION} from './src/common/version';
|
||||||
|
export {downgradeModule} from './src/static/downgrade_module';
|
||||||
export {UpgradeComponent} from './src/static/upgrade_component';
|
export {UpgradeComponent} from './src/static/upgrade_component';
|
||||||
export {UpgradeModule} from './src/static/upgrade_module';
|
export {UpgradeModule} from './src/static/upgrade_module';
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
* found in the LICENSE file at https://angular.io/license
|
* 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 {async} from '@angular/core/testing';
|
||||||
import {BrowserModule} from '@angular/platform-browser';
|
import {BrowserModule} from '@angular/platform-browser';
|
||||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
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(() => {
|
it('should initialize inputs in time for `ngOnChanges`', async(() => {
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ng2',
|
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'));
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
4
tools/public_api_guard/upgrade/static.d.ts
vendored
4
tools/public_api_guard/upgrade/static.d.ts
vendored
@ -1,6 +1,7 @@
|
|||||||
/** @experimental */
|
/** @experimental */
|
||||||
export declare function downgradeComponent(info: {
|
export declare function downgradeComponent(info: {
|
||||||
component: Type<any>;
|
component: Type<any>;
|
||||||
|
/** @experimental */ propagateDigest?: boolean;
|
||||||
/** @deprecated */ inputs?: string[];
|
/** @deprecated */ inputs?: string[];
|
||||||
/** @deprecated */ outputs?: string[];
|
/** @deprecated */ outputs?: string[];
|
||||||
/** @deprecated */ selectors?: string[];
|
/** @deprecated */ selectors?: string[];
|
||||||
@ -9,6 +10,9 @@ export declare function downgradeComponent(info: {
|
|||||||
/** @experimental */
|
/** @experimental */
|
||||||
export declare function downgradeInjectable(token: any): Function;
|
export declare function downgradeInjectable(token: any): Function;
|
||||||
|
|
||||||
|
/** @experimental */
|
||||||
|
export declare function downgradeModule<T>(moduleFactoryOrBootstrapFn: NgModuleFactory<T> | ((extraProviders: Provider[]) => Promise<NgModuleRef<T>>)): string;
|
||||||
|
|
||||||
/** @stable */
|
/** @stable */
|
||||||
export declare function getAngularLib(): any;
|
export declare function getAngularLib(): any;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user