angular/packages/upgrade/src/common/downgrade_component.ts

201 lines
7.1 KiB
TypeScript

/**
* @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 {ComponentFactory, ComponentFactoryResolver, Injector, NgZone, Type} from '@angular/core';
import * as angular from './angular1';
import {$COMPILE, $INJECTOR, $PARSE, INJECTOR_KEY, LAZY_MODULE_REF, REQUIRE_INJECTOR, REQUIRE_NG_MODEL} from './constants';
import {DowngradeComponentAdapter} from './downgrade_component_adapter';
import {LazyModuleRef, controllerKey, getComponentName, isFunction} from './util';
interface Thenable<T> {
then(callback: (value: T) => any): any;
}
let downgradeCount = 0;
/**
* @whatItDoes
*
* *Part of the [upgrade/static](api?query=upgrade%2Fstatic)
* library for hybrid upgrade apps that support AoT compilation*
*
* Allows an Angular component to be used from AngularJS.
*
* @howToUse
*
* Let's assume that you have an Angular component called `ng2Heroes` that needs
* to be made available in AngularJS templates.
*
* {@example upgrade/static/ts/module.ts region="ng2-heroes"}
*
* We must create an AngularJS [directive](https://docs.angularjs.org/guide/directive)
* that will make this Angular component available inside AngularJS templates.
* The `downgradeComponent()` function returns a factory function that we
* can use to define the AngularJS directive that wraps the "downgraded" component.
*
* {@example upgrade/static/ts/module.ts region="ng2-heroes-wrapper"}
*
* @description
*
* A helper function that returns a factory function to be used for registering an
* AngularJS wrapper directive for "downgrading" an Angular component.
*
* The parameter contains information about the Component that is being downgraded:
*
* * `component: Type<any>`: The type of the Component that will be downgraded
*
* @experimental
*/
export function downgradeComponent(info: {
component: Type<any>;
/** @experimental */
propagateDigest?: boolean;
/** @deprecated since v4. This parameter is no longer used */
inputs?: string[];
/** @deprecated since v4. This parameter is no longer used */
outputs?: string[];
/** @deprecated since v4. This parameter is no longer used */
selectors?: string[];
}): any /* angular.IInjectable */ {
const idPrefix = `NG2_UPGRADE_${downgradeCount++}_`;
let idCount = 0;
const directiveFactory:
angular.IAnnotatedFunction = function(
$compile: angular.ICompileService,
$injector: angular.IInjectorService,
$parse: angular.IParseService): angular.IDirective {
// When using `UpgradeModule`, we don't need to ensure callbacks to Angular APIs (e.g. change
// detection) are run inside the Angular zone, because `$digest()` will be run inside the zone
// (except if explicitly escaped, in which case we shouldn't force it back in).
// When using `downgradeModule()` though, we need to ensure such callbacks are run inside the
// Angular zone.
let needsNgZone = false;
let wrapCallback = <T>(cb: () => T) => cb;
let ngZone: NgZone;
return {
restrict: 'E',
terminal: true,
require: [REQUIRE_INJECTOR, REQUIRE_NG_MODEL],
link: (scope: angular.IScope, element: angular.IAugmentedJQuery, attrs: angular.IAttributes,
required: any[]) => {
// We might have to compile the contents asynchronously, because this might have been
// triggered by `UpgradeNg1ComponentAdapterBuilder`, before the Angular templates have
// been compiled.
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;
needsNgZone = lazyModuleRef.needsNgZone;
parentInjector = lazyModuleRef.injector || lazyModuleRef.promise as Promise<Injector>;
}
const doDowngrade = (injector: Injector) => {
const componentFactoryResolver: ComponentFactoryResolver =
injector.get(ComponentFactoryResolver);
const componentFactory: ComponentFactory<any> =
componentFactoryResolver.resolveComponentFactory(info.component) !;
if (!componentFactory) {
throw new Error('Expecting ComponentFactory for: ' + getComponentName(info.component));
}
const id = idPrefix + (idCount++);
const injectorPromise = new ParentInjectorPromise(element);
const facade = new DowngradeComponentAdapter(
id, element, attrs, scope, ngModel, injector, $injector, $compile, $parse,
componentFactory, wrapCallback);
const projectableNodes = facade.compileContents();
facade.createComponent(projectableNodes);
facade.setupInputs(needsNgZone, info.propagateDigest);
facade.setupOutputs();
facade.registerCleanup(needsNgZone);
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(() => {});
}
};
const downgradeFn = !needsNgZone ? doDowngrade : (injector: Injector) => {
if (!ngZone) {
ngZone = injector.get(NgZone);
wrapCallback = <T>(cb: () => T) => () =>
NgZone.isInAngularZone() ? cb() : ngZone.run(cb);
}
wrapCallback(() => doDowngrade(injector))();
};
if (isThenable<Injector>(parentInjector)) {
parentInjector.then(downgradeFn);
} else {
downgradeFn(parentInjector);
}
ranAsync = true;
}
};
};
// bracket-notation because of closure - see #14441
directiveFactory['$inject'] = [$COMPILE, $INJECTOR, $PARSE];
return directiveFactory;
}
/**
* Synchronous promise-like object to wrap parent injectors,
* to preserve the synchronous nature of Angular 1's $compile.
*/
class ParentInjectorPromise {
private injector: Injector;
private injectorKey: string = controllerKey(INJECTOR_KEY);
private callbacks: ((injector: Injector) => any)[] = [];
constructor(private element: angular.IAugmentedJQuery) {
// Store the promise on the element.
element.data !(this.injectorKey, this);
}
then(callback: (injector: Injector) => any) {
if (this.injector) {
callback(this.injector);
} else {
this.callbacks.push(callback);
}
}
resolve(injector: Injector) {
this.injector = injector;
// Store the real injector on the element.
this.element.data !(this.injectorKey, injector);
// Release the element to prevent memory leaks.
this.element = null !;
// Run the queued callbacks.
this.callbacks.forEach(callback => callback(injector));
this.callbacks.length = 0;
}
}
function isThenable<T>(obj: object): obj is Thenable<T> {
return isFunction((obj as any).then);
}