@netflix/x-element
Advanced tools
Comparing version
import XElement from '../../x-element.js'; | ||
class ChessPieceElement extends XElement { | ||
static get styles() { | ||
const styleSheet = new CSSStyleSheet(); | ||
styleSheet.replaceSync(`\ | ||
:host { | ||
display: block; | ||
width: var(--hello-size, 8rem); | ||
height: var(--hello-size, 8rem); | ||
background-color: cyan; | ||
border-radius: 50%; | ||
margin: 0.25rem; | ||
box-sizing: border-box; | ||
transition-duration: 250ms; | ||
transition-timing-function: cubic-bezier(0.77, 0, 0.175, 1); | ||
transition-property: transform, border; | ||
will-change: transform; | ||
cursor: pointer; | ||
} | ||
#container { | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
height: 100%; | ||
font-size: calc(var(--hello-size, 8rem) * calc(5/11)); | ||
} | ||
:host([rank="\\2655"]) { | ||
border: 4px dotted hsl(120, 100%, 50%); | ||
background-color: yellow; | ||
transform: rotateX(15deg) rotateY(15deg); | ||
} | ||
:host([rank="\\2654"]) { | ||
border: 3px solid hsl(270, 100%, 50%); | ||
background-color: magenta; | ||
color: blue; | ||
transform: rotateX(-10deg) rotateY(-15deg); | ||
} | ||
:host(:not([rank])), | ||
:host([rank=""]) { | ||
background-color: #ccc; | ||
} | ||
:host(:hover) { | ||
border: 3px solid hsl(180, 100%, 50%); | ||
transform: translateZ(-25px); | ||
} | ||
:host(:focus) { | ||
border: 12px solid hsl(90, 100%, 50%); | ||
outline: none; | ||
} | ||
#container:empty::before { | ||
content: '\\265F'; | ||
} | ||
`); | ||
return [styleSheet]; | ||
} | ||
static get properties() { | ||
@@ -20,61 +81,3 @@ return { | ||
return ({ rank }) => { | ||
return html` | ||
<style> | ||
:host { | ||
display: block; | ||
width: var(--hello-size, 8rem); | ||
height: var(--hello-size, 8rem); | ||
background-color: cyan; | ||
border-radius: 50%; | ||
margin: 0.25rem; | ||
box-sizing: border-box; | ||
transition-duration: 250ms; | ||
transition-timing-function: cubic-bezier(0.77, 0, 0.175, 1); | ||
transition-property: transform, border; | ||
will-change: transform; | ||
cursor: pointer; | ||
} | ||
#container { | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
height: 100%; | ||
font-size: calc(var(--hello-size, 8rem) * calc(5/11)); | ||
} | ||
:host([rank="\u2655"]) { | ||
border: 4px dotted hsl(120, 100%, 50%); | ||
background-color: yellow; | ||
transform: rotateX(15deg) rotateY(15deg); | ||
} | ||
:host([rank="\u2654"]) { | ||
border: 3px solid hsl(270, 100%, 50%); | ||
background-color: magenta; | ||
color: blue; | ||
transform: rotateX(-10deg) rotateY(-15deg); | ||
} | ||
:host(:not([rank])), | ||
:host([rank=""]) { | ||
background-color: #ccc; | ||
} | ||
:host(:hover) { | ||
border: 3px solid hsl(180, 100%, 50%); | ||
transform: translateZ(-25px); | ||
} | ||
:host(:focus) { | ||
border: 12px solid hsl(90, 100%, 50%); | ||
outline: none; | ||
} | ||
#container:empty::before { | ||
content: '\u265F'; | ||
} | ||
</style> | ||
<div id="container">${rank}</div> | ||
`; | ||
return html`<div id="container">${rank}</div>`; | ||
}; | ||
@@ -81,0 +84,0 @@ } |
@@ -12,38 +12,41 @@ import XElement from '../x-element.js'; | ||
class HelloElement extends XElement { | ||
static get styles() { | ||
const styleSheet = new CSSStyleSheet(); | ||
styleSheet.replaceSync(`\ | ||
:host { | ||
display: contents; | ||
} | ||
#container { | ||
position: fixed; | ||
--width: 150px; | ||
--height: 150px; | ||
--font-size: 13px; | ||
font-weight: bold; | ||
line-height: calc(var(--font-size) * 1.8); | ||
font-size: var(--font-size); | ||
top: calc(0px - var(--width) / 2); | ||
left: calc(0px - var(--height) / 2); | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
width: var(--width); | ||
height: var(--height); | ||
transform: translate(calc(0vw - var(--width)), 50vh) rotate(0deg); | ||
opacity: 1; | ||
transform-origin: center; | ||
border-radius: 100vmax; | ||
cursor: default; | ||
} | ||
#logo { | ||
padding-bottom: var(--font-size); | ||
} | ||
`); | ||
return [styleSheet]; | ||
} | ||
static template(html) { | ||
return () => { | ||
return html` | ||
<style> | ||
:host { | ||
display: contents; | ||
} | ||
#container { | ||
position: fixed; | ||
--width: 150px; | ||
--height: 150px; | ||
--font-size: 13px; | ||
font-weight: bold; | ||
line-height: calc(var(--font-size) * 1.8); | ||
font-size: var(--font-size); | ||
top: calc(0px - var(--width) / 2); | ||
left: calc(0px - var(--height) / 2); | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
width: var(--width); | ||
height: var(--height); | ||
transform: translate(calc(0vw - var(--width)), 50vh) rotate(0deg); | ||
opacity: 1; | ||
transform-origin: center; | ||
border-radius: 100vmax; | ||
cursor: default; | ||
} | ||
#logo { | ||
padding-bottom: var(--font-size); | ||
} | ||
</style> | ||
<div id="container"><pre id="logo">${logo}</pre></div> | ||
`; | ||
return html`<div id="container"><pre id="logo">${logo}</pre></div>`; | ||
}; | ||
@@ -50,0 +53,0 @@ } |
import XElement from '../../x-element.js'; | ||
import { asyncAppend } from 'https://unpkg.com/lit-html@3.1.2/directives/async-append.js'; | ||
import { asyncReplace } from 'https://unpkg.com/lit-html@3.1.2/directives/async-replace.js'; | ||
import { cache } from 'https://unpkg.com/lit-html@3.1.2/directives/cache.js'; | ||
import { classMap } from 'https://unpkg.com/lit-html@3.1.2/directives/class-map.js'; | ||
import { directive } from 'https://unpkg.com/lit-html@3.1.2/directive.js'; | ||
import { guard } from 'https://unpkg.com/lit-html@3.1.2/directives/guard.js'; | ||
import { html, render as originalRender, svg } from 'https://unpkg.com/lit-html@3.1.2/lit-html.js'; | ||
import { ifDefined } from 'https://unpkg.com/lit-html@3.1.2/directives/if-defined.js'; | ||
import { live } from 'https://unpkg.com/lit-html@3.1.2/directives/live.js'; | ||
import { repeat } from 'https://unpkg.com/lit-html@3.1.2/directives/repeat.js'; | ||
import { styleMap } from 'https://unpkg.com/lit-html@3.1.2/directives/style-map.js'; | ||
import { templateContent } from 'https://unpkg.com/lit-html@3.1.2/directives/template-content.js'; | ||
import { unsafeHTML } from 'https://unpkg.com/lit-html@3.1.2/directives/unsafe-html.js'; | ||
import { unsafeSVG } from 'https://unpkg.com/lit-html@3.1.2/directives/unsafe-svg.js'; | ||
import { until } from 'https://unpkg.com/lit-html@3.1.2/directives/until.js'; | ||
import { asyncAppend } from 'lit-html/directives/async-append.js'; | ||
import { asyncReplace } from 'lit-html/directives/async-replace.js'; | ||
import { cache } from 'lit-html/directives/cache.js'; | ||
import { choose } from 'lit-html/directives/choose.js'; | ||
import { classMap } from 'lit-html/directives/class-map.js'; | ||
import { directive } from 'lit-html/directive.js'; | ||
import { guard } from 'lit-html/directives/guard.js'; | ||
import { html, render as originalRender, svg } from 'lit-html/lit-html.js'; | ||
import { ifDefined } from 'lit-html/directives/if-defined.js'; | ||
import { join } from 'lit-html/directives/join.js'; | ||
import { keyed } from 'lit-html/directives/keyed.js'; | ||
import { live } from 'lit-html/directives/live.js'; | ||
import { map } from 'lit-html/directives/map.js'; | ||
import { range } from 'lit-html/directives/range.js'; | ||
import { ref } from 'lit-html/directives/ref.js'; | ||
import { repeat } from 'lit-html/directives/repeat.js'; | ||
import { styleMap } from 'lit-html/directives/style-map.js'; | ||
import { templateContent } from 'lit-html/directives/template-content.js'; | ||
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; | ||
import { unsafeSVG } from 'lit-html/directives/unsafe-svg.js'; | ||
import { until } from 'lit-html/directives/until.js'; | ||
import { when } from 'lit-html/directives/when.js'; | ||
export default class BaseElement extends XElement { | ||
@@ -23,7 +31,7 @@ // Use lit-html's template engine rather than the built-in x-element engine. | ||
return { | ||
render, html, svg, asyncAppend, asyncReplace, cache, classMap, directive, | ||
guard, ifDefined, live, repeat, styleMap, templateContent, unsafeHTML, | ||
unsafeSVG, until, | ||
render, html, svg, asyncAppend, asyncReplace, cache, choose, classMap, | ||
directive, guard, ifDefined, join, keyed, live, map, range, ref, repeat, | ||
styleMap, templateContent, unsafeHTML, unsafeSVG, until, when, | ||
}; | ||
} | ||
} |
@@ -1,122 +0,145 @@ | ||
import XElement from '../../x-element.js'; | ||
import { html as litHtmlHtml, render as litHtmlRender } from 'https://unpkg.com/lit-html@3.1.2/lit-html.js'; | ||
import { html as uhtmlHtml, render as uhtmlRender } from 'https://unpkg.com/uhtml@4.4.7'; | ||
class RootTest { | ||
static #tests = []; | ||
class LitHtmlElement extends XElement { | ||
// Use lit-html's template engine rather than the built-in x-element engine. | ||
static get templateEngine() { | ||
const render = (container, template) => litHtmlRender(template, container); | ||
const html = litHtmlHtml; | ||
return { render, html }; | ||
static async #load(frame, href) { | ||
const { promise, resolve, reject } = Promise.withResolvers(); | ||
setTimeout(() => reject(new Error('Timed out.')), 5_000); | ||
const { port1, port2} = new MessageChannel(); | ||
port1.addEventListener('message', event => { | ||
if (event.data?.type === 'pong') { | ||
resolve(); | ||
} | ||
}, { once: true }); | ||
port1.start(); // Needed for use with addEventListener. | ||
frame.src = href; | ||
frame.setAttribute('data-running', ''); | ||
frame.addEventListener('load', () => { | ||
frame.contentWindow.postMessage({ type: 'ping' }, '*', [port2]); | ||
}, { once: true }); | ||
document.body.append(frame); | ||
await promise; | ||
return port1; | ||
} | ||
} | ||
class UhtmlElement extends XElement { | ||
// Use µhtml's template engine rather than the built-in x-element engine. | ||
static get templateEngine() { | ||
return { render: uhtmlRender, html: uhtmlHtml }; | ||
static #getMicrosecondsText(microseconds) { | ||
if (microseconds >= 10) { | ||
return `${microseconds.toFixed()} µs`; | ||
} else if (microseconds >= 1) { | ||
return `${microseconds.toFixed(1)} µs`; | ||
} else { | ||
return `${microseconds.toFixed(2)} µs`; | ||
} | ||
} | ||
} | ||
/* | ||
// Reference template string (since it's hard to read as an interpolated array). | ||
// Note that there are no special characters here so strings == strings.raw. | ||
<div id="p1" attr="${attr}"><div id="p2" data-foo><div id="p3" data-bar="bar"><div id="${id}" boolean ?hidden="${hidden}" .title="${title}">${content1} -- ${content2}</div></div></div></div> | ||
*/ | ||
const getStrings = () => { | ||
const strings = ['<div id="p1" attr="', '"><div id="p2" data-foo><div id="p3" data-bar="bar"><div id="', '" boolean ?hidden="', '" .title="', '">', ' -- ', '</div></div></div></div>']; | ||
strings.raw = ['<div id="p1" attr="', '"><div id="p2" data-foo><div id="p3" data-bar="bar"><div id="', '" boolean ?hidden="', '" .title="', '">', ' -- ', '</div></div></div></div>']; | ||
return strings; | ||
}; | ||
const getValues = ({ attr, id, hidden, title, content1, content2 }) => { | ||
const values = [attr, id, hidden, title, content1, content2]; | ||
return values; | ||
}; | ||
const tick = async () => { | ||
await new Promise(resolve => requestAnimationFrame(resolve)); | ||
}; | ||
const run = async (output, constructor, ...tests) => { | ||
output.textContent = ''; | ||
const { render, html } = constructor.templateEngine; | ||
const slop = 1000; | ||
const injectCount = 10000; | ||
const initialCount = 100000; | ||
const updateCount = 100000; | ||
if (tests.includes('inject')) { | ||
// Test inject performance. | ||
await tick(); | ||
let injectSum = 0; | ||
const injectProperties = { attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' }; | ||
const injectContainer = document.createElement('div'); | ||
for (let iii = 0; iii < injectCount + slop; iii++) { | ||
const strings = getStrings(); | ||
const values = getValues(injectProperties); | ||
const t0 = performance.now(); | ||
render(injectContainer, html(strings, ...values)); | ||
const t1 = performance.now(); | ||
if (iii >= slop) { | ||
injectSum += t1 - t0; | ||
// Print percentiles (by ten). | ||
static #printDistribution(container, percentiles, min, max) { | ||
const fragment = new DocumentFragment(); | ||
for (const percentile of [20, 30, 40, 50, 60, 70, 80]) { | ||
const value = percentiles[percentile]; | ||
const percent = (value - min) / (max - min) * 100; | ||
const element = document.createElement('div'); | ||
element.classList.add('percentile'); | ||
if (percentile === 50) { | ||
element.classList.add('median'); | ||
} | ||
if (iii % 100 === 0) { | ||
await tick(); | ||
} | ||
element.style.left = `${percent}%`; | ||
element.textContent = '•'; | ||
fragment.append(element); | ||
} | ||
const injectAverage = `${(injectSum / injectCount * 1000).toFixed(1).padStart(6)} µs`; | ||
output.textContent += `${output.textContent ? '\n' : ''}inject: ${injectAverage} (tested ${injectCount.toLocaleString()} times)`; | ||
const minElement = document.createElement('div'); | ||
minElement.classList.add('min'); | ||
minElement.textContent = RootTest.#getMicrosecondsText(min * 1_000); | ||
const maxElement = document.createElement('div'); | ||
maxElement.classList.add('max'); | ||
maxElement.textContent = RootTest.#getMicrosecondsText(max * 1_000); | ||
fragment.append(minElement, maxElement); | ||
container.style.position = 'relative'; | ||
container.replaceChildren(fragment); | ||
} | ||
if (tests.includes('initial')) { | ||
// Test initial performance. | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
let initialSum = 0; | ||
const initialStrings = getStrings(); | ||
const initialProperties = { attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' }; | ||
const initialValues = getValues(initialProperties); | ||
const initialContainer = document.createElement('div'); | ||
for (let iii = 0; iii < initialCount + slop; iii++) { | ||
const t0 = performance.now(); | ||
render(initialContainer, html(initialStrings, ...initialValues)); | ||
const t1 = performance.now(); | ||
if (iii >= slop) { | ||
initialSum += t1 - t0; | ||
static #printOutput(name) { | ||
const output = document.getElementById(name).querySelector('.output'); | ||
output.replaceChildren(); | ||
const tests = RootTest.#tests.filter(candidate => candidate.name === name); | ||
// We only show 20-80 percentiles to ditch some outliers. | ||
const min = Math.min(...tests.filter(({ skip }) => !skip).map(({ percentiles }) => percentiles[20])); | ||
const max = Math.max(...tests.filter(({ skip }) => !skip).map(({ percentiles }) => percentiles[80])); | ||
for (const { id, percentiles, skip, reason } of tests) { | ||
const container = document.createElement('div'); | ||
container.classList.add('distribution'); | ||
if (skip) { | ||
container.setAttribute('data-skipped', ''); | ||
} | ||
if (iii % 1000 === 0) { | ||
await tick(); | ||
const left = document.createElement('div'); | ||
left.classList.add('left'); | ||
const right = document.createElement('div'); | ||
right.classList.add('right'); | ||
if (!skip) { | ||
const inner = document.createElement('div'); | ||
RootTest.#printDistribution(inner, percentiles, min, max); | ||
right.append(inner); | ||
} else { | ||
right.textContent = reason; | ||
} | ||
if (!skip) { | ||
const label = document.createElement('div'); | ||
label.textContent = id.padEnd(15, '\u00A0'); | ||
const median = percentiles[50]; | ||
const value = document.createElement('div'); | ||
value.textContent = RootTest.#getMicrosecondsText(median * 1_000).padStart(7, '\u00A0'); | ||
left.append(label, value); | ||
} else { | ||
const label = document.createElement('div'); | ||
label.textContent = id.padEnd(15, '\u00A0'); | ||
const value = document.createElement('div'); | ||
value.textContent = 'N/A'.padStart(7, '\u00A0'); | ||
left.append(label, value); | ||
} | ||
container.append(left, right); | ||
output.append(container); | ||
} | ||
const initialAverage = `${(initialSum / initialCount * 1000).toFixed(1).padStart(4)} µs`; | ||
output.textContent += `${output.textContent ? '\n' : ''}initial: ${initialAverage} (tested ${initialCount.toLocaleString()} times)`; | ||
} | ||
if (tests.includes('update')) { | ||
// Test update performance. | ||
await new Promise(resolve => setTimeout(resolve, 0)); | ||
let updateSum = 0; | ||
const updateStrings = getStrings(); | ||
const updateProperties = [ | ||
{ attr: '123', id: 'foo', hidden: false, title: 'test', content1: 'AAA', content2: 'BBB' }, | ||
{ attr: '456', id: 'bar', hidden: false, title: 'test', content1: 'ZZZ', content2: 'BBB' }, | ||
]; | ||
const updateContainer = document.createElement('div'); | ||
for (let iii = 0; iii < updateCount + slop; iii++) { | ||
const values = getValues(updateProperties[iii % 2]); | ||
const t0 = performance.now(); | ||
render(updateContainer, html(updateStrings, ...values)); | ||
const t1 = performance.now(); | ||
if (iii >= slop) { | ||
updateSum += t1 - t0; | ||
static async test(href) { | ||
const frame = document.getElementById('frame'); | ||
const port1 = await this.#load(frame, href); | ||
const { promise, resolve, reject } = Promise.withResolvers(); | ||
setTimeout(() => reject(new Error('Timed out.')), 30_000); | ||
port1.addEventListener('message', event => { | ||
if (event.data?.type === 'result') { | ||
resolve(event.data.result); | ||
} | ||
if (iii % 1000 === 0) { | ||
await tick(); | ||
} | ||
} | ||
const updateAverage = `${(updateSum / updateCount * 1000).toFixed(1).padStart(5)} µs`; | ||
output.textContent += `${output.textContent ? '\n' : ''}update: ${updateAverage} (tested ${updateCount.toLocaleString()} times)`; | ||
}); | ||
port1.postMessage({ type: 'start' }); | ||
const { id, name, percentiles } = await promise; | ||
RootTest.#tests.push({ id, name, percentiles }); | ||
RootTest.#printOutput(name); | ||
frame.removeAttribute('data-running'); | ||
} | ||
}; | ||
await run(document.getElementById('default'), XElement, 'inject', 'initial', 'update'); | ||
await run(document.getElementById('lit-html'), LitHtmlElement, 'inject', 'initial', 'update'); | ||
await run(document.getElementById('uhtml'), UhtmlElement, 'inject', 'initial', 'update'); | ||
static skip(href, reason) { | ||
const match = href.match(/(?<id>[a-z-]+)\.html\?test=(?<name>[a-z]+)/); | ||
const { id, name } = match.groups; | ||
this.#tests.push({ id, name, skip: true, reason }); | ||
this.#printOutput(name); | ||
} | ||
} | ||
await RootTest.test('./default.html?test=inject'); | ||
await RootTest.test('./lit-html.html?test=inject'); | ||
await RootTest.test('./uhtml.html?test=inject'); | ||
await RootTest.skip('./react.html?test=inject', 'React does interpretation during compilation.'); | ||
await RootTest.test('./default.html?test=initial'); | ||
await RootTest.test('./lit-html.html?test=initial'); | ||
await RootTest.test('./uhtml.html?test=initial'); | ||
await RootTest.test('./react.html?test=initial'); | ||
await RootTest.test('./default.html?test=update'); | ||
await RootTest.test('./lit-html.html?test=update'); | ||
await RootTest.test('./uhtml.html?test=update'); | ||
await RootTest.test('./react.html?test=update'); |
import ready from '../../etc/ready.js'; | ||
import React from 'react'; | ||
import ReactDOMClient from 'react-dom/client'; | ||
@@ -20,3 +22,4 @@ class ChessPiece extends React.Component { | ||
const root = document.getElementById('root'); | ||
ReactDOM.render(React.createElement(ChessPiece, { rank: ranks[0] }, null), root); | ||
const reactRoot = ReactDOMClient.createRoot(root); | ||
reactRoot.render(React.createElement(ChessPiece, { rank: ranks[0] }, null)); | ||
@@ -26,4 +29,4 @@ setInterval(() => { | ||
counter += 1; | ||
ReactDOM.render(React.createElement(ChessPiece, { rank: rank }, null), root); | ||
reactRoot.render(React.createElement(ChessPiece, { rank: rank }, null)); | ||
}, 1250); | ||
}); |
import XElement from '../../x-element.js'; | ||
import { render, html, svg } from 'https://unpkg.com/uhtml@4.4.7'; | ||
import { render, html, svg } from 'uhtml'; | ||
@@ -4,0 +4,0 @@ export default class BaseElement extends XElement { |
{ | ||
"name": "@netflix/x-element", | ||
"description": "A dead simple starting point for custom elements.", | ||
"version": "1.1.1", | ||
"version": "1.1.2", | ||
"license": "Apache-2.0", | ||
@@ -10,5 +10,7 @@ "repository": "github:Netflix/x-element", | ||
"./x-element.js": "./x-element.js", | ||
"./x-element.d.ts": "./x-element.d.ts", | ||
"./x-template.js": "./x-template.js", | ||
"./etc/ready.js": "./etc/ready.js", | ||
"./etc/ready.d.ts": "./etc/ready.d.ts" | ||
"./ts/x-element.d.ts": "./ts/x-element.d.ts", | ||
"./ts/x-template.d.ts": "./ts/x-template.d.ts", | ||
"./ts/etc/ready.d.ts": "./ts/etc/ready.d.ts" | ||
}, | ||
@@ -29,19 +31,19 @@ "publishConfig": { | ||
"/x-element.js", | ||
"/x-element.d.ts", | ||
"/x-element.d.ts.map", | ||
"/x-template.js", | ||
"/demo", | ||
"/test", | ||
"/etc" | ||
"/etc", | ||
"/ts" | ||
], | ||
"devDependencies": { | ||
"@netflix/eslint-config": "^3.0.0", | ||
"eslint-plugin-jsdoc": "^50.3.1", | ||
"eslint": "^9.12.0", | ||
"puppeteer": "^23.5.3", | ||
"tap-parser": "^18.0.0", | ||
"typescript": "^5.6.3" | ||
"@netflix/eslint-config": "3.0.0", | ||
"eslint-plugin-jsdoc": "50.6.0", | ||
"eslint": "9.16.0", | ||
"puppeteer": "23.10.1", | ||
"tap-parser": "18.0.0", | ||
"typescript": "5.7.2" | ||
}, | ||
"engines": { | ||
"node": "20.18.0", | ||
"npm": "10.8.2" | ||
"node": "22.11.0", | ||
"npm": "10.9.0" | ||
}, | ||
@@ -48,0 +50,0 @@ "contributors": [ |
import { test, coverage } from './x-test.js'; | ||
// We import this here so we can see code coverage. | ||
// We import these here so we can see code coverage. | ||
import '../x-element.js'; | ||
import '../x-template.js'; | ||
// Set a high bar for code coverage! | ||
coverage(new URL('../x-element.js', import.meta.url).href, 100); | ||
coverage(new URL('../x-template.js', import.meta.url).href, 100); | ||
@@ -9,0 +11,0 @@ test('./test-analysis-errors.html'); |
@@ -26,2 +26,5 @@ import XElement from '../x-element.js'; | ||
}, | ||
undefinedProperty: { | ||
type: String, | ||
}, | ||
typelessProperty: {}, | ||
@@ -37,3 +40,3 @@ typelessPropertyWithCustomAttribute: { | ||
static template(html) { | ||
return ({ normalProperty, camelCaseProperty, numericProperty, nullProperty }) => { | ||
return ({ normalProperty, camelCaseProperty, numericProperty, nullProperty, undefinedProperty }) => { | ||
return html` | ||
@@ -44,2 +47,3 @@ <div id="normal">${normalProperty}</div> | ||
<span id="nul">${nullProperty}</span> | ||
<span id="undef">${undefinedProperty}</span> | ||
`; | ||
@@ -66,2 +70,8 @@ }; | ||
it('renders an empty string in place of undefined value', () => { | ||
const el = document.createElement('test-element'); | ||
document.body.append(el); | ||
assert(el.shadowRoot.getElementById('undef').textContent === ''); | ||
}); | ||
it('property setter updates on next micro tick after connect', async () => { | ||
@@ -106,2 +116,3 @@ const el = document.createElement('test-element'); | ||
'null-property', | ||
'undefined-property', | ||
'typeless-property', | ||
@@ -108,0 +119,0 @@ 'custom-attribute-typeless', |
@@ -7,2 +7,25 @@ import XElement from '../x-element.js'; | ||
class TestElement extends XElement { | ||
static get styles() { | ||
const styleSheet = new CSSStyleSheet(); | ||
styleSheet.replaceSync(`\ | ||
#calculation { | ||
background-color: lightgreen; | ||
padding: 10px; | ||
} | ||
:host([negative]) #calculation { | ||
background-color: lightcoral; | ||
} | ||
:host([underline]) #calculation { | ||
text-decoration: underline; | ||
} | ||
:host([italic]) #calculation { | ||
font-style: italic; | ||
} | ||
`); | ||
return [styleSheet]; | ||
} | ||
static get properties() { | ||
@@ -88,23 +111,3 @@ return { | ||
return ({ a, b, c }) => { | ||
return html` | ||
<style> | ||
#calculation { | ||
background-color: lightgreen; | ||
padding: 10px; | ||
} | ||
:host([negative]) #calculation { | ||
background-color: lightcoral; | ||
} | ||
:host([underline]) #calculation { | ||
text-decoration: underline; | ||
} | ||
:host([italic]) #calculation { | ||
font-style: italic; | ||
} | ||
</style> | ||
<span id="calculation">${a} + ${b} = ${c}</span> | ||
`; | ||
return html`<span id="calculation">${a} + ${b} = ${c}</span>`; | ||
}; | ||
@@ -111,0 +114,0 @@ } |
@@ -92,9 +92,9 @@ import XElement from '../x-element.js'; | ||
return { | ||
strings: {}, | ||
strings: { default: () => ['one', 'two', 'three'] }, | ||
}; | ||
} | ||
static template(html, { map }) { | ||
static template(html) { | ||
return ({ strings }) => { | ||
// In this case, "map" will fail if "strings" is not an array. | ||
return html`${map(strings, () => {}, string => html`${string}`)}`; | ||
// In this case, the array will fail if items are not template results. | ||
return html`<div>${strings}</div>`; | ||
}; | ||
@@ -110,3 +110,3 @@ } | ||
} catch (error) { | ||
const expected = ' — Invalid template for "TestElement" at path "test-element-3"'; | ||
const expected = 'Invalid template for "TestElement" / <test-element-3> at path "test-element-3".'; | ||
message = error.message; | ||
@@ -125,9 +125,9 @@ passed = error.message.endsWith(expected); | ||
return { | ||
strings: {}, | ||
strings: { default: () => ['one', 'two', 'three'] }, | ||
}; | ||
} | ||
static template(html, { map }) { | ||
static template(html) { | ||
return ({ strings }) => { | ||
// In this case, "map" will fail if "strings" is not an array. | ||
return html`${map(strings, () => {}, string => html`${string}`)}`; | ||
// In this case, the array will fail if items are not template results. | ||
return html`<div>${strings}</div>`; | ||
}; | ||
@@ -148,3 +148,3 @@ } | ||
} catch (error) { | ||
const expected = ' — Invalid template for "TestElement" at path "test-element-4[id="testing"][class="foo bar"][boolean][variation="primary"]"'; | ||
const expected = 'Invalid template for "TestElement" / <test-element-4> at path "test-element-4[id="testing"][class="foo bar"][boolean][variation="primary"]".'; | ||
message = error.message; | ||
@@ -151,0 +151,0 @@ passed = error.message.endsWith(expected); |
@@ -5,2 +5,20 @@ import XElement from '../x-element.js'; | ||
class TestElement extends XElement { | ||
static get styles() { | ||
const styleSheet = new CSSStyleSheet(); | ||
styleSheet.replaceSync(`\ | ||
:host #container { | ||
transition-property: box-shadow; | ||
transition-duration: 300ms; | ||
transition-timing-function: linear; | ||
box-shadow: 0 0 0 1px black; | ||
padding: 10px; | ||
} | ||
:host([popped]) #container { | ||
box-shadow: 0 0 10px 0 black; | ||
} | ||
`); | ||
return [styleSheet]; | ||
} | ||
static get properties() { | ||
@@ -68,15 +86,2 @@ return { | ||
return html` | ||
<style> | ||
:host #container { | ||
transition-property: box-shadow; | ||
transition-duration: 300ms; | ||
transition-timing-function: linear; | ||
box-shadow: 0 0 0 1px black; | ||
padding: 10px; | ||
} | ||
:host([popped]) #container { | ||
box-shadow: 0 0 10px 0 black; | ||
} | ||
</style> | ||
<div id="container"> | ||
@@ -83,0 +88,0 @@ <div>Changes:</div> |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Found 16 instances in 1 package
382478
30.06%78
18.18%9226
22.52%1
-94.74%