2
0
mirror of https://github.com/iconify/iconify.git synced 2025-01-22 22:58:27 +00:00

fix: better way to detect previously rendered child node in web component

This commit is contained in:
Vjacheslav Trushkin 2022-10-08 13:38:53 +03:00
parent feefd180bc
commit e82c905ec8
7 changed files with 76 additions and 38 deletions

View File

@ -37,16 +37,22 @@ export function renderIcon(parent: Element | ShadowRoot, state: RenderedState) {
} }
// Set element // Set element
// Assumes first node is a style node created with updateStyle() const oldNode = Array.from(parent.childNodes).find((node) => {
if (parent.childNodes.length > 1) { const tag =
const lastChild = parent.lastChild as HTMLElement; (node as HTMLElement).tagName &&
if (node.tagName === 'SPAN' && lastChild.tagName === node.tagName) { (node as HTMLElement).tagName.toUpperCase();
return tag === 'SPAN' || tag === 'SVG';
}) as HTMLElement | undefined;
if (oldNode) {
// Replace old element
if (node.tagName === 'SPAN' && oldNode.tagName === node.tagName) {
// Swap style instead of whole node // Swap style instead of whole node
lastChild.setAttribute('style', node.getAttribute('style')); oldNode.setAttribute('style', node.getAttribute('style'));
} else { } else {
parent.replaceChild(node, lastChild); parent.replaceChild(node, oldNode);
} }
} else { } else {
// Add new element
parent.appendChild(node); parent.appendChild(node);
} }
} }

View File

@ -1,16 +1,27 @@
/**
* Attribute to add
*/
const nodeAttr = 'data-style';
/** /**
* Add/update style node * Add/update style node
*/ */
export function updateStyle(parent: Element | ShadowRoot, inline: boolean) { export function updateStyle(parent: Element | ShadowRoot, inline: boolean) {
// Get node, create if needed // Get node, create if needed
let style = parent.firstChild; let styleNode = Array.from(parent.childNodes).find(
if (!style) { (node) =>
style = document.createElement('style'); (node as HTMLElement).hasAttribute &&
parent.appendChild(style); (node as HTMLElement).hasAttribute(nodeAttr)
) as HTMLElement | undefined;
if (!styleNode) {
styleNode = document.createElement('style');
styleNode.setAttribute(nodeAttr, nodeAttr);
parent.appendChild(styleNode);
} }
// Update content // Update content
style.textContent = styleNode.textContent =
':host{display:inline-block;vertical-align:' + ':host{display:inline-block;vertical-align:' +
(inline ? '-0.125em' : '0') + (inline ? '-0.125em' : '0') +
'}span,svg{display:block}'; '}span,svg{display:block}';

View File

@ -3,6 +3,13 @@ import { mockAPIModule, mockAPIData } from '@iconify/core/lib/api/modules/mock';
import { addAPIProvider } from '@iconify/core/lib/api/config'; import { addAPIProvider } from '@iconify/core/lib/api/config';
import { setAPIModule } from '@iconify/core/lib/api/modules'; import { setAPIModule } from '@iconify/core/lib/api/modules';
/**
* <style> tag with extra attribute
*
* Attribute is used to allow developers inject custom styles, so there could be multiple style tags
*/
export const styleOpeningTag = '<style data-style="data-style">';
/** /**
* Generate next prefix * Generate next prefix
*/ */

View File

@ -7,6 +7,7 @@ import {
fakeAPI, fakeAPI,
mockAPIData, mockAPIData,
awaitUntil, awaitUntil,
styleOpeningTag,
} from '../src/tests/helpers'; } from '../src/tests/helpers';
import { defineIconifyIcon, IconifyIconHTMLElement } from '../src/component'; import { defineIconifyIcon, IconifyIconHTMLElement } from '../src/component';
import type { IconState } from '../src/state'; import type { IconState } from '../src/state';
@ -44,7 +45,7 @@ describe('Testing icon component with API', () => {
// Should be empty // Should be empty
expect(node._shadowRoot.innerHTML).toBe( expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>` `${styleOpeningTag}${expectedBlock}</style>`
); );
expect(node.status).toBe('loading'); expect(node.status).toBe('loading');
@ -78,7 +79,7 @@ describe('Testing icon component with API', () => {
// Should not have sent query to API yet // Should not have sent query to API yet
expect(sendQuery).toBeUndefined(); expect(sendQuery).toBeUndefined();
expect(node._shadowRoot.innerHTML).toBe( expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>` `${styleOpeningTag}${expectedBlock}</style>`
); );
expect(node.status).toBe('loading'); expect(node.status).toBe('loading');
@ -97,7 +98,7 @@ describe('Testing icon component with API', () => {
const blankSVG = const blankSVG =
'<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16"><g></g></svg>'; '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16"><g></g></svg>';
expect(node._shadowRoot.innerHTML).toBe( expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>${blankSVG}` `${styleOpeningTag}${expectedBlock}</style>${blankSVG}`
); );
expect(node.status).toBe('rendered'); expect(node.status).toBe('rendered');
}); });
@ -121,7 +122,7 @@ describe('Testing icon component with API', () => {
// Should be empty // Should be empty
expect(node._shadowRoot.innerHTML).toBe( expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>` `${styleOpeningTag}${expectedBlock}</style>`
); );
expect(node.status).toBe('loading'); expect(node.status).toBe('loading');
@ -147,7 +148,7 @@ describe('Testing icon component with API', () => {
// Should not have sent query to API yet // Should not have sent query to API yet
expect(node._shadowRoot.innerHTML).toBe( expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>` `${styleOpeningTag}${expectedBlock}</style>`
); );
expect(node.status).toBe('loading'); expect(node.status).toBe('loading');
@ -157,7 +158,7 @@ describe('Testing icon component with API', () => {
// Should fail to render // Should fail to render
expect(node._shadowRoot.innerHTML).toBe( expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>` `${styleOpeningTag}${expectedBlock}</style>`
); );
expect(node.status).toBe('failed'); expect(node.status).toBe('failed');
}); });

