@angular/upgrade
Version:
Angular - the library for easing update from v1 to v2
1,134 lines (1,123 loc) • 88 kB
JavaScript
/**
* @license Angular v11.2.4
* (c) 2010-2021 Google LLC. https://angular.io/
* License: MIT
*/
import { Version, Injector, ChangeDetectorRef, Testability, TestabilityRegistry, ApplicationRef, SimpleChange, NgZone, ComponentFactoryResolver, Directive, Inject, ElementRef, EventEmitter, Compiler, resolveForwardRef, NgModule, isDevMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
/**
* @license
* Copyright Google LLC 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
*/
/**
* @publicApi
*/
const VERSION = new Version('11.2.4');
/**
* @license
* Copyright Google LLC 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
*/
function noNg() {
throw new Error('AngularJS v1.x is not loaded!');
}
const noNgElement = (() => noNg());
noNgElement.cleanData = noNg;
let angular = {
bootstrap: noNg,
module: noNg,
element: noNgElement,
injector: noNg,
version: undefined,
resumeBootstrap: noNg,
getTestability: noNg
};
try {
if (window.hasOwnProperty('angular')) {
angular = window.angular;
}
}
catch (_a) {
// ignore in CJS mode.
}
/**
* @deprecated Use `setAngularJSGlobal` instead.
*
* @publicApi
*/
function setAngularLib(ng) {
setAngularJSGlobal(ng);
}
/**
* @deprecated Use `getAngularJSGlobal` instead.
*
* @publicApi
*/
function getAngularLib() {
return getAngularJSGlobal();
}
/**
* Resets the AngularJS global.
*
* Used when AngularJS is loaded lazily, and not available on `window`.
*
* @publicApi
*/
function setAngularJSGlobal(ng) {
angular = ng;
}
/**
* Returns the current AngularJS global.
*
* @publicApi
*/
function getAngularJSGlobal() {
return angular;
}
const bootstrap = (e, modules, config) => angular.bootstrap(e, modules, config);
// Do not declare as `module` to avoid webpack bug
// (see https://github.com/angular/angular/issues/30050).
const module_ = (prefix, dependencies) => angular.module(prefix, dependencies);
const element = (e => angular.element(e));
element.cleanData = nodes => angular.element.cleanData(nodes);
const injector = (modules, strictDi) => angular.injector(modules, strictDi);
const resumeBootstrap = () => angular.resumeBootstrap();
const getTestability = e => angular.getTestability(e);
/**
* @license
* Copyright Google LLC 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
*/
const $COMPILE = '$compile';
const $CONTROLLER = '$controller';
const $DELEGATE = '$delegate';
const $EXCEPTION_HANDLER = '$exceptionHandler';
const $HTTP_BACKEND = '$httpBackend';
const $INJECTOR = '$injector';
const $INTERVAL = '$interval';
const $PARSE = '$parse';
const $PROVIDE = '$provide';
const $ROOT_ELEMENT = '$rootElement';
const $ROOT_SCOPE = '$rootScope';
const $SCOPE = '$scope';
const $TEMPLATE_CACHE = '$templateCache';
const $TEMPLATE_REQUEST = '$templateRequest';
const $$TESTABILITY = '$$testability';
const COMPILER_KEY = '$$angularCompiler';
const DOWNGRADED_MODULE_COUNT_KEY = '$$angularDowngradedModuleCount';
const GROUP_PROJECTABLE_NODES_KEY = '$$angularGroupProjectableNodes';
const INJECTOR_KEY = '$$angularInjector';
const LAZY_MODULE_REF = '$$angularLazyModuleRef';
const NG_ZONE_KEY = '$$angularNgZone';
const UPGRADE_APP_TYPE_KEY = '$$angularUpgradeAppType';
const REQUIRE_INJECTOR = '?^^' + INJECTOR_KEY;
const REQUIRE_NG_MODEL = '?ngModel';
const UPGRADE_MODULE_NAME = '$$UpgradeModule';
/**
* @license
* Copyright Google LLC 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
*/
/**
* A `PropertyBinding` represents a mapping between a property name
* and an attribute name. It is parsed from a string of the form
* `"prop: attr"`; or simply `"propAndAttr" where the property
* and attribute have the same identifier.
*/
class PropertyBinding {
constructor(prop, attr) {
this.prop = prop;
this.attr = attr;
this.parseBinding();
}
parseBinding() {
this.bracketAttr = `[${this.attr}]`;
this.parenAttr = `(${this.attr})`;
this.bracketParenAttr = `[(${this.attr})]`;
const capitalAttr = this.attr.charAt(0).toUpperCase() + this.attr.substr(1);
this.onAttr = `on${capitalAttr}`;
this.bindAttr = `bind${capitalAttr}`;
this.bindonAttr = `bindon${capitalAttr}`;
}
}
/**
* @license
* Copyright Google LLC 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
*/
const DIRECTIVE_PREFIX_REGEXP = /^(?:x|data)[:\-_]/i;
const DIRECTIVE_SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g;
function onError(e) {
// TODO: (misko): We seem to not have a stack trace here!
if (console.error) {
console.error(e, e.stack);
}
else {
// tslint:disable-next-line:no-console
console.log(e, e.stack);
}
throw e;
}
/**
* Clean the jqLite/jQuery data on the element and all its descendants.
* Equivalent to how jqLite/jQuery invoke `cleanData()` on an Element when removed:
* https://github.com/angular/angular.js/blob/2e72ea13fa98bebf6ed4b5e3c45eaf5f990ed16f/src/jqLite.js#L349-L355
* https://github.com/jquery/jquery/blob/6984d1747623dbc5e87fd6c261a5b6b1628c107c/src/manipulation.js#L182
*
* NOTE:
* `cleanData()` will also invoke the AngularJS `$destroy` DOM event on the element:
* https://github.com/angular/angular.js/blob/2e72ea13fa98bebf6ed4b5e3c45eaf5f990ed16f/src/Angular.js#L1932-L1945
*
* @param node The DOM node whose data needs to be cleaned.
*/
function cleanData(node) {
element.cleanData([node]);
if (isParentNode(node)) {
element.cleanData(node.querySelectorAll('*'));
}
}
function controllerKey(name) {
return '$' + name + 'Controller';
}
/**
* Destroy an AngularJS app given the app `$injector`.
*
* NOTE: Destroying an app is not officially supported by AngularJS, but try to do our best by
* destroying `$rootScope` and clean the jqLite/jQuery data on `$rootElement` and all
* descendants.
*
* @param $injector The `$injector` of the AngularJS app to destroy.
*/
function destroyApp($injector) {
const $rootElement = $injector.get($ROOT_ELEMENT);
const $rootScope = $injector.get($ROOT_SCOPE);
$rootScope.$destroy();
cleanData($rootElement[0]);
}
function directiveNormalize(name) {
return name.replace(DIRECTIVE_PREFIX_REGEXP, '')
.replace(DIRECTIVE_SPECIAL_CHARS_REGEXP, (_, letter) => letter.toUpperCase());
}
function getTypeName(type) {
// Return the name of the type or the first line of its stringified version.
return type.overriddenName || type.name || type.toString().split('\n')[0];
}
function getDowngradedModuleCount($injector) {
return $injector.has(DOWNGRADED_MODULE_COUNT_KEY) ? $injector.get(DOWNGRADED_MODULE_COUNT_KEY) :
0;
}
function getUpgradeAppType($injector) {
return $injector.has(UPGRADE_APP_TYPE_KEY) ? $injector.get(UPGRADE_APP_TYPE_KEY) :
0 /* None */;
}
function isFunction(value) {
return typeof value === 'function';
}
function isParentNode(node) {
return isFunction(node.querySelectorAll);
}
function validateInjectionKey($injector, downgradedModule, injectionKey, attemptedAction) {
const upgradeAppType = getUpgradeAppType($injector);
const downgradedModuleCount = getDowngradedModuleCount($injector);
// Check for common errors.
switch (upgradeAppType) {
case 1 /* Dynamic */:
case 2 /* Static */:
if (downgradedModule) {
throw new Error(`Error while ${attemptedAction}: 'downgradedModule' unexpectedly specified.\n` +
'You should not specify a value for \'downgradedModule\', unless you are downgrading ' +
'more than one Angular module (via \'downgradeModule()\').');
}
break;
case 3 /* Lite */:
if (!downgradedModule && (downgradedModuleCount >= 2)) {
throw new Error(`Error while ${attemptedAction}: 'downgradedModule' not specified.\n` +
'This application contains more than one downgraded Angular module, thus you need to ' +
'always specify \'downgradedModule\' when downgrading components and injectables.');
}
if (!$injector.has(injectionKey)) {
throw new Error(`Error while ${attemptedAction}: Unable to find the specified downgraded module.\n` +
'Did you forget to downgrade an Angular module or include it in the AngularJS ' +
'application?');
}
break;
default:
throw new Error(`Error while ${attemptedAction}: Not a valid '@angular/upgrade' application.\n` +
'Did you forget to downgrade an Angular module or include it in the AngularJS ' +
'application?');
}
}
class Deferred {
constructor() {
this.promise = new Promise((res, rej) => {
this.resolve = res;
this.reject = rej;
});
}
}
/**
* @return Whether the passed-in component implements the subset of the
* `ControlValueAccessor` interface needed for AngularJS `ng-model`
* compatibility.
*/
function supportsNgModel(component) {
return typeof component.writeValue === 'function' &&
typeof component.registerOnChange === 'function';
}
/**
* Glue the AngularJS `NgModelController` (if it exists) to the component
* (if it implements the needed subset of the `ControlValueAccessor` interface).
*/
function hookupNgModel(ngModel, component) {
if (ngModel && supportsNgModel(component)) {
ngModel.$render = () => {
component.writeValue(ngModel.$viewValue);
};
component.registerOnChange(ngModel.$setViewValue.bind(ngModel));
if (typeof component.registerOnTouched === 'function') {
component.registerOnTouched(ngModel.$setTouched.bind(ngModel));
}
}
}
/**
* Test two values for strict equality, accounting for the fact that `NaN !== NaN`.
*/
function strictEquals(val1, val2) {
return val1 === val2 || (val1 !== val1 && val2 !== val2);
}
/**
* @license
* Copyright Google LLC 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
*/
const INITIAL_VALUE = {
__UNINITIALIZED__: true
};
class DowngradeComponentAdapter {
constructor(element, attrs, scope, ngModel, parentInjector, $compile, $parse, componentFactory, wrapCallback) {
this.element = element;
this.attrs = attrs;
this.scope = scope;
this.ngModel = ngModel;
this.parentInjector = parentInjector;
this.$compile = $compile;
this.$parse = $parse;
this.componentFactory = componentFactory;
this.wrapCallback = wrapCallback;
this.implementsOnChanges = false;
this.inputChangeCount = 0;
this.inputChanges = {};
this.componentScope = scope.$new();
}
compileContents() {
const compiledProjectableNodes = [];
const projectableNodes = this.groupProjectableNodes();
const linkFns = projectableNodes.map(nodes => this.$compile(nodes));
this.element.empty();
linkFns.forEach(linkFn => {
linkFn(this.scope, (clone) => {
compiledProjectableNodes.push(clone);
this.element.append(clone);
});
});
return compiledProjectableNodes;
}
createComponent(projectableNodes) {
const providers = [{ provide: $SCOPE, useValue: this.componentScope }];
const childInjector = Injector.create({ providers: providers, parent: this.parentInjector, name: 'DowngradeComponentAdapter' });
this.componentRef =
this.componentFactory.create(childInjector, projectableNodes, this.element[0]);
this.viewChangeDetector = this.componentRef.injector.get(ChangeDetectorRef);
this.changeDetector = this.componentRef.changeDetectorRef;
this.component = this.componentRef.instance;
// testability hook is commonly added during component bootstrap in
// packages/core/src/application_ref.bootstrap()
// in downgraded application, component creation will take place here as well as adding the
// testability hook.
const testability = this.componentRef.injector.get(Testability, null);
if (testability) {
this.componentRef.injector.get(TestabilityRegistry)
.registerApplication(this.componentRef.location.nativeElement, testability);
}
hookupNgModel(this.ngModel, this.component);
}
setupInputs(manuallyAttachView, propagateDigest = true) {
const attrs = this.attrs;
const inputs = this.componentFactory.inputs || [];
for (let i = 0; i < inputs.length; i++) {
const input = new PropertyBinding(inputs[i].propName, inputs[i].templateName);
let expr = null;
if (attrs.hasOwnProperty(input.attr)) {
const observeFn = (prop => {
let prevValue = INITIAL_VALUE;
return (currValue) => {
// Initially, both `$observe()` and `$watch()` will call this function.
if (!strictEquals(prevValue, currValue)) {
if (prevValue === INITIAL_VALUE) {
prevValue = currValue;
}
this.updateInput(prop, prevValue, currValue);
prevValue = currValue;
}
};
})(input.prop);
attrs.$observe(input.attr, observeFn);
// Use `$watch()` (in addition to `$observe()`) in order to initialize the input in time
// for `ngOnChanges()`. This is necessary if we are already in a `$digest`, which means that
// `ngOnChanges()` (which is called by a watcher) will run before the `$observe()` callback.
let unwatch = this.componentScope.$watch(() => {
unwatch();
unwatch = null;
observeFn(attrs[input.attr]);
});
}
else if (attrs.hasOwnProperty(input.bindAttr)) {
expr = attrs[input.bindAttr];
}
else if (attrs.hasOwnProperty(input.bracketAttr)) {
expr = attrs[input.bracketAttr];
}
else if (attrs.hasOwnProperty(input.bindonAttr)) {
expr = attrs[input.bindonAttr];
}
else if (attrs.hasOwnProperty(input.bracketParenAttr)) {
expr = attrs[input.bracketParenAttr];
}
if (expr != null) {
const watchFn = (prop => (currValue, prevValue) => this.updateInput(prop, prevValue, currValue))(input.prop);
this.componentScope.$watch(expr, watchFn);
}
}
// Invoke `ngOnChanges()` and Change Detection (when necessary)
const detectChanges = () => this.changeDetector.detectChanges();
const prototype = this.componentFactory.componentType.prototype;
this.implementsOnChanges = !!(prototype && prototype.ngOnChanges);
this.componentScope.$watch(() => this.inputChangeCount, this.wrapCallback(() => {
// Invoke `ngOnChanges()`
if (this.implementsOnChanges) {
const inputChanges = this.inputChanges;
this.inputChanges = {};
this.component.ngOnChanges(inputChanges);
}
this.viewChangeDetector.markForCheck();
// 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(this.wrapCallback(detectChanges));
}
// If necessary, attach the view so that it will be dirty-checked.
// (Allow time for the initial input values to be set and `ngOnChanges()` to be called.)
if (manuallyAttachView || !propagateDigest) {
let unwatch = this.componentScope.$watch(() => {
unwatch();
unwatch = null;
const appRef = this.parentInjector.get(ApplicationRef);
appRef.attachView(this.componentRef.hostView);
});
}
}
setupOutputs() {
const attrs = this.attrs;
const outputs = this.componentFactory.outputs || [];
for (let j = 0; j < outputs.length; j++) {
const output = new PropertyBinding(outputs[j].propName, outputs[j].templateName);
const bindonAttr = output.bindonAttr.substring(0, output.bindonAttr.length - 6);
const bracketParenAttr = `[(${output.bracketParenAttr.substring(2, output.bracketParenAttr.length - 8)})]`;
// order below is important - first update bindings then evaluate expressions
if (attrs.hasOwnProperty(bindonAttr)) {
this.subscribeToOutput(output, attrs[bindonAttr], true);
}
if (attrs.hasOwnProperty(bracketParenAttr)) {
this.subscribeToOutput(output, attrs[bracketParenAttr], true);
}
if (attrs.hasOwnProperty(output.onAttr)) {
this.subscribeToOutput(output, attrs[output.onAttr]);
}
if (attrs.hasOwnProperty(output.parenAttr)) {
this.subscribeToOutput(output, attrs[output.parenAttr]);
}
}
}
subscribeToOutput(output, expr, isAssignment = false) {
const getter = this.$parse(expr);
const setter = getter.assign;
if (isAssignment && !setter) {
throw new Error(`Expression '${expr}' is not assignable!`);
}
const emitter = this.component[output.prop];
if (emitter) {
emitter.subscribe({
next: isAssignment ? (v) => setter(this.scope, v) :
(v) => getter(this.scope, { '$event': v })
});
}
else {
throw new Error(`Missing emitter '${output.prop}' on component '${getTypeName(this.componentFactory.componentType)}'!`);
}
}
registerCleanup() {
const testabilityRegistry = this.componentRef.injector.get(TestabilityRegistry);
const destroyComponentRef = this.wrapCallback(() => this.componentRef.destroy());
let destroyed = false;
this.element.on('$destroy', () => {
// The `$destroy` event may have been triggered by the `cleanData()` call in the
// `componentScope` `$destroy` handler below. In that case, we don't want to call
// `componentScope.$destroy()` again.
if (!destroyed)
this.componentScope.$destroy();
});
this.componentScope.$on('$destroy', () => {
if (!destroyed) {
destroyed = true;
testabilityRegistry.unregisterApplication(this.componentRef.location.nativeElement);
// The `componentScope` might be getting destroyed, because an ancestor element is being
// removed/destroyed. If that is the case, jqLite/jQuery would normally invoke `cleanData()`
// on the removed element and all descendants.
// https://github.com/angular/angular.js/blob/2e72ea13fa98bebf6ed4b5e3c45eaf5f990ed16f/src/jqLite.js#L349-L355
// https://github.com/jquery/jquery/blob/6984d1747623dbc5e87fd6c261a5b6b1628c107c/src/manipulation.js#L182
//
// Here, however, `destroyComponentRef()` may under some circumstances remove the element
// from the DOM and therefore it will no longer be a descendant of the removed element when
// `cleanData()` is called. This would result in a memory leak, because the element's data
// and event handlers (and all objects directly or indirectly referenced by them) would be
// retained.
//
// To ensure the element is always properly cleaned up, we manually call `cleanData()` on
// this element and its descendants before destroying the `ComponentRef`.
cleanData(this.element[0]);
destroyComponentRef();
}
});
}
getInjector() {
return this.componentRef.injector;
}
updateInput(prop, prevValue, currValue) {
if (this.implementsOnChanges) {
this.inputChanges[prop] = new SimpleChange(prevValue, currValue, prevValue === currValue);
}
this.inputChangeCount++;
this.component[prop] = currValue;
}
groupProjectableNodes() {
let ngContentSelectors = this.componentFactory.ngContentSelectors;
return groupNodesBySelector(ngContentSelectors, this.element.contents());
}
}
/**
* Group a set of DOM nodes into `ngContent` groups, based on the given content selectors.
*/
function groupNodesBySelector(ngContentSelectors, nodes) {
const projectableNodes = [];
for (let i = 0, ii = ngContentSelectors.length; i < ii; ++i) {
projectableNodes[i] = [];
}
for (let j = 0, jj = nodes.length; j < jj; ++j) {
const node = nodes[j];
const ngContentIndex = findMatchingNgContentIndex(node, ngContentSelectors);
if (ngContentIndex != null) {
projectableNodes[ngContentIndex].push(node);
}
}
return projectableNodes;
}
function findMatchingNgContentIndex(element, ngContentSelectors) {
const ngContentIndices = [];
let wildcardNgContentIndex = -1;
for (let i = 0; i < ngContentSelectors.length; i++) {
const selector = ngContentSelectors[i];
if (selector === '*') {
wildcardNgContentIndex = i;
}
else {
if (matchesSelector(element, selector)) {
ngContentIndices.push(i);
}
}
}
ngContentIndices.sort();
if (wildcardNgContentIndex !== -1) {
ngContentIndices.push(wildcardNgContentIndex);
}
return ngContentIndices.length ? ngContentIndices[0] : null;
}
let _matches;
function matchesSelector(el, selector) {
if (!_matches) {
const elProto = Element.prototype;
_matches = elProto.matches || elProto.matchesSelector || elProto.mozMatchesSelector ||
elProto.msMatchesSelector || elProto.oMatchesSelector || elProto.webkitMatchesSelector;
}
return el.nodeType === Node.ELEMENT_NODE ? _matches.call(el, selector) : false;
}
/**
* @license
* Copyright Google LLC 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
*/
function isThenable(obj) {
return !!obj && isFunction(obj.then);
}
/**
* Synchronous, promise-like object.
*/
class SyncPromise {
constructor() {
this.resolved = false;
this.callbacks = [];
}
static all(valuesOrPromises) {
const aggrPromise = new SyncPromise();
let resolvedCount = 0;
const results = [];
const resolve = (idx, value) => {
results[idx] = value;
if (++resolvedCount === valuesOrPromises.length)
aggrPromise.resolve(results);
};
valuesOrPromises.forEach((p, idx) => {
if (isThenable(p)) {
p.then(v => resolve(idx, v));
}
else {
resolve(idx, p);
}
});
return aggrPromise;
}
resolve(value) {
// Do nothing, if already resolved.
if (this.resolved)
return;
this.value = value;
this.resolved = true;
// Run the queued callbacks.
this.callbacks.forEach(callback => callback(value));
this.callbacks.length = 0;
}
then(callback) {
if (this.resolved) {
callback(this.value);
}
else {
this.callbacks.push(callback);
}
}
}
/**
* @license
* Copyright Google LLC 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
*/
/**
* @description
*
* A helper function that allows an Angular component to be used from AngularJS.
*
* *Part of the [upgrade/static](api?query=upgrade%2Fstatic)
* library for hybrid upgrade apps that support AOT compilation*
*
* This helper function returns a factory function to be used for registering
* an AngularJS wrapper directive for "downgrading" an Angular component.
*
* @usageNotes
* ### Examples
*
* Let's assume that you have an Angular component called `ng2Heroes` that needs
* to be made available in AngularJS templates.
*
* {@example upgrade/static/ts/full/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/full/module.ts region="ng2-heroes-wrapper"}
*
* For more details and examples on downgrading Angular components to AngularJS components please
* visit the [Upgrade guide](guide/upgrade#using-angular-components-from-angularjs-code).
*
* @param info contains information about the Component that is being downgraded:
*
* - `component: Type<any>`: The type of the Component that will be downgraded
* - `downgradedModule?: string`: The name of the downgraded module (if any) that the component
* "belongs to", as returned by a call to `downgradeModule()`. It is the module, whose
* corresponding Angular module will be bootstrapped, when the component needs to be instantiated.
* <br />
* (This option is only necessary when using `downgradeModule()` to downgrade more than one
* Angular module.)
* - `propagateDigest?: boolean`: Whether to perform {@link ChangeDetectorRef#detectChanges
* change detection} on the component on every
* [$digest](https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$digest). If set to `false`,
* change detection will still be performed when any of the component's inputs changes.
* (Default: true)
*
* @returns a factory function that can be used to register the component in an
* AngularJS module.
*
* @publicApi
*/
function downgradeComponent(info) {
const directiveFactory = function ($compile, $injector, $parse) {
// When using `downgradeModule()`, we need to handle certain things specially. For example:
// - We always need to attach the component view to the `ApplicationRef` for it to be
// dirty-checked.
// - We need to ensure callbacks to Angular APIs (e.g. change detection) are run inside the
// Angular zone.
// NOTE: This is not needed, when using `UpgradeModule`, because `$digest()` will be run
// inside the Angular zone (except if explicitly escaped, in which case we shouldn't
// force it back in).
const isNgUpgradeLite = getUpgradeAppType($injector) === 3 /* Lite */;
const wrapCallback = !isNgUpgradeLite ? cb => cb : cb => () => NgZone.isInAngularZone() ? cb() : ngZone.run(cb);
let ngZone;
// When downgrading multiple modules, special handling is needed wrt injectors.
const hasMultipleDowngradedModules = isNgUpgradeLite && (getDowngradedModuleCount($injector) > 1);
return {
restrict: 'E',
terminal: true,
require: [REQUIRE_INJECTOR, REQUIRE_NG_MODEL],
link: (scope, element, attrs, required) => {
// 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 = required[1];
const parentInjector = required[0];
let moduleInjector = undefined;
let ranAsync = false;
if (!parentInjector || hasMultipleDowngradedModules) {
const downgradedModule = info.downgradedModule || '';
const lazyModuleRefKey = `${LAZY_MODULE_REF}${downgradedModule}`;
const attemptedAction = `instantiating component '${getTypeName(info.component)}'`;
validateInjectionKey($injector, downgradedModule, lazyModuleRefKey, attemptedAction);
const lazyModuleRef = $injector.get(lazyModuleRefKey);
moduleInjector = lazyModuleRef.injector || lazyModuleRef.promise;
}
// Notes:
//
// There are two injectors: `finalModuleInjector` and `finalParentInjector` (they might be
// the same instance, but that is irrelevant):
// - `finalModuleInjector` is used to retrieve `ComponentFactoryResolver`, thus it must be
// on the same tree as the `NgModule` that declares this downgraded component.
// - `finalParentInjector` is used for all other injection purposes.
// (Note that Angular knows to only traverse the component-tree part of that injector,
// when looking for an injectable and then switch to the module injector.)
//
// There are basically three cases:
// - If there is no parent component (thus no `parentInjector`), we bootstrap the downgraded
// `NgModule` and use its injector as both `finalModuleInjector` and
// `finalParentInjector`.
// - If there is a parent component (and thus a `parentInjector`) and we are sure that it
// belongs to the same `NgModule` as this downgraded component (e.g. because there is only
// one downgraded module, we use that `parentInjector` as both `finalModuleInjector` and
// `finalParentInjector`.
// - If there is a parent component, but it may belong to a different `NgModule`, then we
// use the `parentInjector` as `finalParentInjector` and this downgraded component's
// declaring `NgModule`'s injector as `finalModuleInjector`.
// Note 1: If the `NgModule` is already bootstrapped, we just get its injector (we don't
// bootstrap again).
// Note 2: It is possible that (while there are multiple downgraded modules) this
// downgraded component and its parent component both belong to the same NgModule.
// In that case, we could have used the `parentInjector` as both
// `finalModuleInjector` and `finalParentInjector`, but (for simplicity) we are
// treating this case as if they belong to different `NgModule`s. That doesn't
// really affect anything, since `parentInjector` has `moduleInjector` as ancestor
// and trying to resolve `ComponentFactoryResolver` from either one will return
// the same instance.
// If there is a parent component, use its injector as parent injector.
// If this is a "top-level" Angular component, use the module injector.
const finalParentInjector = parentInjector || moduleInjector;
// If this is a "top-level" Angular component or the parent component may belong to a
// different `NgModule`, use the module injector for module-specific dependencies.
// If there is a parent component that belongs to the same `NgModule`, use its injector.
const finalModuleInjector = moduleInjector || parentInjector;
const doDowngrade = (injector, moduleInjector) => {
// Retrieve `ComponentFactoryResolver` from the injector tied to the `NgModule` this
// component belongs to.
const componentFactoryResolver = moduleInjector.get(ComponentFactoryResolver);
const componentFactory = componentFactoryResolver.resolveComponentFactory(info.component);
if (!componentFactory) {
throw new Error(`Expecting ComponentFactory for: ${getTypeName(info.component)}`);
}
const injectorPromise = new ParentInjectorPromise(element);
const facade = new DowngradeComponentAdapter(element, attrs, scope, ngModel, injector, $compile, $parse, componentFactory, wrapCallback);
const projectableNodes = facade.compileContents();
facade.createComponent(projectableNodes);
facade.setupInputs(isNgUpgradeLite, 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(() => { });
}
};
const downgradeFn = !isNgUpgradeLite ? doDowngrade : (pInjector, mInjector) => {
if (!ngZone) {
ngZone = pInjector.get(NgZone);
}
wrapCallback(() => doDowngrade(pInjector, mInjector))();
};
// NOTE:
// Not using `ParentInjectorPromise.all()` (which is inherited from `SyncPromise`), because
// Closure Compiler (or some related tool) complains:
// `TypeError: ...$src$downgrade_component_ParentInjectorPromise.all is not a function`
SyncPromise.all([finalParentInjector, finalModuleInjector])
.then(([pInjector, mInjector]) => downgradeFn(pInjector, mInjector));
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 AngularJS's `$compile`.
*/
class ParentInjectorPromise extends SyncPromise {
constructor(element) {
super();
this.element = element;
this.injectorKey = controllerKey(INJECTOR_KEY);
// Store the promise on the element.
element.data(this.injectorKey, this);
}
resolve(injector) {
// Store the real injector on the element.
this.element.data(this.injectorKey, injector);
// Release the element to prevent memory leaks.
this.element = null;
// Resolve the promise.
super.resolve(injector);
}
}
/**
* @license
* Copyright Google LLC 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
*/
/**
* @description
*
* A helper function to allow an Angular service to be accessible from AngularJS.
*
* *Part of the [upgrade/static](api?query=upgrade%2Fstatic)
* library for hybrid upgrade apps that support AOT compilation*
*
* This helper function returns a factory function that provides access to the Angular
* service identified by the `token` parameter.
*
* @usageNotes
* ### Examples
*
* First ensure that the service to be downgraded is provided in an `NgModule`
* that will be part of the upgrade application. For example, let's assume we have
* defined `HeroesService`
*
* {@example upgrade/static/ts/full/module.ts region="ng2-heroes-service"}
*
* and that we have included this in our upgrade app `NgModule`
*
* {@example upgrade/static/ts/full/module.ts region="ng2-module"}
*
* Now we can register the `downgradeInjectable` factory function for the service
* on an AngularJS module.
*
* {@example upgrade/static/ts/full/module.ts region="downgrade-ng2-heroes-service"}
*
* Inside an AngularJS component's controller we can get hold of the
* downgraded service via the name we gave when downgrading.
*
* {@example upgrade/static/ts/full/module.ts region="example-app"}
*
* <div class="alert is-important">
*
* When using `downgradeModule()`, downgraded injectables will not be available until the Angular
* module that provides them is instantiated. In order to be safe, you need to ensure that the
* downgraded injectables are not used anywhere _outside_ the part of the app where it is
* guaranteed that their module has been instantiated.
*
* For example, it is _OK_ to use a downgraded service in an upgraded component that is only used
* from a downgraded Angular component provided by the same Angular module as the injectable, but
* it is _not OK_ to use it in an AngularJS component that may be used independently of Angular or
* use it in a downgraded Angular component from a different module.
*
* </div>
*
* @param token an `InjectionToken` that identifies a service provided from Angular.
* @param downgradedModule the name of the downgraded module (if any) that the injectable
* "belongs to", as returned by a call to `downgradeModule()`. It is the module, whose injector will
* be used for instantiating the injectable.<br />
* (This option is only necessary when using `downgradeModule()` to downgrade more than one Angular
* module.)
*
* @returns a [factory function](https://docs.angularjs.org/guide/di) that can be
* used to register the service on an AngularJS module.
*
* @publicApi
*/
function downgradeInjectable(token, downgradedModule = '') {
const factory = function ($injector) {
const injectorKey = `${INJECTOR_KEY}${downgradedModule}`;
const injectableName = isFunction(token) ? getTypeName(token) : String(token);
const attemptedAction = `instantiating injectable '${injectableName}'`;
validateInjectionKey($injector, downgradedModule, injectorKey, attemptedAction);
try {
const injector = $injector.get(injectorKey);
return injector.get(token);
}
catch (err) {
throw new Error(`Error while ${attemptedAction}: ${err.message || err}`);
}
};
factory['$inject'] = [$INJECTOR];
return factory;
}
/**
* @license
* Copyright Google LLC 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
*/
// Constants
const REQUIRE_PREFIX_RE = /^(\^\^?)?(\?)?(\^\^?)?/;
// Classes
class UpgradeHelper {
constructor(injector, name, elementRef, directive) {
this.name = name;
this.$injector = injector.get($INJECTOR);
this.$compile = this.$injector.get($COMPILE);
this.$controller = this.$injector.get($CONTROLLER);
this.element = elementRef.nativeElement;
this.$element = element(this.element);
this.directive = directive || UpgradeHelper.getDirective(this.$injector, name);
}
static getDirective($injector, name) {
const directives = $injector.get(name + 'Directive');
if (directives.length > 1) {
throw new Error(`Only support single directive definition for: ${name}`);
}
const directive = directives[0];
// AngularJS will transform `link: xyz` to `compile: () => xyz`. So we can only tell there was a
// user-defined `compile` if there is no `link`. In other cases, we will just ignore `compile`.
if (directive.compile && !directive.link)
notSupported(name, 'compile');
if (directive.replace)
notSupported(name, 'replace');
if (directive.terminal)
notSupported(name, 'terminal');
return directive;
}
static getTemplate($injector, directive, fetchRemoteTemplate = false, $element) {
if (directive.template !== undefined) {
return getOrCall(directive.template, $element);
}
else if (directive.templateUrl) {
const $templateCache = $injector.get($TEMPLATE_CACHE);
const url = getOrCall(directive.templateUrl, $element);
const template = $templateCache.get(url);
if (template !== undefined) {
return template;
}
else if (!fetchRemoteTemplate) {
throw new Error('loading directive templates asynchronously is not supported');
}
return new Promise((resolve, reject) => {
const $httpBackend = $injector.get($HTTP_BACKEND);
$httpBackend('GET', url, null, (status, response) => {
if (status === 200) {
resolve($templateCache.put(url, response));
}
else {
reject(`GET component template from '${url}' returned '${status}: ${response}'`);
}
});
});
}
else {
throw new Error(`Directive '${directive.name}' is not a component, it is missing template.`);
}
}
buildController(controllerType, $scope) {
// TODO: Document that we do not pre-assign bindings on the controller instance.
// Quoted properties below so that this code can be optimized with Closure Compiler.
const locals = { '$scope': $scope, '$element': this.$element };
const controller = this.$controller(controllerType, locals, null, this.directive.controllerAs);
this.$element.data(controllerKey(this.directive.name), controller);
return controller;
}
compileTemplate(template) {
if (template === undefined) {
template =
UpgradeHelper.getTemplate(this.$injector, this.directive, false, this.$element);
}
return this.compileHtml(template);
}
onDestroy($scope, controllerInstance) {
if (controllerInstance && isFunction(controllerInstance.$onDestroy)) {
controllerInstance.$onDestroy();
}
$scope.$destroy();
cleanData(this.element);
}
prepareTransclusion() {
const transclude = this.directive.transclude;
const contentChildNodes = this.extractChildNodes();
const attachChildrenFn = (scope, cloneAttachFn) => {
// Since AngularJS v1.5.8, `cloneAttachFn` will try to destroy the transclusion scope if
// `$template` is empty. Since the transcluded content comes from Angular, not AngularJS,
// there will be no transclusion scope here.
// Provide a dummy `scope.$destroy()` method to prevent `cloneAttachFn` from throwing.
scope = scope || { $destroy: () => undefined };
return cloneAttachFn($template, scope);
};
let $template = contentChildNodes;
if (transclude) {
const slots = Object.create(null);
if (typeof transclude === 'object') {
$template = [];
const slotMap = Object.create(null);
const filledSlots = Object.create(null);
// Parse the element selectors.
Object.keys(transclude).forEach(slotName => {
let selector = transclude[slotName];
const optional = selector.charAt(0) === '?';
selector = optional ? selector.substring(1) : selector;
slotMap[selector] = slotName;
slots[slotName] = null; // `null`: Defined but not yet filled.
filledSlots[slotName] = optional; // Consider optional slots as filled.
});
// Add the matching elements into their slot.
contentChildNodes.forEach(node => {
const slotName = slotMap[directiveNormalize(node.nodeName.toLowerCase())];
if (slotName) {
filledSlots[slotName] = true;
slots[slotName] = slots[slotName] || [];
slots[slotName].push(node);
}
else {
$template.push(node);
}
});
// Check for required slots that were not filled.
Object.keys(filledSlots).forEach(slotName => {
if (!filledSlots[slotName]) {
throw new Error(`Required transclusion slot '${slotName}' on directive: ${this.name}`);
}
});
Object.keys(slots).filter(slotName => slots[slotName]).forEach(slotName => {
const nodes = slots[slotName];
slots[slotName] = (scope, cloneAttach) => {
return cloneAttach(nodes, scope);
};
});
}
// Attach `$$slots` to default slot transclude fn.
attachChildrenFn.$$slots = slots;
// AngularJS v1.6+ ignores empty or whitespace-only transcluded text nodes. But Angular
// removes all text content after the first interpolation and updates it later, after
// evaluating the expressions. This would result in AngularJS failing to recognize text
// nodes that start with an interpolation as transcluded content and use the fallback
// content instead.
// To avoid this issue, we add a
// [zero-width non-joiner character](https://en.wikipedia.org/wiki/Zero-width_non-joiner)
// to empty text nodes (which can only be a result of Angular removing their initial content).
// NOTE: Transcluded text content that starts with whitespace followed by an interpolation
// will still fail to be detected by AngularJS v1.6+
$template.forEach(node => {
if (node.nodeType === Node.TEXT_NODE && !node.nodeValue) {
node.nodeValue = '\u200C';
}
});
}
return attachChildrenFn;
}
resolveAndBindRequiredControllers(controllerInstance) {
const directiveRequire = this.getDirectiveRequire();
const requiredControllers = this.resolveRequire(directiveRequire);
if (controllerInstance && this.directive.bindToController && isMap(directiveRequire)) {
const requiredControllersMap = requiredControllers;
Object.keys(requiredControllersMap).forEach(key => {
controllerInstance[key] = requiredControllersMap[key];
});
}
return requiredControllers;
}
compileHtml(html) {
this.element.innerHTML = html;
return this.$compile(this.element.childNodes);
}
extractChildNodes() {
const childNodes = [];
let childNode;
while (childNode = this.element.firstChild) {
this.element.removeChild(childNode);
childNodes.push(childNode);
}
return childNodes;
}
getDirectiveRequire() {
const require = this.directive.require || (this.directive.controller && this.directive.name);
if (isMap(require)) {
Object.keys(require).forEach(key => {
const value = require[key];
const match = value.match(REQUIRE_PREFIX_RE);
const name = value.substring(match[0].length);
if (!name) {
require[key] = match[0] + key;
}
});
}
return require;
}
resolveRequire(require, controllerInstance) {
if (!require) {
return null;
}
else if (Array.isArray(require)) {
return require.map(req => this.resolveRequire(req));
}
else if (typeof require === 'object') {
const value = {};
Object.keys(require).forEach(key => value[key] = this.resolveRequire(require[key]));
return value;
}
else if (typeof require === 'string') {
const match = require.match(REQUIRE_PREFIX_RE);
const inheritType = match[1] || match[3];
const name = require.substring(match[0].length);
const isOptional = !!match[2];
const searchParents = !!inheritType;
const startOnParent = inheritType === '^^';
const ctrlKey = controllerKey(name);
const elem = startOnParent ? this.$element.parent() : this.$element;
const value = searchParents ? elem.inheritedData(ctrlKey) : elem.data(ctrlKey);
if (!value && !isOptional) {
throw