Skip to content

Commit

Permalink
feat(upgrade): support lazy-loading Angular module into AngularJS app
Browse files Browse the repository at this point in the history
  • Loading branch information
gkalpak authored and alxhub committed Jul 14, 2017
1 parent 44b5042 commit 30e76fc
Show file tree
Hide file tree
Showing 12 changed files with 431 additions and 37 deletions.
1 change: 1 addition & 0 deletions packages/upgrade/src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
36 changes: 30 additions & 6 deletions packages/upgrade/src/common/downgrade_component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
then(callback: (value: T) => any): any;
}

let downgradeCount = 0;

Expand Down Expand Up @@ -50,6 +55,8 @@ let downgradeCount = 0;
*/
export function downgradeComponent(info: {
component: Type<any>;
/** @experimental */
propagateDigest?: boolean;

This comment has been minimized.

Copy link
@IgorMinar

IgorMinar Jul 18, 2017

Contributor

please add api docs

This comment has been minimized.

Copy link
@KhalipskiSiarhei

KhalipskiSiarhei Jul 24, 2017

Am I right that this feature is about lazy loaded Angular module into AngularJS app (lazy loading for hybrid app)?

This comment has been minimized.

Copy link
@gkalpak

gkalpak Jul 24, 2017

Author Member

@KhalipskiSiarhei, yes. It is still an experimental API, but docs are comming soon.

This comment has been minimized.

Copy link
@samudrak

samudrak Jul 25, 2017

I tried with these changes with respect to lazy loaded Angular module into AngularJS app

But still issue exists
I am referring to following issue
#17490

This comment has been minimized.

Copy link
@gkalpak

gkalpak Jul 25, 2017

Author Member

@samudrak, yes this is still an issue. This changes allow loading the whole Angular part lazily (e.g. when you navigate to a particular route), but do not yet solve the injectors issue described in #17490.

This comment has been minimized.

Copy link
@samudrak

samudrak Jul 25, 2017

@gkalpak , we are waiting see if issue #17490 will be fixed as suggested by @aaronfrost . We have tested it and works fine . But we wanted it to officially done by angular team before we move forward . Looking for this fix soon so that we can start working on a major Angular 1 to Angular 4 or 5 migration project.

/** @deprecated since v4. This parameter is no longer used */
inputs?: string[];
/** @deprecated since v4. This parameter is no longer used */
Expand All @@ -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<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 componentFactoryResolver: ComponentFactoryResolver =
Expand All @@ -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<Injector>(parentInjector)) {
parentInjector.then(downgradeFn);
} else {
downgradeFn(parentInjector);
}

ranAsync = true;
}
};
};
Expand Down Expand Up @@ -155,3 +175,7 @@ class ParentInjectorPromise {
this.callbacks.length = 0;
}
}

function isThenable<T>(obj: object): obj is Thenable<T> {
return isFunction((obj as any).then);
}
33 changes: 23 additions & 10 deletions packages/upgrade/src/common/downgrade_component_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>|null = null;
private component: any = null;
Expand Down Expand Up @@ -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++) {
Expand Down Expand Up @@ -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 && (<OnChanges>prototype).ngOnChanges) {
// Detect: OnChanges interface
this.inputChanges = {};
this.componentScope.$watch(() => this.inputChangeCount, () => {
this.implementsOnChanges = !!(prototype && (<OnChanges>prototype).ngOnChanges);

this.componentScope.$watch(() => this.inputChangeCount, () => {
// Invoke `ngOnChanges()`
if (this.implementsOnChanges) {
const inputChanges = this.inputChanges;
this.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() {
Expand Down Expand Up @@ -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;
}

Expand Down
7 changes: 6 additions & 1 deletion packages/upgrade/src/common/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
* `ControlValueAccessor` interface needed for AngularJS `ng-model`
Expand Down
8 changes: 7 additions & 1 deletion packages/upgrade/src/dynamic/upgrade_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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([
Expand Down
60 changes: 60 additions & 0 deletions packages/upgrade/src/static/downgrade_module.ts
Original file line number Diff line number Diff line change
@@ -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>(

This comment has been minimized.

Copy link
@IgorMinar

IgorMinar Jul 18, 2017

Contributor

no api docs?

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;
}
28 changes: 10 additions & 18 deletions packages/upgrade/src/static/upgrade_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';


/**
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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 changes: 26 additions & 0 deletions packages/upgrade/src/static/util.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 1 addition & 0 deletions packages/upgrade/static/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Loading

0 comments on commit 30e76fc

Please sign in to comment.