Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lib): lazy url config #78

Merged
merged 5 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/ngx-fast-icon-demo/src/app/app.config.server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { join } from 'node:path';
import { readFileSync } from 'node:fs';

import { mergeApplicationConfig, ApplicationConfig, Injectable } from '@angular/core';
import { ApplicationConfig, Injectable, mergeApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';

import { Observable, of } from 'rxjs';
Expand All @@ -11,7 +11,7 @@ import { provideFastSVG, SvgLoadStrategy } from '@push-based/ngx-fast-svg';
import { appConfig } from './app.config';

@Injectable()
export class SvgLoadStrategySsr implements SvgLoadStrategy {
export class SvgLoadStrategySsr extends SvgLoadStrategy {
load(url: string): Observable<string> {
const iconPath = join(process.cwd(), 'packages', 'ngx-fast-icon-demo', 'src', url);
const iconSVG = readFileSync(iconPath, 'utf8')
Expand Down
14 changes: 12 additions & 2 deletions packages/ngx-fast-icon-demo/src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApplicationConfig } from '@angular/core';
import { ApplicationConfig, Injectable } from '@angular/core';
import {
provideRouter,
withComponentInputBinding,
Expand All @@ -9,11 +9,20 @@ import { provideHttpClient, withFetch } from '@angular/common/http';
import { provideClientHydration } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';

import { provideFastSVG } from '@push-based/ngx-fast-svg';
import { provideFastSVG, SvgLoadStrategyImpl } from '@push-based/ngx-fast-svg';
import { provideAngularSvgIcon } from 'angular-svg-icon';
import { provideIonicAngular } from '@ionic/angular/standalone';

import { appRoutes } from './app.routes';
import { Observable, of, switchMap, timer } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class ConfigService extends SvgLoadStrategyImpl {
lazy$ = timer(10_000)
override config(url: string): Observable<string> {
return this.lazy$.pipe(switchMap(() => of(url)))
}
}

export const appConfig: ApplicationConfig = {
providers: [
Expand All @@ -33,6 +42,7 @@ export const appConfig: ApplicationConfig = {
provideIonicAngular({}),
provideFastSVG({
url: (name: string) => `assets/svg-icons/${name}.svg`,
svgLoadStrategy: ConfigService
}),
],
};
2 changes: 1 addition & 1 deletion packages/ngx-fast-lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
export * from './lib/token/svg-options.model';
export * from './lib/token/svg-options.token';
export * from './lib/token/svg-load.strategy.model';
export * from './lib/token/svg-load.strategy';
export { SvgLoadStrategyImpl } from './lib/token/svg-load.strategy';
// service
export * from './lib/svg-registry.service';
// component
Expand Down
44 changes: 25 additions & 19 deletions packages/ngx-fast-lib/src/lib/fast-svg.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
effect,
ElementRef,
inject,
Injector,
input,
OnDestroy,
PLATFORM_ID,
Renderer2,
effect,
inject,
input,
untracked,
Renderer2
} from '@angular/core';
import { getZoneUnPatchedApi } from './internal/get-zone-unpatched-api';
import { SvgRegistry } from './svg-registry.service';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { of, switchMap } from 'rxjs';

/**
* getZoneUnPatchedApi
Expand Down Expand Up @@ -110,6 +111,10 @@ export class FastSvgComponent implements AfterViewInit, OnDestroy {
width = input<string>('');
height = input<string>('');

#url = toSignal(toObservable(this.name).pipe(switchMap((name) => {
return this.registry.url(name);
})))

// When the browser loaded the svg resource we trigger the caching mechanism
// re-fetch -> cache-hit -> get SVG -> cache in DOM
loadedListener = () => {
Expand Down Expand Up @@ -142,7 +147,6 @@ export class FastSvgComponent implements AfterViewInit, OnDestroy {
(onCleanup) => {
const name = this.name();

untracked(() => {
if (!name) {
throw new Error('svg component needs a name to operate');
}
Expand All @@ -153,32 +157,35 @@ export class FastSvgComponent implements AfterViewInit, OnDestroy {
if (!this.registry.isSvgCached(name)) {
/**
CSR - Browser native lazy loading hack

We use an img element here to leverage the browsers native features:
- lazy loading (loading="lazy") to only load the svg that are actually visible
- priority hints to down prioritize the fetch to avoid delaying the LCP

While the SVG is loading we display a fallback SVG.
After the image is loaded we remove it from the DOM. (IMG load event)
When the new svg arrives we append it to the template.

Note:
- the image is styled with display none. this prevents any loading of the resource ever.
on component bootstrap we decide what we want to do. when we remove display none it performs the browser native behavior
- the image has 0 height and with and containment as well as contnet-visibility to reduce any performance impact
- the image has 0 height and with and containment as well as content-visibility to reduce any performance impact


Edge cases:
- only resources that are not loaded in the current session of the browser will get lazy loaded (same URL to trigger loading is not possible)
- already loaded resources will get emitted from the cache immediately, even if loading is set to lazy :o
- the image needs to have display other than none
*/
const i = this.getImg(this.registry.url(name));
this.renderer.appendChild(this.element.nativeElement, i);

// get img
img = elem.querySelector('img') as HTMLImageElement;
addEventListener(img, 'load', this.loadedListener);
const url = this.#url();
if (url) {
const i = this.getImg(url);
this.renderer.appendChild(this.element.nativeElement, i);

// get img
img = elem.querySelector('img') as HTMLImageElement;
addEventListener(img, 'load', this.loadedListener);
}
}

// Listen to svg changes
Expand Down Expand Up @@ -225,7 +232,6 @@ export class FastSvgComponent implements AfterViewInit, OnDestroy {
img.removeEventListener('load', this.loadedListener);
}
});
});
},
{ injector: this.injector }
);
Expand Down
4 changes: 2 additions & 2 deletions packages/ngx-fast-lib/src/lib/svg-registry.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BehaviorSubject, map, Observable } from 'rxjs';
import { SvgOptionsToken } from './token/svg-options.token';
import { suspenseSvg } from './token/default-token-values';
import { SvgLoadStrategy } from './token/svg-load.strategy.model';
import { SvgLoadStrategyImpl } from "./token/svg-load.strategy";
import { SvgLoadStrategyImpl } from './token/svg-load.strategy';

// @TODO compose svg in 1 sprite and fetch by id as before

Expand Down Expand Up @@ -69,7 +69,7 @@ export class SvgRegistry {
public defaultSize = this.svgOptions?.defaultSize || '24';
private _defaultViewBox = `0 0 ${this.defaultSize} ${this.defaultSize}`;

public url = this.svgOptions.url;
public url = (name: string) => this.svgLoadStrategy.config(this.svgOptions.url(name));

constructor() {
// configure suspense svg
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Observable } from 'rxjs';
import { Observable, of } from 'rxjs';
import { Injectable } from '@angular/core';

@Injectable()
export abstract class SvgLoadStrategy {
abstract load(url: string): Observable<string>;
abstract load(url: string | Observable<string>): Observable<string>;
config(url: string): Observable<string> {
return of(url)
};
}
2 changes: 1 addition & 1 deletion packages/ngx-fast-lib/src/lib/token/svg-load.strategy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { from, Observable } from 'rxjs';
import { getZoneUnPatchedApi } from '../internal/get-zone-unpatched-api';
import { SvgLoadStrategy } from "./svg-load.strategy.model";
import { SvgLoadStrategy } from './svg-load.strategy.model';

export class SvgLoadStrategyImpl extends SvgLoadStrategy {
fetch = getZoneUnPatchedApi('fetch', window as any);
Expand Down
Loading