Skip to content

Commit

Permalink
feat(lib): lazy url config
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristopherPHolder authored Oct 13, 2024
2 parents 376a952 + c4e3ff5 commit c92af22
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 51 deletions.
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This library covers next aspects that developers should consider for their proje
- SVG reusability
- Optimized bundle size
- SSR
- Edge ready (only edge save APIs are used)
- Edge ready (only edge safe APIs are used)

## Getting started

Expand Down Expand Up @@ -103,7 +103,8 @@ During setup phase you can provide additional optional settings such as:
svgLoadStrategy?: Type<SvgLoadStrategy>;
```

`svgLoadStrategy` can be any injectable class that has `load` method that accepts url and returns observable string:
`svgLoadStrategy` can be any injectable class that has `config` that excepts method that accepts url and returns observable string,
and `load` which accepts the configured url as an observable and returns the svg as an observable string.

```typescript
@Injectable()
Expand Down Expand Up @@ -196,6 +197,36 @@ And then just provide it in you server module.
export class AppServerModule {}
```

#### Providing a lazy configuration

If you need to provide a lazy configuration you can use the config method in the `SvgLoadStrategy`:

```typescript
@Injectable()
class LazyConfigSvgLoadStrategy extends SvgLoadStrategyImpl {
dummyLazyConfig$ = timer(5_000).pipe(map(() => 'assets/svg-icons'))
override config(url: string): Observable<string> {
return this.dummyLazyConfig$.pipe(map((svgConfig) => `${svgConfig}/${url}`));
}
}
```

And pass it to the provider function:

```typescript
import { provideFastSVG } from '@push-based/ngx-fast-svg';

bootstrapApplication(AppComponent, {
providers: [
// ... other providers
provideFastSVG({
url: (name: string) => `${name}.svg`,
svgLoadStrategy: LazyConfigSvgLoadStrategy,
})
]
});
```

## Features

### :sloth: Lazy loading for SVGs
Expand Down Expand Up @@ -273,4 +304,3 @@ To display (render) SVGs the browser takes time. We can reduce that time by addi
---

made with ❤ by [push-based.io](https://www.push-based.io)

8 changes: 4 additions & 4 deletions packages/ngx-fast-icon-demo/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Component, inject, PLATFORM_ID } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
import { NavigationEnd, Router, RouterOutlet } from '@angular/router';

import { filter, map, Observable, startWith } from 'rxjs';
import {MediaMatcher} from '@angular/cdk/layout';
import {ShellComponent} from './misc/shell.component';
import {AsyncPipe, isPlatformServer} from '@angular/common';
import { MediaMatcher } from '@angular/cdk/layout';
import { ShellComponent } from './misc/shell.component';
import { AsyncPipe, isPlatformServer } from '@angular/common';

@Component({
selector: 'ngx-fast-icon-root',
Expand Down
18 changes: 10 additions & 8 deletions packages/ngx-fast-icon-demo/src/app/app.config.server.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { join } from 'node:path';
import { readFileSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { cwd } from 'node:process';

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';
import { from, Observable, of, switchMap } from 'rxjs';

import { provideFastSVG, SvgLoadStrategy } from '@push-based/ngx-fast-svg';

import { appConfig } from './app.config';

@Injectable()
export class SvgLoadStrategySsr implements SvgLoadStrategy {
load(url: string): Observable<string> {
const iconPath = join(process.cwd(), 'packages', 'ngx-fast-icon-demo', 'src', url);
const iconSVG = readFileSync(iconPath, 'utf8')
return of(iconSVG);
config(url: string) {
return of(join(cwd(), 'packages', 'ngx-fast-icon-demo', 'src', 'assets', 'svg-icons', url));
}
load(iconPath$: Observable<string>) {
return iconPath$.pipe(switchMap((iconPath) => from(readFile(iconPath, { encoding: 'utf8' }))))
}
}

Expand All @@ -24,7 +26,7 @@ const serverConfig: ApplicationConfig = {
provideServerRendering(),
provideFastSVG({
svgLoadStrategy: SvgLoadStrategySsr,
url: (name: string) => `assets/svg-icons/${name}.svg`,
url: (name: string) => `${name}.svg`,
}),
],
};
Expand Down
48 changes: 41 additions & 7 deletions packages/ngx-fast-lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This library covers next aspects that developers should consider for their proje
- SVG reusability
- Optimized bundle size
- SSR
- Edge ready (only edge safe APIs are used)

## Getting started

Expand Down Expand Up @@ -102,12 +103,14 @@ During setup phase you can provide additional optional settings such as:
svgLoadStrategy?: Type<SvgLoadStrategy>;
```

