2
0
mirror of https://github.com/iconify/iconify.git synced 2024-09-19 16:59:02 +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
// Assumes first node is a style node created with updateStyle()
if (parent.childNodes.length > 1) {
const lastChild = parent.lastChild as HTMLElement;
if (node.tagName === 'SPAN' && lastChild.tagName === node.tagName) {
const oldNode = Array.from(parent.childNodes).find((node) => {
const tag =
(node as HTMLElement).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
lastChild.setAttribute('style', node.getAttribute('style'));
oldNode.setAttribute('style', node.getAttribute('style'));
} else {
parent.replaceChild(node, lastChild);
parent.replaceChild(node, oldNode);
}
} else {
// Add new element
parent.appendChild(node);
}
}

View File

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

View File

@ -7,6 +7,7 @@ import {
fakeAPI,
mockAPIData,
awaitUntil,
styleOpeningTag,
} from '../src/tests/helpers';
import { defineIconifyIcon, IconifyIconHTMLElement } from '../src/component';
import type { IconState } from '../src/state';
@ -44,7 +45,7 @@ describe('Testing icon component with API', () => {
// Should be empty
expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>`
`${styleOpeningTag}${expectedBlock}</style>`
);
expect(node.status).toBe('loading');
@ -78,7 +79,7 @@ describe('Testing icon component with API', () => {
// Should not have sent query to API yet
expect(sendQuery).toBeUndefined();
expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>`
`${styleOpeningTag}${expectedBlock}</style>`
);
expect(node.status).toBe('loading');
@ -97,7 +98,7 @@ describe('Testing icon component with API', () => {
const blankSVG =
'<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(
`<style>${expectedBlock}</style>${blankSVG}`
`${styleOpeningTag}${expectedBlock}</style>${blankSVG}`
);
expect(node.status).toBe('rendered');
});
@ -121,7 +122,7 @@ describe('Testing icon component with API', () => {
// Should be empty
expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>`
`${styleOpeningTag}${expectedBlock}</style>`
);
expect(node.status).toBe('loading');
@ -147,7 +148,7 @@ describe('Testing icon component with API', () => {
// Should not have sent query to API yet
expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>`
`${styleOpeningTag}${expectedBlock}</style>`
);
expect(node.status).toBe('loading');
@ -157,7 +158,7 @@ describe('Testing icon component with API', () => {
// Should fail to render
expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>`
`${styleOpeningTag}${expectedBlock}</style>`
);
expect(node.status).toBe('failed');
});

View File

@ -4,6 +4,7 @@ import {
expectedInline,
setupDOM,
nextTick,
styleOpeningTag,
} from '../src/tests/helpers';
import { defineIconifyIcon, IconifyIconHTMLElement } from '../src/component';
import type { IconState } from '../src/state';
@ -73,7 +74,7 @@ describe('Testing icon component', () => {
// Should be empty
expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>`
`${styleOpeningTag}${expectedBlock}</style>`
);
expect(node.status).toBe('loading');
@ -96,7 +97,7 @@ describe('Testing icon component', () => {
// Should still be empty: waiting for next tick
expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>`
`${styleOpeningTag}${expectedBlock}</style>`
);
expect(node.status).toBe('loading');
await nextTick();
@ -105,7 +106,7 @@ describe('Testing icon component', () => {
const blankSVG =
'<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(
`<style>${expectedBlock}</style>${blankSVG}`
`${styleOpeningTag}${expectedBlock}</style>${blankSVG}`
);
expect(node.status).toBe('rendered');
@ -119,7 +120,7 @@ describe('Testing icon component', () => {
expect(node.getAttribute('inline')).toBe('true');
expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedInline}</style>${blankSVG}`
`${styleOpeningTag}${expectedInline}</style>${blankSVG}`
);
expect(node.status).toBe('rendered');
});
@ -145,7 +146,7 @@ describe('Testing icon component', () => {
// Should be empty with block style
expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>`
`${styleOpeningTag}${expectedBlock}</style>`
);
// Check inline
@ -157,7 +158,7 @@ describe('Testing icon component', () => {
node.inline = true;
expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedInline}</style>`
`${styleOpeningTag}${expectedInline}</style>`
);
expect(node.inline).toBe(true);
expect(node.hasAttribute('inline')).toBe(true);
@ -167,7 +168,7 @@ describe('Testing icon component', () => {
node.removeAttribute('inline');
expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedBlock}</style>`
`${styleOpeningTag}${expectedBlock}</style>`
);
expect(node.inline).toBe(false);
expect(node.hasAttribute('inline')).toBe(false);
@ -177,7 +178,7 @@ describe('Testing icon component', () => {
node.setAttribute('inline', 'inline');
expect(node._shadowRoot.innerHTML).toBe(
`<style>${expectedInline}</style>`
`${styleOpeningTag}${expectedInline}</style>`
);
expect(node.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>";
const html1 = node._shadowRoot.innerHTML;
expect(html1.replace(/-- [0-9]+ --/, '-- --')).toBe(
`<style>${expectedBlock}</style>${renderedIconWithComment}`
`${styleOpeningTag}${expectedBlock}</style>${renderedIconWithComment}`
);
// Restart animation, test icon again
@ -238,7 +239,7 @@ describe('Testing icon component', () => {
const html2 = node._shadowRoot.innerHTML;
expect(html2).not.toBe(html1);
expect(html2.replace(/-- [0-9]+ --/, '-- --')).toBe(
`<style>${expectedBlock}</style>${renderedIconWithComment}`
`${styleOpeningTag}${expectedBlock}</style>${renderedIconWithComment}`
);
expect(node.status).toBe('rendered');
@ -252,7 +253,7 @@ describe('Testing icon component', () => {
const html3 = node._shadowRoot.innerHTML;
expect(html3.replace(/-- [0-9]+ --/, '-- --')).toBe(
`<style>${expectedBlock}</style>${renderedIconWithComment}`
`${styleOpeningTag}${expectedBlock}</style>${renderedIconWithComment}`
);
expect(html3).not.toBe(html1);
expect(html3).not.toBe(html2);
@ -306,7 +307,9 @@ describe('Testing icon component', () => {
const html1 = node._shadowRoot.innerHTML;
const svg1 = node._shadowRoot.lastChild as SVGSVGElement;
const setCurrentTimeSupported = !!svg1.setCurrentTime;
expect(html1).toBe(`<style>${expectedBlock}</style>${renderedIcon}`);
expect(html1).toBe(
`${styleOpeningTag}${expectedBlock}</style>${renderedIcon}`
);
expect(svg1.outerHTML).toBe(renderedIcon);
// Restart animation, test icon again

View File

@ -4,6 +4,7 @@ import {
expectedBlock,
expectedInline,
setupDOM,
styleOpeningTag,
} from '../src/tests/helpers';
import { updateStyle } from '../src/render/style';
import { renderIcon } from '../src/render/icon';
@ -39,7 +40,7 @@ describe('Testing rendering loaded icon', () => {
// Test HTML
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
@ -65,7 +66,7 @@ describe('Testing rendering loaded icon', () => {
// Test HTML
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
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
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
@ -151,7 +152,7 @@ describe('Testing rendering loaded icon', () => {
// Test HTML
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,
expectedInline,
setupDOM,
styleOpeningTag,
} from '../src/tests/helpers';
describe('Testing rendering style', () => {
@ -18,18 +19,26 @@ describe('Testing rendering style', () => {
// Add style to empty parent
updateStyle(node, false);
expect(node.innerHTML).toBe('<style>' + expectedBlock + '</style>');
expect(node.innerHTML).toBe(
styleOpeningTag + expectedBlock + '</style>'
);
// Change inline mode
updateStyle(node, true);
expect(node.innerHTML).toBe('<style>' + expectedInline + '</style>');
expect(node.innerHTML).toBe(
styleOpeningTag + expectedInline + '</style>'
);
// Do not change anything
updateStyle(node, true);
expect(node.innerHTML).toBe('<style>' + expectedInline + '</style>');
expect(node.innerHTML).toBe(
styleOpeningTag + expectedInline + '</style>'
);
// Change to block
updateStyle(node, false);
expect(node.innerHTML).toBe('<style>' + expectedBlock + '</style>');
expect(node.innerHTML).toBe(
styleOpeningTag + expectedBlock + '</style>'
);
});
});