View File

@ -4,6 +4,7 @@ import {
expectedInline, expectedInline,
setupDOM, setupDOM,
nextTick, nextTick,
styleOpeningTag,
} from '../src/tests/helpers'; } from '../src/tests/helpers';
import { defineIconifyIcon, IconifyIconHTMLElement } from '../src/component'; import { defineIconifyIcon, IconifyIconHTMLElement } from '../src/component';
import type { IconState } from '../src/state'; import type { IconState } from '../src/state';
@ -73,7 +74,7 @@ describe('Testing icon component', () => {
// Should be empty // Should be empty
expect(node._shadowRoot.innerHTML).toBe( expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>` `${styleOpeningTag}${expectedBlock}</style>`
); );
expect(node.status).toBe('loading'); expect(node.status).toBe('loading');
@ -96,7 +97,7 @@ describe('Testing icon component', () => {
// Should still be empty: waiting for next tick // Should still be empty: waiting for next tick
expect(node._shadowRoot.innerHTML).toBe( expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>` `${styleOpeningTag}${expectedBlock}</style>`
); );
expect(node.status).toBe('loading'); expect(node.status).toBe('loading');
await nextTick(); await nextTick();
@ -105,7 +106,7 @@ describe('Testing icon component', () => {
const blankSVG = const blankSVG =
'<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16"><g></g></svg>'; '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16"><g></g></svg>';
expect(node._shadowRoot.innerHTML).toBe( expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>${blankSVG}` `${styleOpeningTag}${expectedBlock}</style>${blankSVG}`
); );
expect(node.status).toBe('rendered'); expect(node.status).toBe('rendered');
@ -119,7 +120,7 @@ describe('Testing icon component', () => {
expect(node.getAttribute('inline')).toBe('true'); expect(node.getAttribute('inline')).toBe('true');
expect(node._shadowRoot.innerHTML).toBe( expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedInline}</style>${blankSVG}` `${styleOpeningTag}${expectedInline}</style>${blankSVG}`
); );
expect(node.status).toBe('rendered'); expect(node.status).toBe('rendered');
}); });
@ -145,7 +146,7 @@ describe('Testing icon component', () => {
// Should be empty with block style // Should be empty with block style
expect(node._shadowRoot.innerHTML).toBe( expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>` `${styleOpeningTag}${expectedBlock}</style>`
); );
// Check inline // Check inline
@ -157,7 +158,7 @@ describe('Testing icon component', () => {
node.inline = true; node.inline = true;
expect(node._shadowRoot.innerHTML).toBe( expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedInline}</style>` `${styleOpeningTag}${expectedInline}</style>`
); );
expect(node.inline).toBe(true); expect(node.inline).toBe(true);
expect(node.hasAttribute('inline')).toBe(true); expect(node.hasAttribute('inline')).toBe(true);
@ -167,7 +168,7 @@ describe('Testing icon component', () => {
node.removeAttribute('inline'); node.removeAttribute('inline');
expect(node._shadowRoot.innerHTML).toBe( expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>` `${styleOpeningTag}${expectedBlock}</style>`
); );
expect(node.inline).toBe(false); expect(node.inline).toBe(false);
expect(node.hasAttribute('inline')).toBe(false); expect(node.hasAttribute('inline')).toBe(false);
@ -177,7 +178,7 @@ describe('Testing icon component', () => {
node.setAttribute('inline', 'inline'); node.setAttribute('inline', 'inline');
expect(node._shadowRoot.innerHTML).toBe( expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedInline}</style>` `${styleOpeningTag}${expectedInline}</style>`
); );
expect(node.inline).toBe(true); expect(node.inline).toBe(true);
expect(node.hasAttribute('inline')).toBe(true); expect(node.hasAttribute('inline')).toBe(true);
@ -229,7 +230,7 @@ describe('Testing icon component', () => {
"<span style=\"--svg: url(&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Crect width='10' height='10'%3E%3Canimate attributeName='width' values='10;5;10' dur='10s' repeatCount='indefinite' /%3E%3C/rect%3E%3C!-- --%3E%3C/svg%3E&quot;); width: 1em; height: 1em; background-color: transparent; background-repeat: no-repeat; background-size: 100% 100%;\"></span>"; "<span style=\"--svg: url(&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Crect width='10' height='10'%3E%3Canimate attributeName='width' values='10;5;10' dur='10s' repeatCount='indefinite' /%3E%3C/rect%3E%3C!-- --%3E%3C/svg%3E&quot;); width: 1em; height: 1em; background-color: transparent; background-repeat: no-repeat; background-size: 100% 100%;\"></span>";
const html1 = node._shadowRoot.innerHTML; const html1 = node._shadowRoot.innerHTML;
expect(html1.replace(/-- [0-9]+ --/, '-- --')).toBe( expect(html1.replace(/-- [0-9]+ --/, '-- --')).toBe(
`<style>${expectedBlock}</style>${renderedIconWithComment}` `${styleOpeningTag}${expectedBlock}</style>${renderedIconWithComment}`
); );
// Restart animation, test icon again // Restart animation, test icon again
@ -238,7 +239,7 @@ describe('Testing icon component', () => {
const html2 = node._shadowRoot.innerHTML; const html2 = node._shadowRoot.innerHTML;
expect(html2).not.toBe(html1); expect(html2).not.toBe(html1);
expect(html2.replace(/-- [0-9]+ --/, '-- --')).toBe( expect(html2.replace(/-- [0-9]+ --/, '-- --')).toBe(
`<style>${expectedBlock}</style>${renderedIconWithComment}` `${styleOpeningTag}${expectedBlock}</style>${renderedIconWithComment}`
); );
expect(node.status).toBe('rendered'); expect(node.status).toBe('rendered');
@ -252,7 +253,7 @@ describe('Testing icon component', () => {
const html3 = node._shadowRoot.innerHTML; const html3 = node._shadowRoot.innerHTML;
expect(html3.replace(/-- [0-9]+ --/, '-- --')).toBe( expect(html3.replace(/-- [0-9]+ --/, '-- --')).toBe(
`<style>${expectedBlock}</style>${renderedIconWithComment}` `${styleOpeningTag}${expectedBlock}</style>${renderedIconWithComment}`
); );
expect(html3).not.toBe(html1); expect(html3).not.toBe(html1);
expect(html3).not.toBe(html2); expect(html3).not.toBe(html2);
@ -306,7 +307,9 @@ describe('Testing icon component', () => {
const html1 = node._shadowRoot.innerHTML; const html1 = node._shadowRoot.innerHTML;
const svg1 = node._shadowRoot.lastChild as SVGSVGElement; const svg1 = node._shadowRoot.lastChild as SVGSVGElement;
const setCurrentTimeSupported = !!svg1.setCurrentTime; const setCurrentTimeSupported = !!svg1.setCurrentTime;
expect(html1).toBe(`<style>${expectedBlock}</style>${renderedIcon}`); expect(html1).toBe(
`${styleOpeningTag}${expectedBlock}</style>${renderedIcon}`
);
expect(svg1.outerHTML).toBe(renderedIcon); expect(svg1.outerHTML).toBe(renderedIcon);
// Restart animation, test icon again // Restart animation, test icon again

View File

@ -4,6 +4,7 @@ import {
expectedBlock, expectedBlock,
expectedInline, expectedInline,
setupDOM, setupDOM,
styleOpeningTag,
} from '../src/tests/helpers'; } from '../src/tests/helpers';
import { updateStyle } from '../src/render/style'; import { updateStyle } from '../src/render/style';
import { renderIcon } from '../src/render/icon'; import { renderIcon } from '../src/render/icon';
@ -39,7 +40,7 @@ describe('Testing rendering loaded icon', () => {
// Test HTML // Test HTML
expect(node.innerHTML).toBe( expect(node.innerHTML).toBe(
`<style>${expectedBlock}</style><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16"><g></g></svg>` `${styleOpeningTag}${expectedBlock}</style><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16"><g></g></svg>`
); );
// Replace icon content // Replace icon content
@ -65,7 +66,7 @@ describe('Testing rendering loaded icon', () => {
// Test HTML // Test HTML
expect(node.innerHTML).toBe( expect(node.innerHTML).toBe(
`<style>${expectedBlock}</style><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g transform="rotate(90 12 12)"><g><path d=""></path></g></g></svg>` `${styleOpeningTag}${expectedBlock}</style><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g transform="rotate(90 12 12)"><g><path d=""></path></g></g></svg>`
); );
}); });
@ -97,7 +98,7 @@ describe('Testing rendering loaded icon', () => {
// Test HTML // Test HTML
expect(node.innerHTML).toBe( expect(node.innerHTML).toBe(
`<style>${expectedBlock}</style><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16" preserveAspectRatio="xMidYMid meet"><g></g></svg>` `${styleOpeningTag}${expectedBlock}</style><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 16 16" preserveAspectRatio="xMidYMid meet"><g></g></svg>`
); );
}); });
@ -128,7 +129,7 @@ describe('Testing rendering loaded icon', () => {
// Test HTML // Test HTML
expect(node.innerHTML).toBe( expect(node.innerHTML).toBe(
`<style>${expectedInline}</style><span style="--svg: url(&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cg /%3E%3C/svg%3E&quot;); width: 1em; height: 1em; background-color: currentColor; mask-image: var(--svg); mask-repeat: no-repeat; mask-size: 100% 100%;"></span>` `${styleOpeningTag}${expectedInline}</style><span style="--svg: url(&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cg /%3E%3C/svg%3E&quot;); width: 1em; height: 1em; background-color: currentColor; mask-image: var(--svg); mask-repeat: no-repeat; mask-size: 100% 100%;"></span>`
); );
// Change mode to background, add some customisations // Change mode to background, add some customisations
@ -151,7 +152,7 @@ describe('Testing rendering loaded icon', () => {
// Test HTML // Test HTML
expect(node.innerHTML).toBe( expect(node.innerHTML).toBe(
`<style>${expectedInline}</style><span style="--svg: url(&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cg /%3E%3C/svg%3E&quot;); width: 24px; height: 24px; background-color: transparent; background-repeat: no-repeat; background-size: 100% 100%;"></span>` `${styleOpeningTag}${expectedInline}</style><span style="--svg: url(&quot;data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cg /%3E%3C/svg%3E&quot;); width: 24px; height: 24px; background-color: transparent; background-repeat: no-repeat; background-size: 100% 100%;"></span>`
); );
}); });
}); });

View File

@ -4,6 +4,7 @@ import {
expectedBlock, expectedBlock,
expectedInline, expectedInline,
setupDOM, setupDOM,
styleOpeningTag,
} from '../src/tests/helpers'; } from '../src/tests/helpers';
describe('Testing rendering style', () => { describe('Testing rendering style', () => {
@ -18,18 +19,26 @@ describe('Testing rendering style', () => {
// Add style to empty parent // Add style to empty parent
updateStyle(node, false); updateStyle(node, false);
expect(node.innerHTML).toBe('<style>' + expectedBlock + '</style>'); expect(node.innerHTML).toBe(
styleOpeningTag + expectedBlock + '</style>'
);
// Change inline mode // Change inline mode
updateStyle(node, true); updateStyle(node, true);
expect(node.innerHTML).toBe('<style>' + expectedInline + '</style>'); expect(node.innerHTML).toBe(
styleOpeningTag + expectedInline + '</style>'
);
// Do not change anything // Do not change anything
updateStyle(node, true); updateStyle(node, true);
expect(node.innerHTML).toBe('<style>' + expectedInline + '</style>'); expect(node.innerHTML).toBe(
styleOpeningTag + expectedInline + '</style>'
);
// Change to block // Change to block
updateStyle(node, false); updateStyle(node, false);
expect(node.innerHTML).toBe('<style>' + expectedBlock + '</style>'); expect(node.innerHTML).toBe(
styleOpeningTag + expectedBlock + '</style>'
);
}); });
}); });