Skip to content

Commit 3212411

Browse files
crisbetotinayuangao
authored andcommitted
fix(icon): handle icons as <symbol> nodes (#4699)
* fix(icon): handle icons as <symbol> nodes Fixes icons not being rendered if they're defined as a `<symbol>` inside the source file. Fixes #4680. * chore: fix IE issues
1 parent fa0e914 commit 3212411

File tree

3 files changed

+64
-8
lines changed

3 files changed

+64
-8
lines changed

src/lib/icon/fake-svgs.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ const FAKE_SVGS = (() => {
3030
</svg>
3131
`);
3232

33+
svgs.set('farm-set-3.svg', `
34+
<svg>
35+
<symbol id="duck">
36+
<path id="quack"></path>
37+
</symbol>
38+
</svg>
39+
`);
40+
3341
svgs.set('arrow-set.svg', `
3442
<svg>
3543
<defs>

src/lib/icon/icon-registry.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,14 +327,24 @@ export class MdIconRegistry {
327327
*/
328328
private _extractSvgIconFromSet(iconSet: SVGElement, iconName: string): SVGElement {
329329
const iconNode = iconSet.querySelector('#' + iconName);
330+
330331
if (!iconNode) {
331332
return null;
332333
}
334+
333335
// If the icon node is itself an <svg> node, clone and return it directly. If not, set it as
334336
// the content of a new <svg> node.
335-
if (iconNode.tagName.toLowerCase() == 'svg') {
337+
if (iconNode.tagName.toLowerCase() === 'svg') {
336338
return this._setSvgAttributes(iconNode.cloneNode(true) as SVGElement);
337339
}
340+
341+
// If the node is a <symbol>, it won't be rendered so we have to convert it into <svg>. Note
342+
// that the same could be achieved by referring to it via <use href="#id">, however the <use>
343+
// tag is problematic on Firefox, because it needs to include the current page path.
344+
if (iconNode.nodeName.toLowerCase() === 'symbol') {
345+
return this._setSvgAttributes(this._toSvgElement(iconNode));
346+
}
347+
338348
// createElement('SVG') doesn't work as expected; the DOM ends up with
339349
// the correct nodes, but the SVG content doesn't render. Instead we
340350
// have to create an empty SVG node using innerHTML and append its content.
@@ -343,6 +353,7 @@ export class MdIconRegistry {
343353
const svg = this._svgElementFromString('<svg></svg>');
344354
// Clone the node so we don't remove it from the parent icon set element.
345355
svg.appendChild(iconNode.cloneNode(true));
356+
346357
return this._setSvgAttributes(svg);
347358
}
348359

@@ -361,6 +372,21 @@ export class MdIconRegistry {
361372
return svg;
362373
}
363374

375+
/**
376+
* Converts an element into an SVG node by cloning all of its children.
377+
*/
378+
private _toSvgElement(element: Element): SVGElement {
379+
let svg = this._svgElementFromString('<svg></svg>');
380+
381+
for (let i = 0; i < element.childNodes.length; i++) {
382+
if (element.childNodes[i].nodeType === Node.ELEMENT_NODE) {
383+
svg.appendChild(element.childNodes[i].cloneNode(true));
384+
}
385+
}
386+
387+
return svg;
388+
}
389+
364390
/**
365391
* Sets the default attributes for an SVG element to be used as an icon.
366392
*/

src/lib/icon/icon.spec.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,28 +10,31 @@ import {wrappedErrorMessage} from '../core/testing/wrapped-error-message';
1010

1111

1212
/** Returns the CSS classes assigned to an element as a sorted array. */
13-
const sortedClassNames = (elem: Element) => elem.className.split(' ').sort();
13+
function sortedClassNames(element: Element): string[] {
14+
return element.className.split(' ').sort();
15+
}
1416

1517
/**
1618
* Verifies that an element contains a single <svg> child element, and returns that child.
1719
*/
18-
const verifyAndGetSingleSvgChild = (element: SVGElement): any => {
20+
function verifyAndGetSingleSvgChild(element: SVGElement): SVGElement {
1921
expect(element.childNodes.length).toBe(1);
20-
const svgChild = <Element>element.childNodes[0];
22+
const svgChild = element.childNodes[0] as SVGElement;
2123
expect(svgChild.tagName.toLowerCase()).toBe('svg');
2224
return svgChild;
23-
};
25+
}
2426

2527
/**
2628
* Verifies that an element contains a single <path> child element whose "id" attribute has
2729
* the specified value.
2830
*/
29-
const verifyPathChildElement = (element: Element, attributeValue: string) => {
31+
function verifyPathChildElement(element: Element, attributeValue: string): void {
3032
expect(element.childNodes.length).toBe(1);
31-
const pathElement = <Element>element.childNodes[0];
33+
const pathElement = element.childNodes[0] as SVGPathElement;
3234
expect(pathElement.tagName.toLowerCase()).toBe('path');
3335
expect(pathElement.getAttribute('id')).toBe(attributeValue);
34-
};
36+
}
37+
3538

3639
describe('MdIcon', () => {
3740

@@ -240,6 +243,25 @@ describe('MdIcon', () => {
240243
expect(httpRequestUrls.sort()).toEqual(['farm-set-1.svg', 'farm-set-2.svg']);
241244
});
242245

246+
it('should unwrap <symbol> nodes', () => {
247+
mdIconRegistry.addSvgIconSetInNamespace('farm', trust('farm-set-3.svg'));
248+
249+
const fixture = TestBed.createComponent(MdIconFromSvgNameTestApp);
250+
const testComponent = fixture.componentInstance;
251+
const mdIconElement = fixture.debugElement.nativeElement.querySelector('md-icon');
252+
253+
testComponent.iconName = 'farm:duck';
254+
fixture.detectChanges();
255+
256+
const svgElement = verifyAndGetSingleSvgChild(mdIconElement);
257+
const firstChild = svgElement.childNodes[0];
258+
259+
expect(svgElement.querySelector('symbol')).toBeFalsy();
260+
expect(svgElement.childNodes.length).toBe(1);
261+
expect(firstChild.nodeName.toLowerCase()).toBe('path');
262+
expect((firstChild as HTMLElement).getAttribute('id')).toBe('quack');
263+
});
264+
243265
it('should not wrap <svg> elements in icon sets in another svg tag', () => {
244266
mdIconRegistry.addSvgIconSet(trust('arrow-set.svg'));
245267

0 commit comments

Comments
 (0)