diff --git a/src/lib/icon/icon.spec.ts b/src/lib/icon/icon.spec.ts
index 0e265e0ee8ef..748f8ee8b4f1 100644
--- a/src/lib/icon/icon.spec.ts
+++ b/src/lib/icon/icon.spec.ts
@@ -39,8 +39,11 @@ function verifyPathChildElement(element: Element, attributeValue: string): void
describe('MatIcon', () => {
+ let fakePath: string;
beforeEach(async(() => {
+ fakePath = '/fake-path';
+
TestBed.configureTestingModule({
imports: [HttpClientTestingModule, MatIconModule],
declarations: [
@@ -55,7 +58,7 @@ describe('MatIcon', () => {
],
providers: [{
provide: MAT_ICON_LOCATION,
- useValue: {pathname: '/fake-path'}
+ useValue: {getPathname: () => fakePath}
}]
});
@@ -608,6 +611,63 @@ describe('MatIcon', () => {
tick();
}));
+ it('should use latest path when prefixing the `url()` references', fakeAsync(() => {
+ iconRegistry.addSvgIconLiteral('fido', trustHtml(`
+
+ `));
+
+ let fixture = TestBed.createComponent(IconFromSvgName);
+ fixture.componentInstance.iconName = 'fido';
+ fixture.detectChanges();
+ let circle = fixture.nativeElement.querySelector('mat-icon svg circle');
+
+ expect(circle.getAttribute('filter')).toMatch(/^url\(['"]?\/fake-path#blur['"]?\)$/);
+ tick();
+ fixture.destroy();
+
+ fakePath = '/another-fake-path';
+ fixture = TestBed.createComponent(IconFromSvgName);
+ fixture.componentInstance.iconName = 'fido';
+ fixture.detectChanges();
+ circle = fixture.nativeElement.querySelector('mat-icon svg circle');
+
+ expect(circle.getAttribute('filter')).toMatch(/^url\(['"]?\/another-fake-path#blur['"]?\)$/);
+ tick();
+ }));
+
+ it('should update the `url()` references when the path changes', fakeAsync(() => {
+ iconRegistry.addSvgIconLiteral('fido', trustHtml(`
+
+ `));
+
+ const fixture = TestBed.createComponent(IconFromSvgName);
+ fixture.componentInstance.iconName = 'fido';
+ fixture.detectChanges();
+ const circle = fixture.nativeElement.querySelector('mat-icon svg circle');
+
+ // We use a regex to match here, rather than the exact value, because different browsers
+ // return different quotes through `getAttribute`, while some even omit the quotes altogether.
+ expect(circle.getAttribute('filter')).toMatch(/^url\(['"]?\/fake-path#blur['"]?\)$/);
+ tick();
+
+ fakePath = '/different-path';
+ fixture.detectChanges();
+
+ expect(circle.getAttribute('filter')).toMatch(/^url\(['"]?\/different-path#blur['"]?\)$/);
+ }));
+
});
describe('custom fonts', () => {
diff --git a/src/lib/icon/icon.ts b/src/lib/icon/icon.ts
index a091d25191e7..13002a2b557b 100644
--- a/src/lib/icon/icon.ts
+++ b/src/lib/icon/icon.ts
@@ -21,6 +21,8 @@ import {
InjectionToken,
inject,
Inject,
+ OnDestroy,
+ AfterViewChecked,
} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {CanColor, CanColorCtor, mixinColor} from '@angular/material/core';
@@ -51,14 +53,18 @@ export const MAT_ICON_LOCATION = new InjectionToken('mat-icon-l
* @docs-private
*/
export interface MatIconLocation {
- pathname: string;
+ getPathname: () => string;
}
/** @docs-private */
export function MAT_ICON_LOCATION_FACTORY(): MatIconLocation {
const _document = inject(DOCUMENT);
- const pathname = (_document && _document.location && _document.location.pathname) || '';
- return {pathname};
+
+ return {
+ // Note that this needs to be a function, rather than a property, because Angular
+ // will only resolve it once, but we want the current path on each call.
+ getPathname: () => (_document && _document.location && _document.location.pathname) || ''
+ };
}
@@ -126,7 +132,9 @@ const funcIriPattern = /^url\(['"]?#(.*?)['"]?\)$/;
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, CanColor {
+export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, AfterViewChecked,
+ CanColor, OnDestroy {
+
/**
* Whether the icon should be inlined, automatically sizing the icon to match the font size of
* the element the icon is contained in.
@@ -162,6 +170,12 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Can
private _previousFontSetClass: string;
private _previousFontIconClass: string;
+ /** Keeps track of the current page path. */
+ private _previousPath?: string;
+
+ /** Keeps track of the elements and attributes that we've prefixed with the current path. */
+ private _elementsWithExternalReferences?: Map;
+
constructor(
elementRef: ElementRef,
private _iconRegistry: MatIconRegistry,
@@ -233,6 +247,31 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Can
}
}
+ ngAfterViewChecked() {
+ const cachedElements = this._elementsWithExternalReferences;
+
+ if (cachedElements && this._location && cachedElements.size) {
+ const newPath = this._location.getPathname();
+
+ // We need to check whether the URL has changed on each change detection since
+ // the browser doesn't have an API that will let us react on link clicks and
+ // we can't depend on the Angular router. The references need to be updated,
+ // because while most browsers don't care whether the URL is correct after
+ // the first render, Safari will break if the user navigates to a different
+ // page and the SVG isn't re-rendered.
+ if (newPath !== this._previousPath) {
+ this._previousPath = newPath;
+ this._prependPathToReferences(newPath);
+ }
+ }
+ }
+
+ ngOnDestroy() {
+ if (this._elementsWithExternalReferences) {
+ this._elementsWithExternalReferences.clear();
+ }
+ }
+
private _usingFontIcon(): boolean {
return !this.svgIcon;
}
@@ -251,7 +290,13 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Can
// Note: we do this fix here, rather than the icon registry, because the
// references have to point to the URL at the time that the icon was created.
- this._prependCurrentPathToReferences(svg);
+ if (this._location) {
+ const path = this._location.getPathname();
+ this._previousPath = path;
+ this._cacheChildrenWithExternalReferences(svg);
+ this._prependPathToReferences(path);
+ }
+
this._elementRef.nativeElement.appendChild(svg);
}
@@ -259,6 +304,10 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Can
const layoutElement: HTMLElement = this._elementRef.nativeElement;
let childCount = layoutElement.childNodes.length;
+ if (this._elementsWithExternalReferences) {
+ this._elementsWithExternalReferences.clear();
+ }
+
// Remove existing non-element child nodes and SVGs, and add the new SVG element. Note that
// we can't use innerHTML, because IE will throw if the element has a data binding.
while (childCount--) {
@@ -317,24 +366,42 @@ export class MatIcon extends _MatIconMixinBase implements OnChanges, OnInit, Can
* reference. This is required because WebKit browsers require references to be prefixed with
* the current path, if the page has a `base` tag.
*/
- private _prependCurrentPathToReferences(element: SVGElement) {
- // @breaking-change 8.0.0 Remove this null check once `_location` parameter is required.
- if (!this._location) {
- return;
+ private _prependPathToReferences(path: string) {
+ const elements = this._elementsWithExternalReferences;
+
+ if (elements) {
+ elements.forEach((attrs, element) => {
+ attrs.forEach(attr => {
+ element.setAttribute(attr.name, `url('${path}#${attr.value}')`);
+ });
+ });
}
+ }
+ /**
+ * Caches the children of an SVG element that have `url()`
+ * references that we need to prefix with the current path.
+ */
+ private _cacheChildrenWithExternalReferences(element: SVGElement) {
const elementsWithFuncIri = element.querySelectorAll(funcIriAttributeSelector);
- const path = this._location.pathname ? this._location.pathname.split('#')[0] : '';
+ const elements = this._elementsWithExternalReferences =
+ this._elementsWithExternalReferences || new Map();
for (let i = 0; i < elementsWithFuncIri.length; i++) {
funcIriAttributes.forEach(attr => {
- const value = elementsWithFuncIri[i].getAttribute(attr);
+ const elementWithReference = elementsWithFuncIri[i];
+ const value = elementWithReference.getAttribute(attr);
const match = value ? value.match(funcIriPattern) : null;
if (match) {
- // Note the quotes inside the `url()`. They're important, because URLs pointing to named
- // router outlets can contain parentheses which will break if they aren't quoted.
- elementsWithFuncIri[i].setAttribute(attr, `url('${path}#${match[1]}')`);
+ let attributes = elements.get(elementWithReference);
+
+ if (!attributes) {
+ attributes = [];
+ elements.set(elementWithReference, attributes);
+ }
+
+ attributes!.push({name: attr, value: match[1]});
}
});
}