@angular/upgrade
Version:
Angular - the library for easing update from v1 to v2
1,080 lines (1,069 loc) • 51 kB
JavaScript
/**
* @license Angular v20.1.6
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
import { Version, ɵNG_MOD_DEF as _NG_MOD_DEF, Injector, ChangeDetectorRef, Testability, TestabilityRegistry, ApplicationRef, SimpleChange, ɵSIGNAL as _SIGNAL, NgZone, ComponentFactoryResolver } from '@angular/core';
import { element, $ROOT_ELEMENT, $ROOT_SCOPE, DOWNGRADED_MODULE_COUNT_KEY, UPGRADE_APP_TYPE_KEY, $SCOPE, $COMPILE, $INJECTOR, $PARSE, REQUIRE_INJECTOR, REQUIRE_NG_MODEL, LAZY_MODULE_REF, INJECTOR_KEY, $CONTROLLER, $TEMPLATE_CACHE, $HTTP_BACKEND } from './constants.mjs';
/**
* @module
* @description
* Entry point for all public APIs of the upgrade package.
*/
/**
* @publicApi
*/
const VERSION = new Version('20.1.6');
/**
* 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 {
prop;
attr;
bracketAttr;
bracketParenAttr;
parenAttr;
onAttr;
bindAttr;
bindonAttr;
constructor(prop, attr) {
this.prop = prop;
this.attr = attr;
this.bracketAttr = `[${this.attr}]`;
this.parenAttr = `(${this.attr})`;
this.bracketParenAttr = `[(${this.attr})]`;
const capitalAttr = this.attr.charAt(0).toUpperCase() + this.attr.slice(1);
this.onAttr = `on${capitalAttr}`;
this.bindAttr = `bind${capitalAttr}`;
this.bindonAttr = `bindon${capitalAttr}`;
}
}
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!
console.error(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 /* UpgradeAppType.None */;
}
function isFunction(value) {
return typeof value === 'function';
}
function isNgModuleType(value) {
// NgModule class should have the `ɵmod` static property attached by AOT or JIT compiler.
return isFunction(value) && !!value[_NG_MOD_DEF];
}
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 /* UpgradeAppType.Dynamic */:
case 2 /* UpgradeAppType.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 /* UpgradeAppType.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 {
promise;
resolve;
reject;
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);
}
var util = /*#__PURE__*/Object.freeze({
__proto__: null,
Deferred: Deferred,
cleanData: cleanData,
controllerKey: controllerKey,
destroyApp: destroyApp,
directiveNormalize: directiveNormalize,
getDowngradedModuleCount: getDowngradedModuleCount,
getTypeName: getTypeName,
getUpgradeAppType: getUpgradeAppType,
hookupNgModel: hookupNgModel,
isFunction: isFunction,
isNgModuleType: isNgModuleType,
onError: onError,
strictEquals: strictEquals,
validateInjectionKey: validateInjectionKey
});
const INITIAL_VALUE = {
__UNINITIALIZED__: true,
};
class DowngradeComponentAdapter {
element;
attrs;
scope;
ngModel;
parentInjector;
$compile;
$parse;
componentFactory;
wrapCallback;
unsafelyOverwriteSignalInputs;
implementsOnChanges = false;
inputChangeCount = 0;
inputChanges = {};
componentScope;
constructor(element, attrs, scope, ngModel, parentInjector, $compile, $parse, componentFactory, wrapCallback, unsafelyOverwriteSignalInputs) {
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.unsafelyOverwriteSignalInputs = unsafelyOverwriteSignalInputs;
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;
}
createComponentAndSetup(projectableNodes, manuallyAttachView = false, propagateDigest = true) {
const component = this.createComponent(projectableNodes);
this.setupInputs(manuallyAttachView, propagateDigest, component);
this.setupOutputs(component.componentRef);
this.registerCleanup(component.componentRef);
return component.componentRef;
}
createComponent(projectableNodes) {
const providers = [{ provide: $SCOPE, useValue: this.componentScope }];
const childInjector = Injector.create({
providers: providers,
parent: this.parentInjector,
name: 'DowngradeComponentAdapter',
});
const componentRef = this.componentFactory.create(childInjector, projectableNodes, this.element[0]);
const viewChangeDetector = componentRef.injector.get(ChangeDetectorRef);
const changeDetector = componentRef.changeDetectorRef;
// 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 = componentRef.injector.get(Testability, null);
if (testability) {
componentRef.injector
.get(TestabilityRegistry)
.registerApplication(componentRef.location.nativeElement, testability);
}
hookupNgModel(this.ngModel, componentRef.instance);
return { viewChangeDetector, componentRef, changeDetector };
}
setupInputs(manuallyAttachView, propagateDigest = true, { componentRef, changeDetector, viewChangeDetector }) {
const attrs = this.attrs;
const inputs = this.componentFactory.inputs || [];
for (const input of inputs) {
const inputBinding = new PropertyBinding(input.propName, input.templateName);
let expr = null;
if (attrs.hasOwnProperty(inputBinding.attr)) {
const observeFn = ((prop, isSignal) => {
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(componentRef, prop, prevValue, currValue, isSignal);
prevValue = currValue;
}
};
})(inputBinding.prop, input.isSignal);
attrs.$observe(inputBinding.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[inputBinding.attr]);
});
}
else if (attrs.hasOwnProperty(inputBinding.bindAttr)) {
expr = attrs[inputBinding.bindAttr];
}
else if (attrs.hasOwnProperty(inputBinding.bracketAttr)) {
expr = attrs[inputBinding.bracketAttr];
}
else if (attrs.hasOwnProperty(inputBinding.bindonAttr)) {
expr = attrs[inputBinding.bindonAttr];
}
else if (attrs.hasOwnProperty(inputBinding.bracketParenAttr)) {
expr = attrs[inputBinding.bracketParenAttr];
}
if (expr != null) {
const watchFn = ((prop, isSignal) => (currValue, prevValue) => this.updateInput(componentRef, prop, prevValue, currValue, isSignal))(inputBinding.prop, input.isSignal);
this.componentScope.$watch(expr, watchFn);
}
}
// Invoke `ngOnChanges()` and Change Detection (when necessary)
const detectChanges = () => 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 = {};
componentRef.instance.ngOnChanges(inputChanges);
}
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(componentRef.hostView);
});
}
}
setupOutputs(componentRef) {
const attrs = this.attrs;
const outputs = this.componentFactory.outputs || [];
for (const output of outputs) {
const outputBindings = new PropertyBinding(output.propName, output.templateName);
const bindonAttr = outputBindings.bindonAttr.substring(0, outputBindings.bindonAttr.length - 6);
const bracketParenAttr = `[(${outputBindings.bracketParenAttr.substring(2, outputBindings.bracketParenAttr.length - 8)})]`;
// order below is important - first update bindings then evaluate expressions
if (attrs.hasOwnProperty(bindonAttr)) {
this.subscribeToOutput(componentRef, outputBindings, attrs[bindonAttr], true);
}
if (attrs.hasOwnProperty(bracketParenAttr)) {
this.subscribeToOutput(componentRef, outputBindings, attrs[bracketParenAttr], true);
}
if (attrs.hasOwnProperty(outputBindings.onAttr)) {
this.subscribeToOutput(componentRef, outputBindings, attrs[outputBindings.onAttr]);
}
if (attrs.hasOwnProperty(outputBindings.parenAttr)) {
this.subscribeToOutput(componentRef, outputBindings, attrs[outputBindings.parenAttr]);
}
}
}
subscribeToOutput(componentRef, 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 = componentRef.instance[output.prop];
if (emitter) {
const subscription = emitter.subscribe(isAssignment
? (v) => setter(this.scope, v)
: (v) => getter(this.scope, { '$event': v }));
componentRef.onDestroy(() => subscription.unsubscribe());
}
else {
throw new Error(`Missing emitter '${output.prop}' on component '${getTypeName(this.componentFactory.componentType)}'!`);
}
}
registerCleanup(componentRef) {
const testabilityRegistry = componentRef.injector.get(TestabilityRegistry);
const destroyComponentRef = this.wrapCallback(() => 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(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();
}
});
}
updateInput(componentRef, prop, prevValue, currValue, isSignal) {
if (this.implementsOnChanges) {
this.inputChanges[prop] = new SimpleChange(prevValue, currValue, prevValue === currValue);
}
this.inputChangeCount++;
if (isSignal && !this.unsafelyOverwriteSignalInputs) {
const node = componentRef.instance[prop][_SIGNAL];
node.applyValueToInputSignal(node, currValue);
}
else {
componentRef.instance[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;
}
function matchesSelector(el, selector) {
const elProto = Element.prototype;
return el.nodeType === Node.ELEMENT_NODE
? // matches is supported by all browsers from 2014 onwards except non-chromium edge
(elProto.matches ?? elProto.msMatchesSelector).call(el, selector)
: false;
}
function isThenable(obj) {
return !!obj && isFunction(obj.then);
}
/**
* Synchronous, promise-like object.
*/
class SyncPromise {
value;
resolved = false;
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);
}
}
}
/**
* @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](https://angular.io/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 /api/core/ChangeDetectorRef#detectChanges detectChanges} on the
* component on every {@link https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$digest $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) {
const unsafelyOverwriteSignalInputs = info.unsafelyOverwriteSignalInputs ?? false;
// 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 /* UpgradeAppType.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],
// Controller needs to be set so that `angular-component-router.js` (from beta Angular 2)
// configuration properties can be made available. See:
// See G3: javascript/angular2/angular1_router_lib.js
// https://github.com/angular/angular.js/blob/47bf11ee94664367a26ed8c91b9b586d3dd420f5/src/ng/compile.js#L1670-L1691.
controller: function () { },
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, unsafelyOverwriteSignalInputs);
const projectableNodes = facade.compileContents();
const componentRef = facade.createComponentAndSetup(projectableNodes, isNgUpgradeLite, info.propagateDigest);
injectorPromise.resolve(componentRef.injector);
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 {
element;
injectorKey = controllerKey(INJECTOR_KEY);
constructor(element) {
super();
this.element = element;
// 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);
}
}
/**
* @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="docs-alert docs-alert-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;
}
/**
* The Trusted Types policy, or null if Trusted Types are not
* enabled/supported, or undefined if the policy has not been created yet.
*/
let policy;
/**
* Returns the Trusted Types policy, or null if Trusted Types are not
* enabled/supported. The first call to this function will create the policy.
*/
function getPolicy() {
if (policy === undefined) {
policy = null;
const windowWithTrustedTypes = window;
if (windowWithTrustedTypes.trustedTypes) {
try {
policy = windowWithTrustedTypes.trustedTypes.createPolicy('angular#unsafe-upgrade', {
createHTML: (s) => s,
});
}
catch {
// trustedTypes.createPolicy throws if called with a name that is
// already registered, even in report-only mode. Until the API changes,
// catch the error not to break the applications functionally. In such
// cases, the code will fall back to using strings.
}
}
}
return policy;
}
/**
* Unsafely promote a legacy AngularJS template to a TrustedHTML, falling back
* to strings when Trusted Types are not available.
* @security This is a security-sensitive function; any use of this function
* must go through security review. In particular, the template string should
* always be under full control of the application author, as untrusted input
* can cause an XSS vulnerability.
*/
function trustedHTMLFromLegacyTemplate(html) {
return getPolicy()?.createHTML(html) || html;
}
// Constants
const REQUIRE_PREFIX_RE = /^(\^\^?)?(\?)?(\^\^?)?/;
// Classes
class UpgradeHelper {
name;
$injector;
element;
$element;
directive;
$compile;
$controller;
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 trustedHTMLFromLegacyTemplate(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 trustedHTMLFromLegacyTemplate(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(trustedHTMLFromLegacyTemplate($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)) {
childNode.remove();
childNodes.push(childNode);
}
return childNodes;
}
getDirectiveRequire() {
const require = this.directive.require || (this.directive.controller && this.directive.name);
if (isMap(require)) {
Object.entries(require).forEach(([key, value]) => {
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) {
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 new Error(`Unable to find required '${require}' in upgraded direct