@@ -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' ;
1517import { getZoneUnPatchedApi } from './internal/get-zone-unpatched-api' ;
1618import { SvgRegistry } from './svg-registry.service' ;
1719
@@ -95,119 +97,141 @@ function createGetImgFn(renderer: Renderer2): (src: string) => HTMLElement {
9597 changeDetection : ChangeDetectionStrategy . OnPush ,
9698} )
9799export 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