);
+ readonly #elementsLoaderService = inject(LazyElementsLoaderService);
ngOnInit() {
// There's no sense to execute the below logic on the Node.js side since the JavaScript
// will not be loaded on the server-side (Angular will only append the script to body).
// The `loadElement` promise will never be resolved, since it gets resolved when the `load` event is emitted.
// `customElements` are also undefined on the Node.js side; thus, it will always render the error template.
- if (isPlatformServer(this.platformId)) {
+ if (isPlatformServer(this.#platformId)) {
return;
}
@@ -68,19 +69,20 @@ export class LazyElementDynamicDirective implements OnInit {
const tag = this.tag!;
const elementConfig =
- this.elementsLoaderService.getElementConfig(tag) || ({} as ElementConfig);
- const options = this.elementsLoaderService.options;
+ this.#elementsLoaderService.getElementConfig(tag) ||
+ ({} as ElementConfig);
+ const options = this.#elementsLoaderService.options;
const loadingComponent =
elementConfig.loadingComponent || options.loadingComponent;
if (this.loadingTemplateRef) {
- this.vcr.createEmbeddedView(this.loadingTemplateRef);
+ this.#vcr.createEmbeddedView(this.loadingTemplateRef);
} else if (loadingComponent) {
- this.vcr.createComponent(loadingComponent);
+ this.#vcr.createComponent(loadingComponent);
}
const loadElement$ = from(
- this.elementsLoaderService.loadElement(
+ this.#elementsLoaderService.loadElement(
this.url,
tag,
this.isModule,
@@ -92,34 +94,34 @@ export class LazyElementDynamicDirective implements OnInit {
loadElement$
.pipe(
mergeMap(() => customElements.whenDefined(tag)),
- takeUntilDestroyed(this.destroyRef),
+ takeUntilDestroyed(this.#destroyRef),
)
.subscribe({
next: () => {
this.loadingSuccess.emit();
- this.vcr.clear();
- const originalCreateElement = this.renderer.createElement;
- this.renderer.createElement = (name: string, namespace: string) => {
+ this.#vcr.clear();
+ const originalCreateElement = this.#renderer.createElement;
+ this.#renderer.createElement = (name: string, namespace: string) => {
if (name === 'ax-lazy-element') {
name = tag;
}
- return this.document.createElement(name);
+ return this.#document.createElement(name);
};
- this.viewRef = this.vcr.createEmbeddedView(this.template);
- this.renderer.createElement = originalCreateElement;
- this.cdr.markForCheck();
+ this.#viewRef = this.#vcr.createEmbeddedView(this.#template);
+ this.#renderer.createElement = originalCreateElement;
+ this.#cdr.markForCheck();
},
error: (error) => {
this.loadingError.emit(error);
const errorComponent =
elementConfig.errorComponent || options.errorComponent;
- this.vcr.clear();
+ this.#vcr.clear();
if (this.errorTemplateRef) {
- this.vcr.createEmbeddedView(this.errorTemplateRef);
- this.cdr.markForCheck();
+ this.#vcr.createEmbeddedView(this.errorTemplateRef);
+ this.#cdr.markForCheck();
} else if (errorComponent) {
- this.vcr.createComponent(errorComponent);
- this.cdr.markForCheck();
+ this.#vcr.createComponent(errorComponent);
+ this.#cdr.markForCheck();
} else if (ngDevMode) {
console.error(
`${LOG_PREFIX} - Loading of element <${this.tag}> failed, please provide Loading failed... and reference it in *axLazyElementDynamic="errorTemplate: error" to display customized error message in place of element\n\n`,
@@ -131,10 +133,10 @@ export class LazyElementDynamicDirective implements OnInit {
}
destroyEmbeddedView() {
- if (this.viewRef && !this.viewRef.destroyed) {
- this.viewRef.detach();
- this.viewRef.destroy();
- this.viewRef = null;
+ if (this.#viewRef && !this.#viewRef.destroyed) {
+ this.#viewRef.detach();
+ this.#viewRef.destroy();
+ this.#viewRef = null;
}
}
}
diff --git a/projects/elements/src/lib/lazy-elements/lazy-element/lazy-element.directive.spec.ts b/projects/elements/src/lib/lazy-elements/lazy-element/lazy-element.directive.spec.ts
index 75314e8..9c5e285 100644
--- a/projects/elements/src/lib/lazy-elements/lazy-element/lazy-element.directive.spec.ts
+++ b/projects/elements/src/lib/lazy-elements/lazy-element/lazy-element.directive.spec.ts
@@ -1,36 +1,37 @@
import { jest } from '@jest/globals';
-import { Component, CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
+import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LazyElementsModule } from '../lazy-elements.module';
import { LazyElementsLoaderService } from '../lazy-elements-loader.service';
+import { LazyElementDirective } from './lazy-element.directive';
@Component({
+ standalone: true,
template: ` Spinner...
`,
})
class SpinnerTestComponent {}
-@NgModule({
- declarations: [SpinnerTestComponent],
-})
-class TestModule {}
-
@Component({
+ standalone: true,
+ imports: [SpinnerTestComponent, LazyElementDirective],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
-
+ @if (addSameElement) {
-
-
+ }
+ @if (addOtherElement) {
-
-
+ }
+
+ @if (useLoadingTemplate) {
Loading...
@@ -40,8 +41,9 @@ class TestModule {}
loadingTemplate: loading
"
>
-
-
+ }
+
+ @if (useErrorTemplate) {
Loading...
@@ -55,26 +57,30 @@ class TestModule {}
errorTemplate: error
"
>
-
-
+ }
+
+ @if (useModule) {
-
-
+ }
+
+ @if (useImportMap) {
-
-
+ }
+
+ @if (useElementConfig) {
-
-
+ }
+
+ @if (useUrlBinding) {
-
+ }
`,
})
class TestHostComponent {
@@ -111,7 +117,7 @@ describe('LazyElementDirective', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
- TestModule,
+ TestHostComponent,
LazyElementsModule.forRoot({
elementConfigs: [
{
@@ -129,7 +135,6 @@ describe('LazyElementDirective', () => {
}),
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
- declarations: [TestHostComponent],
}).compileComponents();
});
diff --git a/projects/elements/src/lib/lazy-elements/lazy-element/lazy-element.directive.ts b/projects/elements/src/lib/lazy-elements/lazy-element/lazy-element.directive.ts
index 86480e5..5ba0ec5 100644
--- a/projects/elements/src/lib/lazy-elements/lazy-element/lazy-element.directive.ts
+++ b/projects/elements/src/lib/lazy-elements/lazy-element/lazy-element.directive.ts
@@ -1,11 +1,11 @@
import {
ChangeDetectorRef,
+ DestroyRef,
Directive,
EmbeddedViewRef,
inject,
Input,
OnChanges,
- OnDestroy,
OnInit,
output,
PLATFORM_ID,
@@ -14,12 +14,12 @@ import {
ViewContainerRef,
} from '@angular/core';
import { isPlatformServer } from '@angular/common';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
animationFrameScheduler,
BehaviorSubject,
EMPTY,
from,
- Subscription,
catchError,
debounceTime,
mergeMap,
@@ -33,9 +33,17 @@ import { LazyElementsLoaderService } from '../lazy-elements-loader.service';
const LOG_PREFIX = '@angular-extensions/elements';
@Directive({
+ standalone: true,
selector: '[axLazyElement]',
})
-export class LazyElementDirective implements OnChanges, OnInit, OnDestroy {
+export class LazyElementDirective implements OnInit, OnChanges {
+ readonly #platformId = inject(PLATFORM_ID);
+ readonly #destroyRef = inject(DestroyRef);
+ readonly #vcr = inject(ViewContainerRef);
+ readonly #cdr = inject(ChangeDetectorRef);
+ readonly #template = inject(TemplateRef);
+ readonly #elementsLoaderService = inject(LazyElementsLoaderService);
+
@Input('axLazyElement') url: string | null = null;
@Input('axLazyElementLoadingTemplate') // eslint-disable-line @angular-eslint/no-input-rename
loadingTemplateRef: TemplateRef | null = null;
@@ -47,19 +55,12 @@ export class LazyElementDirective implements OnChanges, OnInit, OnDestroy {
loadingSuccess = output();
loadingError = output();
- private viewRef: EmbeddedViewRef | null = null;
- private subscription = Subscription.EMPTY;
- private url$ = new BehaviorSubject(null);
-
- private readonly platformId = inject(PLATFORM_ID);
- private readonly vcr = inject(ViewContainerRef);
- private readonly template = inject(TemplateRef);
- private readonly elementsLoaderService = inject(LazyElementsLoaderService);
- private readonly cdr = inject(ChangeDetectorRef);
+ #viewRef: EmbeddedViewRef | null = null;
+ #url$ = new BehaviorSubject(null);
ngOnChanges(changes: SimpleChanges): void {
if (changes.url) {
- this.url$.next(this.url);
+ this.#url$.next(this.url);
}
}
@@ -68,39 +69,35 @@ export class LazyElementDirective implements OnChanges, OnInit, OnDestroy {
// will not be loaded on the server-side (Angular will only append the script to body).
// The `loadElement` promise will never be resolved, since it gets resolved when the `load` event is emitted.
// `customElements` are also undefined on the Node.js side; thus, it will always render the error template.
- if (isPlatformServer(this.platformId)) {
+ if (isPlatformServer(this.#platformId)) {
return;
}
- this.setupUrlListener();
- }
-
- ngOnDestroy(): void {
- this.subscription.unsubscribe();
+ this.#setupUrlListener();
}
destroyEmbeddedView() {
- if (this.viewRef && !this.viewRef.destroyed) {
- this.viewRef.detach();
- this.viewRef.destroy();
- this.viewRef = null;
+ if (this.#viewRef && !this.#viewRef.destroyed) {
+ this.#viewRef.detach();
+ this.#viewRef.destroy();
+ this.#viewRef = null;
}
}
- private setupUrlListener(): void {
- const tpl = this.template as any;
+ #setupUrlListener(): void {
+ const tpl = this.#template as any;
const elementTag = tpl._declarationTContainer
? tpl._declarationTContainer.tagName || tpl._declarationTContainer.value
- : tpl._def.element.template.nodes[0].element.name;
+ : tpl._def.element.#template.nodes[0].element.name;
const elementConfig =
- this.elementsLoaderService.getElementConfig(elementTag) ||
+ this.#elementsLoaderService.getElementConfig(elementTag) ||
({} as ElementConfig);
- const options = this.elementsLoaderService.options;
+ const options = this.#elementsLoaderService.options;
const loadingComponent =
elementConfig.loadingComponent || options.loadingComponent;
- this.subscription = this.url$
+ this.#url$
.pipe(
// This is used to coalesce changes since the `url$` subject might emit multiple values initially, e.g.
// `null` (initial value) and the url itself (when the `url` binding is provided).
@@ -108,13 +105,13 @@ export class LazyElementDirective implements OnChanges, OnInit, OnDestroy {
debounceTime(0, animationFrameScheduler),
switchMap((url) => {
if (this.loadingTemplateRef) {
- this.vcr.createEmbeddedView(this.loadingTemplateRef);
+ this.#vcr.createEmbeddedView(this.loadingTemplateRef);
} else if (loadingComponent) {
- this.vcr.createComponent(loadingComponent);
+ this.#vcr.createComponent(loadingComponent);
}
return from(
- this.elementsLoaderService.loadElement(
+ this.#elementsLoaderService.loadElement(
url,
elementTag,
this.isModule,
@@ -124,15 +121,15 @@ export class LazyElementDirective implements OnChanges, OnInit, OnDestroy {
).pipe(
catchError((error) => {
this.loadingError.emit(error);
- this.vcr.clear();
+ this.#vcr.clear();
const errorComponent =
elementConfig.errorComponent || options.errorComponent;
if (this.errorTemplateRef) {
- this.vcr.createEmbeddedView(this.errorTemplateRef);
- this.cdr.markForCheck();
+ this.#vcr.createEmbeddedView(this.errorTemplateRef);
+ this.#cdr.markForCheck();
} else if (errorComponent) {
- this.vcr.createComponent(errorComponent);
- this.cdr.markForCheck();
+ this.#vcr.createComponent(errorComponent);
+ this.#cdr.markForCheck();
} else if (ngDevMode) {
console.error(
`${LOG_PREFIX} - Loading of element <${elementTag}> failed, please provide Loading failed... and reference it in *axLazyElement="errorTemplate: error" to display customized error message in place of element`,
@@ -144,12 +141,13 @@ export class LazyElementDirective implements OnChanges, OnInit, OnDestroy {
}),
tap(() => this.loadingSuccess.emit()),
mergeMap(() => customElements.whenDefined(elementTag)),
+ takeUntilDestroyed(this.#destroyRef),
)
.subscribe({
next: () => {
- this.vcr.clear();
- this.viewRef = this.vcr.createEmbeddedView(this.template);
- this.cdr.markForCheck();
+ this.#vcr.clear();
+ this.#viewRef = this.#vcr.createEmbeddedView(this.#template);
+ this.#cdr.markForCheck();
},
});
}
diff --git a/projects/elements/src/lib/lazy-elements/lazy-elements-loader.service.ts b/projects/elements/src/lib/lazy-elements/lazy-elements-loader.service.ts
index 72a5716..f0d85ec 100644
--- a/projects/elements/src/lib/lazy-elements/lazy-elements-loader.service.ts
+++ b/projects/elements/src/lib/lazy-elements/lazy-elements-loader.service.ts
@@ -30,14 +30,21 @@ interface Notifier {
providedIn: 'root',
})
export class LazyElementsLoaderService implements OnDestroy {
- static controller: any = new AbortController();
- configs: ElementConfig[] = [];
+ static controller = new AbortController();
- private readonly errorHandler = inject(ErrorHandler);
- private readonly registry = inject(LAZY_ELEMENTS_REGISTRY);
+ readonly #errorHandler = inject(ErrorHandler);
+ readonly #registry = inject(LAZY_ELEMENTS_REGISTRY);
public readonly options =
inject(LAZY_ELEMENT_ROOT_OPTIONS, { optional: true }) ?? {};
+ configs: ElementConfig[] = [];
+
+ @HostListener('unloaded')
+ ngOnDestroy(): void {
+ LazyElementsLoaderService.controller?.abort();
+ LazyElementsLoaderService.controller = null;
+ }
+
addConfigs(newConfigs: ElementConfig[]) {
newConfigs.forEach((newConfig) => {
const existingConfig = this.getElementConfig(newConfig.tag);
@@ -113,8 +120,8 @@ export class LazyElementsLoaderService implements OnDestroy {
}
}
- if (!this.hasElement(url)) {
- const notifier = this.addElement(url);
+ if (!this.#hasElement(url)) {
+ const notifier = this.#addElement(url);
const beforeLoadHook =
hooksConfig?.beforeLoad ??
@@ -126,7 +133,7 @@ export class LazyElementsLoaderService implements OnDestroy {
this.options?.hooks?.afterLoad;
if (importMap) {
- url = await this.resolveImportMap(url);
+ url = await this.#resolveImportMap(url);
}
const script = document.createElement('script') as HTMLScriptElement;
@@ -136,7 +143,7 @@ export class LazyElementsLoaderService implements OnDestroy {
script.src = getPolicy()?.createScriptURL(url) ?? url;
const onLoad = () => {
if (afterLoadHook) {
- this.handleHook(afterLoadHook, tag)
+ this.#handleHook(afterLoadHook, tag)
.then(notifier.resolve)
.catch(notifier.reject);
} else {
@@ -151,7 +158,7 @@ export class LazyElementsLoaderService implements OnDestroy {
// Caretaker note: don't put it before the `reject` and `cleanup` since the user may have some
// custom error handler that will re-throw the error through `throw error`. Hence the code won't
// be executed, and the promise won't be rejected.
- this.errorHandler.handleError(error);
+ this.#errorHandler.handleError(error);
};
// The `load` and `error` event listeners capture `this`. That's why they have to be removed manually.
// Otherwise, the `LazyElementsLoaderService` is not going to be GC'd.
@@ -166,34 +173,34 @@ export class LazyElementsLoaderService implements OnDestroy {
signal: LazyElementsLoaderService.controller?.signal,
} as AddEventListenerOptions);
if (beforeLoadHook) {
- this.handleHook(beforeLoadHook, tag)
+ this.#handleHook(beforeLoadHook, tag)
.then(() => document.body.appendChild(script))
.catch(notifier.reject);
} else {
document.body.appendChild(script);
}
}
- return this.registry.get(this.stripUrlProtocol(url));
+ return this.#registry.get(this.#stripUrlProtocol(url));
}
- private addElement(url: string): Notifier {
+ #addElement(url: string): Notifier {
let notifier: Notifier;
- this.registry.set(
- this.stripUrlProtocol(url),
+ this.#registry.set(
+ this.#stripUrlProtocol(url),
new Promise((resolve, reject) => (notifier = { resolve, reject })),
);
return notifier!;
}
- private hasElement(url: string): boolean {
- return this.registry.has(this.stripUrlProtocol(url));
+ #hasElement(url: string): boolean {
+ return this.#registry.has(this.#stripUrlProtocol(url));
}
- private stripUrlProtocol(url: string): string {
+ #stripUrlProtocol(url: string): string {
return url.replace(/https?:\/\//, '');
}
- private handleHook(hook: Hook, tag: string): Promise {
+ #handleHook(hook: Hook, tag: string): Promise {
try {
return Promise.resolve(hook(tag));
} catch (err) {
@@ -201,7 +208,7 @@ export class LazyElementsLoaderService implements OnDestroy {
}
}
- private async resolveImportMap(url: string) {
+ async #resolveImportMap(url: string) {
const System = (window as any).System;
if (System) {
await System.prepareImport();
@@ -213,10 +220,4 @@ export class LazyElementsLoaderService implements OnDestroy {
}
return url;
}
-
- @HostListener('unloaded')
- public ngOnDestroy(): void {
- LazyElementsLoaderService.controller?.abort();
- LazyElementsLoaderService.controller = null;
- }
}
diff --git a/projects/elements/src/lib/lazy-elements/lazy-elements.module.ts b/projects/elements/src/lib/lazy-elements/lazy-elements.module.ts
index bd9c98d..9a6deb3 100644
--- a/projects/elements/src/lib/lazy-elements/lazy-elements.module.ts
+++ b/projects/elements/src/lib/lazy-elements/lazy-elements.module.ts
@@ -5,7 +5,6 @@ import {
Optional,
SkipSelf,
} from '@angular/core';
-import { CommonModule } from '@angular/common';
import {
ElementConfig,
@@ -36,8 +35,7 @@ export function createLazyElementRootGuard(
}
@NgModule({
- declarations: [LazyElementDirective, LazyElementDynamicDirective],
- imports: [CommonModule],
+ imports: [LazyElementDirective, LazyElementDynamicDirective],
exports: [LazyElementDirective, LazyElementDynamicDirective],
})
export class LazyElementsModule {