Skip to content

Commit c92af22

Browse files
feat(lib): lazy url config
2 parents 376a952 + c4e3ff5 commit c92af22

File tree

9 files changed

+132
-51
lines changed

9 files changed

+132
-51
lines changed

README.md

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ This library covers next aspects that developers should consider for their proje
1313
- SVG reusability
1414
- Optimized bundle size
1515
- SSR
16-
- Edge ready (only edge save APIs are used)
16+
- Edge ready (only edge safe APIs are used)
1717

1818
## Getting started
1919

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

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

108109
```typescript
109110
@Injectable()
@@ -196,6 +197,36 @@ And then just provide it in you server module.
196197
export class AppServerModule {}
197198
```
198199

200+
#### Providing a lazy configuration
201+
202+
If you need to provide a lazy configuration you can use the config method in the `SvgLoadStrategy`:
203+
204+
```typescript
205+
@Injectable()
206+
class LazyConfigSvgLoadStrategy extends SvgLoadStrategyImpl {
207+
dummyLazyConfig$ = timer(5_000).pipe(map(() => 'assets/svg-icons'))
208+
override config(url: string): Observable<string> {
209+
return this.dummyLazyConfig$.pipe(map((svgConfig) => `${svgConfig}/${url}`));
210+
}
211+
}
212+
```
213+
214+
And pass it to the provider function:
215+
216+
```typescript
217+
import { provideFastSVG } from '@push-based/ngx-fast-svg';
218+
219+
bootstrapApplication(AppComponent, {
220+
providers: [
221+
// ... other providers
222+
provideFastSVG({
223+
url: (name: string) => `${name}.svg`,
224+
svgLoadStrategy: LazyConfigSvgLoadStrategy,
225+
})
226+
]
227+
});
228+
```
229+
199230
## Features
200231

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

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

packages/ngx-fast-icon-demo/src/app/app.component.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Component, inject, PLATFORM_ID } from '@angular/core';
2-
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
2+
import { NavigationEnd, Router, RouterOutlet } from '@angular/router';
33

44
import { filter, map, Observable, startWith } from 'rxjs';
5-
import {MediaMatcher} from '@angular/cdk/layout';
6-
import {ShellComponent} from './misc/shell.component';
7-
import {AsyncPipe, isPlatformServer} from '@angular/common';
5+
import { MediaMatcher } from '@angular/cdk/layout';
6+
import { ShellComponent } from './misc/shell.component';
7+
import { AsyncPipe, isPlatformServer } from '@angular/common';
88

