diff --git a/src/core/features/compile/components/compile-html/compile-html.ts b/src/core/features/compile/components/compile-html/compile-html.ts index dd6b6edbcc5..2cceb8f0fc6 100644 --- a/src/core/features/compile/components/compile-html/compile-html.ts +++ b/src/core/features/compile/components/compile-html/compile-html.ts @@ -13,6 +13,7 @@ // limitations under the License. import { toBoolean } from '@/core/transforms/boolean'; +import { effectWithInjectionContext } from '@/core/utils/signals'; import { Component, Input, @@ -34,6 +35,9 @@ import { Type, KeyValueDiffer, Injector, + EffectRef, + EffectCleanupRegisterFn, + CreateEffectOptions, } from '@angular/core'; import { CorePromisedValue } from '@classes/promised-value'; @@ -212,6 +216,7 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { return class CoreCompileHtmlFakeComponent implements OnInit, AfterContentInit, AfterViewInit, OnDestroy { private ongoingLifecycleHooks: Set = new Set(); + protected effectRefs: EffectRef[] = []; constructor() { // Store this instance so it can be accessed by the outer component. @@ -221,12 +226,25 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { this['dataObject'] = {}; this['dataArray'] = []; + const effectWithContext = effectWithInjectionContext(compileInstance.injector); + // Inject the libraries. - CoreCompile.injectLibraries( - this, - compileInstance.extraProviders, - compileInstance.injector, - ); + CoreCompile.injectLibraries(this, { + extraLibraries: compileInstance.extraProviders, + injector: compileInstance.injector, + // Capture calls to effect to retrieve the effectRefs and destroy them when this component is destroyed. + // Otherwise effects are only destroyed when the parent component is destroyed. + effectWrapper: ( + effectFn: (onCleanup: EffectCleanupRegisterFn) => void, + options?: Omit, + ): EffectRef => { + const effectRef = effectWithContext(effectFn, options); + + this.effectRefs.push(effectRef); + + return effectRef; + }, + }); // Always add these elements, they could be needed on component init (componentObservable). this['ChangeDetectorRef'] = compileInstance.changeDetector; @@ -280,6 +298,9 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { * @inheritdoc */ ngOnDestroy(): void { + this.effectRefs.forEach(effectRef => effectRef.destroy()); + this.effectRefs = []; + this.callLifecycleHookOverride('ngOnDestroy'); } diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index b22e454942b..d142682cff7 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -23,6 +23,7 @@ import { ViewContainerRef, signal, computed, + effect, untracked, } from '@angular/core'; import { @@ -261,20 +262,19 @@ export class CoreCompileProvider { * Inject all the core libraries in a certain object. * * @param instance The instance where to inject the libraries. - * @param extraLibraries Extra imported providers if needed and not imported by this class. - * @param injector Injector of the injection context. E.g. for a component, use the component's injector. + * @param options Options. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any - injectLibraries(instance: any, extraLibraries: Type[] = [], injector?: Injector): void { + injectLibraries(instance: any, options: InjectLibrariesOptions = {}): void { if (!this.libraries || !this.exportedObjects) { throw new CoreError('Libraries not loaded. You need to call loadLibraries before calling injectLibraries.'); } const libraries = [ ...this.libraries, - ...extraLibraries, + ...options.extraLibraries ?? [], ]; - injector = injector ?? this.injector; + const injector = options.injector ?? this.injector; // We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance. for (const i in libraries) { @@ -305,7 +305,7 @@ export class CoreCompileProvider { instance['signal'] = signal; instance['computed'] = computed; instance['untracked'] = untracked; - instance['effect'] = effectWithInjectionContext(injector); + instance['effect'] = options.effectWrapper ?? effectWithInjectionContext(injector); instance['model'] = modelWithInjectionContext(injector); /** @@ -430,3 +430,13 @@ export class CoreCompileProvider { } export const CoreCompile = makeSingleton(CoreCompileProvider); + +/** + * Options for injectLibraries. + */ +type InjectLibrariesOptions = { + extraLibraries?: Type[]; // Extra imported providers if needed and not imported by this class. + injector?: Injector; // Injector of the injection context. E.g. for a component, use the component's injector. + effectWrapper?: typeof effect; // Wrapper function to create an effect. If not provided, a wrapper will be created using the + // injector. Use this wrapper if you want to capture the created EffectRefs. +};