Skip to content

Commit cb5f6b9

Browse files
authored
fix: make dynamic name work (#74)
1 parent 41db70c commit cb5f6b9

File tree

3 files changed

+131
-96
lines changed

3 files changed

+131
-96
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
ViewEncapsulation,
5+
} from '@angular/core';
26
import { AsyncPipe } from '@angular/common';
37

48
import { FastSvgComponent } from '@push-based/ngx-fast-svg';
59

610
import { ControllerComponent } from '../misc/controller.component';
7-
import { IconTester } from '../misc/icon-tester.service';
811
import { SUPPORTED_ICONS } from '../misc/icon-data';
912
import { BaseDemoComponent } from '../misc/base-demo.component';
1013
import { DEMO_ROUTE } from '../misc/constants';
@@ -13,12 +16,13 @@ import { DEMO_ROUTE } from '../misc/constants';
1316
standalone: true,
1417
template: `
1518
<app-controller [demoLib]='"Fast svg"' [tester]='tester' />
19+
<button (click)='changeSort()'>Reload</button>
1620
<div class='row icons' [class]='layout()'>
1721
@for (list of countArr(); track $index) {
1822
<ul class='group'>
19-
@for (icon of tester.icons; track $index) {
23+
@for (icon of tester.icons; track icon) {
2024
<li>
21-
<fast-svg [name]='$any(icon)' />
25+
<fast-svg [name]='$any(icon)' [size]="size" />
2226
</li>
2327
}
2428
</ul>
@@ -35,4 +39,11 @@ export class FastIconRouteComponent extends BaseDemoComponent {
3539
this.tester.activeDemo.set(DEMO_ROUTE.FAST_SVG);
3640
this.tester.defineSet(SUPPORTED_ICONS);
3741
}
42+
43+
size = '24';
44+
45+
changeSort() {
46+
this.tester.defineSet(SUPPORTED_ICONS.sort(() => Math.random() - 0.5));
47+
this.size = this.size === '24' ? '32' : '24';
48+
}
3849
}

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

Lines changed: 115 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import {
55
ChangeDetectionStrategy,
66
Component,
77
ElementRef,
8-
Input,
8+
Injector,
99
OnDestroy,
1010
PLATFORM_ID,
1111
Renderer2,
12+
effect,
1213
inject,
14+
input,
15+
untracked,
1316
} from '@angular/core';
14-
import { Subscription } from 'rxjs';
1517
import { getZoneUnPatchedApi } from './internal/get-zone-unpatched-api';
1618
import { SvgRegistry } from './svg-registry.service';
1719

@@ -95,119 +97,141 @@ function createGetImgFn(renderer: Renderer2): (src: string) => HTMLElement {
9597
changeDetection: ChangeDetectionStrategy.OnPush,
9698
})
9799
export class FastSvgComponent implements AfterViewInit, OnDestroy {
100+
private readonly injector = inject(Injector);
98101
private readonly platform = inject(PLATFORM_ID);
99102
private readonly renderer = inject(Renderer2);
100103
private readonly registry = inject(SvgRegistry);
101104
private readonly element = inject<ElementRef<HTMLElement>>(ElementRef);
102105

103-
private readonly sub = new Subscription();
104106
private readonly getImg = createGetImgFn(this.renderer);
105107

106-
@Input() name = '';
107-
@Input() size: string = this.registry.defaultSize;
108-
@Input() width = '';
109-
@Input() height = '';
108+
name = input<string>('');
109+
size = input<string>(this.registry.defaultSize);
110+
width = input<string>('');
111+
height = input<string>('');
110112

111113
// When the browser loaded the svg resource we trigger the caching mechanism
112114
// re-fetch -> cache-hit -> get SVG -> cache in DOM
113115
loadedListener = () => {
114-
this.registry.fetchSvg(this.name);
116+
this.registry.fetchSvg(this.name());
115117
};
116118

117119
ngAfterViewInit() {
118-
if (!this.name) {
119-
throw new Error('svg component needs a name to operate');
120-
}
121-
122120
// Setup view refs and init them
123121
const elem = this.element.nativeElement;
124122

125123
const svg = elem.querySelector('svg') as SVGElement;
126-
// apply size
127-
if (this.size && svg) {
128-
// We apply fixed dimensions
129-
// Additionally to SEO rules, to avoid any scroll flicker caused by `content-visibility:auto` defined in component styles
130-
svg.setAttribute('width', this.width || this.size);
131-
svg.setAttribute('height', this.height || this.width || this.size);
132-
}
133-
134-
let img: HTMLImageElement | null = null;
135-
136-
// if svg is not in cache we append
137-
if (!this.registry.isSvgCached(this.name)) {
138-
/**
139-
CSR - Browser native lazy loading hack
140-
141-
We use an img element here to leverage the browsers native features:
142-
- lazy loading (loading="lazy") to only load the svg that are actually visible
143-
- priority hints to down prioritize the fetch to avoid delaying the LCP
144-
145-
While the SVG is loading we display a fallback SVG.
146-
After the image is loaded we remove it from the DOM. (IMG load event)
147-
When the new svg arrives we append it to the template.
148-
149-
Note:
150-
- the image is styled with display none. this prevents any loading of the resource ever.
151-
on component bootstrap we decide what we want to do. when we remove display none it performs the browser native behavior
152-
- the image has 0 height and with and containment as well as contnet-visibility to recuce any performance impact
153-
154-
155-
Edge cases:
156-
- 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)
157-
- already loaded resources will get emitted from the cache immediately, even if loading is set to lazy :o
158-
- the image needs to have display other than none
159-
160-
*/
161-
const i = this.getImg(this.registry.url(this.name));
162-
this.renderer.appendChild(this.element.nativeElement, i);
163-
164-
// get img
165-
img = elem.querySelector('img') as HTMLImageElement;
166-
addEventListener(img, 'load', this.loadedListener);
167-
}
168-
169-
// Listen to svg changes
170-
// This potentially could already receive the svg from the cache and drop the img from the DOM before it gets activated for lazy loading.
171-
// NOTICE:
172-
// If the svg is already cached the following code will execute synchronously. This gives us the chance to add
173-
this.sub.add(
174-
this.registry.svgCache$(this.name).subscribe((cache) => {
175-
// The first child is the `use` tag. The value of href gets displayed as SVG
176-
svg.children[0].setAttribute('href', cache.name);
177-
svg.setAttribute('viewBox', cache.viewBox);
178-
179-
// early exvit no image
180-
if (!img) return;
181-
182-
// If the img is present
183-
// and the name in included in the href (svg is fully loaded, not only the suspense svg)
184-
// Remove the element from the DOM as it is no longer needed
185-
if (cache.name.includes(this.name)) {
186-
img.removeEventListener('load', this.loadedListener);
187-
// removeEventListener.bind(img, 'load', this.loadedListener);
188-
img.remove();
124+
125+
effect(
126+
() => {
127+
// apply size
128+
if (this.size() && svg) {
129+
// We apply fixed dimensions
130+
// Additionally to SEO rules, to avoid any scroll flicker caused by `content-visibility:auto` defined in component styles
131+
svg.setAttribute('width', this.width() || this.size());
132+
svg.setAttribute(
133+
'height',
134+
this.height() || this.width() || this.size()
135+
);
189136
}
190-
})
137+
},
138+
{ injector: this.injector }
191139
);
192140

193-
// SSR
194-
if (isPlatformServer(this.platform)) {
195-
// if SSR load svgs on server => ends up in DOM cache and ships to the client
196-
this.registry.fetchSvg(this.name);
197-
}
198-
// CSR
199-
else {
200-
// Activate the lazy loading hack
201-
// Loading is triggered in the template over loading="lazy" and onload
202-
// Than the same image is fetched over fromFetch and rendered as SVG. (This will result in a cache hit for this svg)
203-
//
204-
// If the img is present activate it
205-
img && img.style.setProperty('display', 'block');
206-
}
141+
effect(
142+
(onCleanup) => {
143+
const name = this.name();
144+
145+
untracked(() => {
146+
if (!name) {
147+
throw new Error('svg component needs a name to operate');
148+
}
149+
150+
let img: HTMLImageElement | null = null;
151+
152+
// if svg is not in cache we append
153+
if (!this.registry.isSvgCached(name)) {
154+
/**
155+
CSR - Browser native lazy loading hack
156+
157+
We use an img element here to leverage the browsers native features:
158+
- lazy loading (loading="lazy") to only load the svg that are actually visible
159+
- priority hints to down prioritize the fetch to avoid delaying the LCP
160+
161+
While the SVG is loading we display a fallback SVG.
162+
After the image is loaded we remove it from the DOM. (IMG load event)
163+
When the new svg arrives we append it to the template.
164+
165+
Note:
166+
- the image is styled with display none. this prevents any loading of the resource ever.
167+
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+
171+
Edge cases:
172+
- 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)
173+
- already loaded resources will get emitted from the cache immediately, even if loading is set to lazy :o
174+
- the image needs to have display other than none
175+
*/
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);
182+
}
183+
184+
// Listen to svg changes
185+
// This potentially could already receive the svg from the cache and drop the img from the DOM before it gets activated for lazy loading.
186+
// NOTICE:
187+
// If the svg is already cached the following code will execute synchronously. This gives us the chance to add
188+
const sub = this.registry.svgCache$(name).subscribe((cache) => {
189+
// The first child is the `use` tag. The value of href gets displayed as SVG
190+
svg.children[0].setAttribute('href', cache.name);
191+
svg.setAttribute('viewBox', cache.viewBox);
192+
193+
// early exit no image
194+
if (!img) return;
195+
196+
// If the img is present
197+
// and the name in included in the href (svg is fully loaded, not only the suspense svg)
198+
// Remove the element from the DOM as it is no longer needed
199+
if (cache.name.includes(name)) {
200+
img.removeEventListener('load', this.loadedListener);
201+
// removeEventListener.bind(img, 'load', this.loadedListener);
202+
img.remove();
203+
}
204+
});
205+
206+
// SSR
207+
if (isPlatformServer(this.platform)) {
208+
// if SSR load svgs on server => ends up in DOM cache and ships to the client
209+
this.registry.fetchSvg(name);
210+
}
211+
// CSR
212+
else {
213+
// Activate the lazy loading hack
214+
// Loading is triggered in the template over loading="lazy" and onload
215+
// Than the same image is fetched over fromFetch and rendered as SVG. (This will result in a cache hit for this svg)
216+
//
217+
// If the img is present activate it
218+
img && img.style.setProperty('display', 'block');
219+
}
220+
221+
onCleanup(() => {
222+
sub.unsubscribe();
223+
224+
if (img) {
225+
img.removeEventListener('load', this.loadedListener);
226+
}
227+
});
228+
});
229+
},
230+
{ injector: this.injector }
231+
);
207232
}
208233

209234
ngOnDestroy() {
210-
this.sub.unsubscribe();
211235
this.element.nativeElement
212236
.querySelector('img')
213237
?.removeEventListener('load', this.loadedListener);

0 commit comments

Comments
 (0)