`svgLoadStrategy` can be any injectable class that has `load` method that accepts url and returns observable string:
`svgLoadStrategy` can be any injectable class that has `config` that excepts method that accepts url and returns observable string,
and `load` which accepts the configured url as an observable and returns the svg as an observable string.

```typescript
@Injectable()
export abstract class SvgLoadStrategy {
abstract load(url: string): Observable<string>;
abstract config(url: string): Observable<string>;
abstract load(url: Observable<string>): Observable<string>;
}
```

Expand Down Expand Up @@ -164,10 +167,11 @@ You can provide your own SSR loading strategy that can look like this:
```typescript
@Injectable()
export class SvgLoadStrategySsr implements SvgLoadStrategy {
load(url: string): Observable<string> {
const iconPath = join(process.cwd(), 'dist', 'app-name', 'browser', url);
const iconSVG = readFileSync(iconPath, 'utf8');
return of(iconSVG);
config(url: string) {
return of(join(cwd(), 'path', 'to', 'svg', 'assets', url));
}
load(iconPath$: Observable<string>) {
return iconPath$.pipe(switchMap((iconPath) => from(readFile(iconPath, { encoding: 'utf8' }))));
}
}
```
Expand All @@ -187,14 +191,44 @@ And then just provide it in you server module.
providers: [
provideFastSVG({
svgLoadStrategy: SvgLoadStrategySsr,
url: (name: string) => `assets/svg-icons/${name}.svg`,
url: (name: string) => `${name}.svg`,
}),
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
```

#### Providing a lazy configuration

If you need to provide a lazy configuration you can use the config method in the `SvgLoadStrategy`:

```typescript
@Injectable()
class LazyConfigSvgLoadStrategy extends SvgLoadStrategyImpl {
dummyLazyConfig$ = timer(5_000).pipe(map(() => 'assets/svg-icons'))
override config(url: string): Observable<string> {
return this.dummyLazyConfig$.pipe(map((svgConfig) => `${svgConfig}/${url}`));
}
}
```

And pass it to the provider function:

```typescript
import { provideFastSVG } from '@push-based/ngx-fast-svg';

bootstrapApplication(AppComponent, {
providers: [
// ... other providers
provideFastSVG({
url: (name: string) => `${name}.svg`,
svgLoadStrategy: LazyConfigSvgLoadStrategy,
})
]
});
```

## Features

### :sloth: Lazy loading for SVGs
Expand Down
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
6 changes: 3 additions & 3 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 Expand Up @@ -108,7 +108,7 @@ export class SvgRegistry {

// trigger fetch
this.svgLoadStrategy
.load(this.svgOptions.url(svgName))
.load(this.url(svgName))
.subscribe({
next: (body: string) => this.cacheSvgInDOM(svgId, body),
error: console.error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import { Injectable } from '@angular/core';

@Injectable()
export abstract class SvgLoadStrategy {
abstract load(url: string): Observable<string>;
abstract config(url: string): Observable<string>;
abstract load(url: Observable<string>): Observable<string>;
}
18 changes: 13 additions & 5 deletions packages/ngx-fast-lib/src/lib/token/svg-load.strategy.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { from, Observable } from 'rxjs';
import { from, Observable, of, switchMap } from 'rxjs';
import { getZoneUnPatchedApi } from '../internal/get-zone-unpatched-api';
import { SvgLoadStrategy } from "./svg-load.strategy.model";
import { SvgLoadStrategy } from './svg-load.strategy.model';
import { Injectable } from '@angular/core';

export class SvgLoadStrategyImpl extends SvgLoadStrategy {
@Injectable()
export class SvgLoadStrategyImpl implements SvgLoadStrategy {
fetch = getZoneUnPatchedApi('fetch', window as any);

load(url: string): Observable<string> {
return from(fetch(url).then((res) => (!res.ok ? '' : res.text())));
load(url$: Observable<string>): Observable<string> {
return url$.pipe(switchMap((url) => {
return from(fetch(url).then((res) => (!res.ok ? '' : res.text())));
}));
}

config(url: string) {
return of(url);
}
}

0 comments on commit c92af22

Please sign in to comment.