@@ -21,6 +21,8 @@ import {
2121 InjectionToken ,
2222 inject ,
2323 Inject ,
24+ OnDestroy ,
25+ AfterViewChecked ,
2426} from '@angular/core' ;
2527import { DOCUMENT } from '@angular/common' ;
2628import { CanColor , CanColorCtor , mixinColor } from '@angular/material/core' ;
@@ -51,14 +53,18 @@ export const MAT_ICON_LOCATION = new InjectionToken<MatIconLocation>('mat-icon-l
5153 * @docs -private
5254 */
5355export interface MatIconLocation {
54- pathname : string ;
56+ getPathname : ( ) => string ;
5557}
5658
5759/** @docs -private */
5860export function MAT_ICON_LOCATION_FACTORY ( ) : MatIconLocation {
5961 const _document = inject ( DOCUMENT ) ;
60- const pathname = ( _document && _document . location && _document . location . pathname ) || '' ;
61- return { pathname} ;
62+
63+ return {
64+ // Note that this needs to be a function, rather than a property, because Angular
65+ // will only resolve it once, but we want the current path on each call.
66+ getPathname : ( ) => ( _document && _document . location && _document . location . pathname ) || ''
67+ } ;
6268}
6369
6470
@@ -126,7 +132,9 @@ const funcIriPattern = /^url\(['"]?#(.*?)['"]?\)$/;
126132 encapsulation : ViewEncapsulation . None ,
127133 changeDetection : ChangeDetectionStrategy . OnPush ,
128134} )
129- export class MatIcon extends _MatIconMixinBase implements OnChanges , OnInit , CanColor {
135+ export class MatIcon extends _MatIconMixinBase implements OnChanges , OnInit , AfterViewChecked ,
136+ CanColor , OnDestroy {
137+
130138 /**
131139 * Whether the icon should be inlined, automatically sizing the icon to match the font size of
132140 * the element the icon is contained in.
@@ -162,6 +170,12 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Can
162170 private _previousFontSetClass : string ;
163171 private _previousFontIconClass : string ;
164172
173+ /** Keeps track of the current page path. */
174+ private _previousPath ?: string ;
175+
176+ /** Keeps track of the elements and attributes that we've prefixed with the current path. */
177+ private _elementsWithExternalReferences ?: Map < Element , { name : string , value : string } [ ] > ;
178+
165179 constructor (
166180 elementRef : ElementRef < HTMLElement > ,
167181 private _iconRegistry : MatIconRegistry ,
@@ -233,6 +247,31 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Can
233247 }
234248 }
235249
250+ ngAfterViewChecked ( ) {
251+ const cachedElements = this . _elementsWithExternalReferences ;
252+
253+ if ( cachedElements && this . _location && cachedElements . size ) {
254+ const newPath = this . _location . getPathname ( ) ;
255+
256+ // We need to check whether the URL has changed on each change detection since
257+ // the browser doesn't have an API that will let us react on link clicks and
258+ // we can't depend on the Angular router. The references need to be updated,
259+ // because while most browsers don't care whether the URL is correct after
260+ // the first render, Safari will break if the user navigates to a different
261+ // page and the SVG isn't re-rendered.
262+ if ( newPath !== this . _previousPath ) {
263+ this . _previousPath = newPath ;
264+ this . _prependPathToReferences ( newPath ) ;
265+ }
266+ }
267+ }
268+
269+ ngOnDestroy ( ) {
270+ if ( this . _elementsWithExternalReferences ) {
271+ this . _elementsWithExternalReferences . clear ( ) ;
272+ }
273+ }
274+
236275 private _usingFontIcon ( ) : boolean {
237276 return ! this . svgIcon ;
238277 }
@@ -251,14 +290,24 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Can
251290
252291 // Note: we do this fix here, rather than the icon registry, because the
253292 // references have to point to the URL at the time that the icon was created.
254- this . _prependCurrentPathToReferences ( svg ) ;
293+ if ( this . _location ) {
294+ const path = this . _location . getPathname ( ) ;
295+ this . _previousPath = path ;
296+ this . _cacheChildrenWithExternalReferences ( svg ) ;
297+ this . _prependPathToReferences ( path ) ;
298+ }
299+
255300 this . _elementRef . nativeElement . appendChild ( svg ) ;
256301 }
257302
258303 private _clearSvgElement ( ) {
259304 const layoutElement : HTMLElement = this . _elementRef . nativeElement ;
260305 let childCount = layoutElement . childNodes . length ;
261306
307+ if ( this . _elementsWithExternalReferences ) {
308+ this . _elementsWithExternalReferences . clear ( ) ;
309+ }
310+
262311 // Remove existing non-element child nodes and SVGs, and add the new SVG element. Note that
263312 // we can't use innerHTML, because IE will throw if the element has a data binding.
264313 while ( childCount -- ) {
@@ -317,24 +366,42 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Can
317366 * reference. This is required because WebKit browsers require references to be prefixed with
318367 * the current path, if the page has a `base` tag.
319368 */
320- private _prependCurrentPathToReferences ( element : SVGElement ) {
321- // @breaking -change 8.0.0 Remove this null check once `_location` parameter is required.
322- if ( ! this . _location ) {
323- return ;
369+ private _prependPathToReferences ( path : string ) {
370+ const elements = this . _elementsWithExternalReferences ;
371+
372+ if ( elements ) {
373+ elements . forEach ( ( attrs , element ) => {
374+ attrs . forEach ( attr => {
375+ element . setAttribute ( attr . name , `url('${ path } #${ attr . value } ')` ) ;
376+ } ) ;
377+ } ) ;
324378 }
379+ }
325380
381+ /**
382+ * Caches the children of an SVG element that have `url()`
383+ * references that we need to prefix with the current path.
384+ */
385+ private _cacheChildrenWithExternalReferences ( element : SVGElement ) {
326386 const elementsWithFuncIri = element . querySelectorAll ( funcIriAttributeSelector ) ;
327- const path = this . _location . pathname ? this . _location . pathname . split ( '#' ) [ 0 ] : '' ;
387+ const elements = this . _elementsWithExternalReferences =
388+ this . _elementsWithExternalReferences || new Map ( ) ;
328389
329390 for ( let i = 0 ; i < elementsWithFuncIri . length ; i ++ ) {
330391 funcIriAttributes . forEach ( attr => {
331- const value = elementsWithFuncIri [ i ] . getAttribute ( attr ) ;
392+ const elementWithReference = elementsWithFuncIri [ i ] ;
393+ const value = elementWithReference . getAttribute ( attr ) ;
332394 const match = value ? value . match ( funcIriPattern ) : null ;
333395
334396 if ( match ) {
335- // Note the quotes inside the `url()`. They're important, because URLs pointing to named
336- // router outlets can contain parentheses which will break if they aren't quoted.
337- elementsWithFuncIri [ i ] . setAttribute ( attr , `url('${ path } #${ match [ 1 ] } ')` ) ;
397+ let attributes = elements . get ( elementWithReference ) ;
398+
399+ if ( ! attributes ) {
400+ attributes = [ ] ;
401+ elements . set ( elementWithReference , attributes ) ;
402+ }
403+
404+ attributes ! . push ( { name : attr , value : match [ 1 ] } ) ;
338405 }
339406 } ) ;
340407 }
0 commit comments