UNPKG

@angular/upgrade

Version:

Angular - the library for easing update from v1 to v2

1,080 lines (1,069 loc) 51 kB
/** * @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