99
@Component({
1010
selector: 'ngx-fast-icon-root',

packages/ngx-fast-icon-demo/src/app/app.config.server.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import { join } from 'node:path';
2-
import { readFileSync } from 'node:fs';
2+
import { readFile } from 'node:fs/promises';
3+
import { cwd } from 'node:process';
34

4-
import { mergeApplicationConfig, ApplicationConfig, Injectable } from '@angular/core';
5+
import { ApplicationConfig, Injectable, mergeApplicationConfig } from '@angular/core';
56
import { provideServerRendering } from '@angular/platform-server';
67

7-
import { Observable, of } from 'rxjs';
8+
import { from, Observable, of, switchMap } from 'rxjs';
89

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

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

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

@@ -24,7 +26,7 @@ const serverConfig: ApplicationConfig = {
2426
provideServerRendering(),
2527
provideFastSVG({
2628
svgLoadStrategy: SvgLoadStrategySsr,
27-
url: (name: string) => `assets/svg-icons/${name}.svg`,
29+
url: (name: string) => `${name}.svg`,
2830
}),
2931
],
3032
};

packages/ngx-fast-lib/README.md

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ This library covers next aspects that developers should consider for their proje
1313
- SVG reusability
1414
- Optimized bundle size
1515
- SSR
16+
- Edge ready (only edge safe APIs are used)
1617

1718
## Getting started
1819

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

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

107109
```typescript
108110
@Injectable()
109111
export abstract class SvgLoadStrategy {
110-
abstract load(url: string): Observable<string>;
112+
abstract config(url: string): Observable<string>;
113+
abstract load(url: Observable<string>): Observable<string>;
111114
}
112115
```
113116

@@ -164,10 +167,11 @@ You can provide your own SSR loading strategy that can look like this:
164167
```typescript
165168
@Injectable()
166169
export class SvgLoadStrategySsr implements SvgLoadStrategy {
167-
load(url: string): Observable<string> {
168-
const iconPath = join(process.cwd(), 'dist', 'app-name', 'browser', url);
169-
const iconSVG = readFileSync(iconPath, 'utf8');
170-
return of(iconSVG);
170+
config(url: string) {
171+
return of(join(cwd(), 'path', 'to', 'svg', 'assets', url));
172+
}
173+
load(iconPath$: Observable<string>) {
174+
return iconPath$.pipe(switchMap((iconPath) => from(readFile(iconPath, { encoding: 'utf8' }))));
171175
}
172176
}
173177
```
@@ -187,14 +191,44 @@ And then just provide it in you server module.
187191
providers: [
188192
provideFastSVG({
189193
svgLoadStrategy: SvgLoadStrategySsr,
190-
url: (name: string) => `assets/svg-icons/${name}.svg`,
194+
url: (name: string) => `${name}.svg`,
191195
}),
192196
],
193197
bootstrap: [AppComponent],
194198
})
195199
export class AppServerModule {}
196200
```
197201

202+
#### Providing a lazy configuration
203+
204+
If you need to provide a lazy configuration you can use the config method in the `SvgLoadStrategy`:
205+
206+
```typescript
207+
@Injectable()
208+
class LazyConfigSvgLoadStrategy extends SvgLoadStrategyImpl {
209+
dummyLazyConfig$ = timer(5_000).pipe(map(() => 'assets/svg-icons'))
210+
override config(url: string): Observable<string> {
211+
return this.dummyLazyConfig$.pipe(map((svgConfig) => `${svgConfig}/${url}`));
212+
}
213+
}
214+
```
215+
216+
And pass it to the provider function:
217+
218+
```typescript
219+
import { provideFastSVG } from '@push-based/ngx-fast-svg';
220+
221+
bootstrapApplication(AppComponent, {
222+
providers: [
223+
// ... other providers
224+
provideFastSVG({
225+
url: (name: string) => `${name}.svg`,
226+
svgLoadStrategy: LazyConfigSvgLoadStrategy,
227+
})
228+
]
229+
});
230+
```
231+
198232
## Features
199233

200234
### :sloth: Lazy loading for SVGs

packages/ngx-fast-lib/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
export * from './lib/token/svg-options.model';
33
export * from './lib/token/svg-options.token';
44
export * from './lib/token/svg-load.strategy.model';
5-
export * from './lib/token/svg-load.strategy';
5+
export { SvgLoadStrategyImpl } from './lib/token/svg-load.strategy';
66
// service
77
export * from './lib/svg-registry.service';
88
// component

packages/ngx-fast-lib/src/lib/fast-svg.component.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,19 @@ import {
44
AfterViewInit,
55
ChangeDetectionStrategy,
66
Component,
7+
effect,
78
ElementRef,
9+
inject,
810
Injector,
11+
input,
912
OnDestroy,
1013
PLATFORM_ID,
11-
Renderer2,
12-
effect,
13-
inject,
14-
input,
15-
untracked,
14+
Renderer2
1615
} from '@angular/core';
1716
import { getZoneUnPatchedApi } from './internal/get-zone-unpatched-api';
1817
import { SvgRegistry } from './svg-registry.service';
18+
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
19+
import { of, switchMap } from 'rxjs';
1920

2021
/**
2122
* getZoneUnPatchedApi
@@ -110,6 +111,10 @@ export class FastSvgComponent implements AfterViewInit, OnDestroy {
110111
width = input<string>('');
111112
height = input<string>('');
112113

114+
#url = toSignal(toObservable(this.name).pipe(switchMap((name) => {
115+
return this.registry.url(name);
116+
})))
117+
113118
// When the browser loaded the svg resource we trigger the caching mechanism
114119
// re-fetch -> cache-hit -> get SVG -> cache in DOM
115120
loadedListener = () => {
@@ -142,7 +147,6 @@ export class FastSvgComponent implements AfterViewInit, OnDestroy {
142147
(onCleanup) => {
143148
const name = this.name();
144149

145-
untracked(() => {
146150
if (!name) {
147151
throw new Error('svg component needs a name to operate');
148152
}
@@ -153,32 +157,35 @@ export class FastSvgComponent implements AfterViewInit, OnDestroy {
153157
if (!this.registry.isSvgCached(name)) {
154158
/**
155159
CSR - Browser native lazy loading hack
156-
160+
157161
We use an img element here to leverage the browsers native features:
158162
- lazy loading (loading="lazy") to only load the svg that are actually visible
159163
- priority hints to down prioritize the fetch to avoid delaying the LCP
160-
164+
161165
While the SVG is loading we display a fallback SVG.
162166
After the image is loaded we remove it from the DOM. (IMG load event)
163167
When the new svg arrives we append it to the template.
164-
168+
165169
Note:
166170
- the image is styled with display none. this prevents any loading of the resource ever.
167171
on component bootstrap we decide what we want to do. when we remove display none it performs the browser native behavior
168-
- the image has 0 height and with and containment as well as contnet-visibility to reduce any performance impact
169-
170-
172+
- the image has 0 height and with and containment as well as content-visibility to reduce any performance impact
173+
174+
171175
Edge cases:
172176
- 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)
173177
- already loaded resources will get emitted from the cache immediately, even if loading is set to lazy :o
174178
- the image needs to have display other than none
175179
*/
176-
const i = this.getImg(this.registry.url(name));
177-
this.renderer.appendChild(this.element.nativeElement, i);
178-
179-
// get img
180-
img = elem.querySelector('img') as HTMLImageElement;
181-
addEventListener(img, 'load', this.loadedListener);
180+
const url = this.#url();
181+
if (url) {
182+
const i = this.getImg(url);
183+
this.renderer.appendChild(this.element.nativeElement, i);
184+
185+
// get img
186+
img = elem.querySelector('img') as HTMLImageElement;
187+
addEventListener(img, 'load', this.loadedListener);
188+
}
182189
}
183190

184191
// Listen to svg changes
@@ -225,7 +232,6 @@ export class FastSvgComponent implements AfterViewInit, OnDestroy {
225232
img.removeEventListener('load', this.loadedListener);
226233
}
227234
});
228-
});
229235
},
230236
{ injector: this.injector }
231237
);

packages/ngx-fast-lib/src/lib/svg-registry.service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { BehaviorSubject, map, Observable } from 'rxjs';
44
import { SvgOptionsToken } from './token/svg-options.token';
55
import { suspenseSvg } from './token/default-token-values';
66
import { SvgLoadStrategy } from './token/svg-load.strategy.model';
7-
import { SvgLoadStrategyImpl } from "./token/svg-load.strategy";
7+
import { SvgLoadStrategyImpl } from './token/svg-load.strategy';
88

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

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

72-
public url = this.svgOptions.url;
72+
public url = (name: string) => this.svgLoadStrategy.config(this.svgOptions.url(name));
7373

7474
constructor() {
7575
// configure suspense svg
@@ -108,7 +108,7 @@ export class SvgRegistry {
108108

109109
// trigger fetch
110110
this.svgLoadStrategy
111-
.load(this.svgOptions.url(svgName))
111+
.load(this.url(svgName))
112112
.subscribe({
113113
next: (body: string) => this.cacheSvgInDOM(svgId, body),
114114
error: console.error

packages/ngx-fast-lib/src/lib/token/svg-load.strategy.model.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ import { Injectable } from '@angular/core';
33

44
@Injectable()
55
export abstract class SvgLoadStrategy {
6-
abstract load(url: string): Observable<string>;
6+
abstract config(url: string): Observable<string>;
7+
abstract load(url: Observable<string>): Observable<string>;
78
}
Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
import { from, Observable } from 'rxjs';
1+
import { from, Observable, of, switchMap } from 'rxjs';
22
import { getZoneUnPatchedApi } from '../internal/get-zone-unpatched-api';
3-
import { SvgLoadStrategy } from "./svg-load.strategy.model";
3+
import { SvgLoadStrategy } from './svg-load.strategy.model';
4+
import { Injectable } from '@angular/core';
45

5-
export class SvgLoadStrategyImpl extends SvgLoadStrategy {
6+
@Injectable()
7+
export class SvgLoadStrategyImpl implements SvgLoadStrategy {
68
fetch = getZoneUnPatchedApi('fetch', window as any);
79

8-
load(url: string): Observable<string> {
9-
return from(fetch(url).then((res) => (!res.ok ? '' : res.text())));
10+
load(url$: Observable<string>): Observable<string> {
11+
return url$.pipe(switchMap((url) => {
12+
return from(fetch(url).then((res) => (!res.ok ? '' : res.text())));
13+
}));
14+
}
15+
16+
config(url: string) {
17+
return of(url);
1018
}
1119
}

0 commit comments

Comments
 (0)