diff --git a/CHANGELOG.md b/CHANGELOG.md index f1877ad..e9c9707 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,42 +8,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- You can now bind attributes with `??foo="${bar}"` syntax. This is functionally - equivalent to the `nullish` updater and will replace that functionality later. -- A new `unsafe` updater was added to replace `unsafeHTML` and `unsafeSVG`. You - use it like `unsafe(value, 'html')` and `unsafe(value, 'svg')`. +- You can now bind attributes with `??foo="${bar}"` syntax in the default + template engine. This is functionally equivalent to the `nullish` updater from + the default template engine and will replace that functionality later. +- A new `unsafe` updater was added to replace `unsafeHTML` in the default + template engine. You use it like `unsafe(value)`. Only unsafe html injection + will be supported in the default template engine. ### Changed - Template errors now include approximate line numbers from the offending - template. They also print the registered custom element tag name (#201). + template in the default template engine. They also print the registered custom + element tag name (#201). - The `ifDefined` updater now deletes the attribute on `null` in addition to - `undefined`. This makes it behave identically to `nullish`. However, both - updaters are deprecated and the `??attr` binding should be used instead. -- Interpolation of `textarea` is stricter. This used to be handled with some - leniency — ``. Now, you have to fit the - interpolation exactly — ``. + `undefined` in the default template engine. This makes it behave identically + to `nullish` in the default template engine. However, both updaters are + deprecated — the `??attr` binding should be used instead when using the + default template engine (#204). +- Interpolation of `textarea` is more strict in the default template engine. + This used to be handled with some leniency for newlines in templates — + ``. Now, you have to interpolate exactly — + ``. ### Deprecated - The `ifDefined` and `nullish` updaters are deprecated, update templates to use syntax like `??foo="${bar}"`. -- The `repeat` updater is deprecated, use `map` instead. -- The `unsafeHTML` and `unsafeSVG` updaters are deprecated, use `unsafe`. +- The `repeat` updater is deprecated, use `map` instead (#204). +- The `unsafeHTML` and `unsafeSVG` updaters are deprecated, use `unsafe` (#204). - The `plaintext` tag is no longer handled. This is a deprecated html tag which required special handling… but it’s unlikely that anyone is using that. ### Fixed -- Transitions from different content values should all now work. For example, - you previously could not change from a text value to an array. Additionally, - state is properly cleared when going from one value type to another — e.g., - when going from `unsafe` back to `null`. -- The `map` updater throws immediately when given non-array input. Previously, - it only threw _just before_ it was bound as content. -- Dummy content cursor is no longer appended to end of template. This was an - innocuous off-by-one error when creating instrumented html from the tagged - template strings. +- Transitions from different content values should all now work for the default + template engine. For example, you previously could not change from a text + value to an array. Additionally, state is properly cleared when going from one + value type to another — e.g., when going from `unsafe` back to `null`. +- The `map` updater throws immediately when given non-array input for the + default template engine. Previously, it only threw _just before_ it was bound. +- Dummy content cursor is no longer appended to end of template for the default + template engine. This was an innocuous off-by-one error when creating + instrumented html from the tagged template strings. ## [1.1.1] - 2024-11-09 diff --git a/doc/TEMPLATES.md b/doc/TEMPLATES.md index 67fab91..77947ce 100644 --- a/doc/TEMPLATES.md +++ b/doc/TEMPLATES.md @@ -21,49 +21,19 @@ static template(html, { map }) { } ``` -The following binding types are supported: - -| Type | Example | -| :------------------ | :----------------------------------------- | -| attribute | `` | -| attribute (boolean) | `` | -| attribute (defined) | `` | -| property | `` | -| content | `${foo}` | - -Emulates: - -```javascript -const el = document.createElement('div'); -el.attachShadow({ mode: 'open' }); -el.innerHTML = ''; -const target = el.shadowRoot.getElementById('target'); - -// attribute value bindings set the attribute value -target.setAttribute('foo', bar); - -// attribute boolean bindings set the attribute to an empty string or remove -target.setAttribute('foo', ''); // when bar is truthy -target.removeAttribute('foo'); // when bar is falsy - -// attribute defined bindings set the attribute if the value is non-nullish -target.setAttribute('foo', bar); // when bar is non-nullish -target.removeAttribute('foo'); // when bar is nullish - -// property bindings assign the value to the property of the node -target.foo = bar; - -// content bindings create text nodes for basic content -const text = document.createTextNode(''); -text.textContent = foo; -target.append(text); - -// content bindings append a child for singular, nested content -target.append(foo); - -// content binding maps and appends children for arrays of nested content -target.append(...foo); -``` +The following bindings are supported: + +| Binding | Template | Emulates | +| :------------------ | :--------------------------- | :------------------------------------------------------------ | +| -- | -- | `const el = document.createElement('div');` | +| attribute | `
` | `el.setAttribute('foo', bar);` | +| attribute (boolean) | `
` | `el.setAttribute('foo', ''); // if “bar” is truthy` | +| -- | -- | `el.removeAttribute('foo'); // if “bar” is falsy` | +| attribute (defined) | `
` | `el.setAttribute('foo', bar); // if “bar” is non-nullish` | +| -- | -- | `el.removeAttribute('foo'); // if “bar” is nullish` | +| property | `
` | `el.foo = bar;` | +| content | `
${foo}
` | `el.append(document.createTextNode(foo)) // if “bar” is text` | +| -- | -- | (see [content binding](#content-binding) for composition) | **Important note on serialization during data binding:** diff --git a/test/test-template-engine.js b/test/test-template-engine.js index 2d89ff4..c797140 100644 --- a/test/test-template-engine.js +++ b/test/test-template-engine.js @@ -564,7 +564,7 @@ describe('html updaters', () => { it('unsafe html', () => { const getTemplate = ({ content }) => { - return html`
${unsafe(content, 'html')}
`; + return html`
${unsafe(content)}
`; }; const container = document.createElement('div'); document.body.append(container); @@ -892,7 +892,7 @@ describe('html updaters', () => { const resolve = (type, value) => { switch(type) { case 'map': return map(value, item => item.id, item => html`
`); - case 'html': return unsafe(value, 'html'); + case 'html': return unsafe(value); default: return value; // E.g., an array, some text, null, undefined, etc. } }; @@ -1032,31 +1032,6 @@ describe('svg rendering', () => { }); describe('svg updaters', () => { - it('unsafe svg', () => { - const getTemplate = ({ content }) => { - return html` - - ${unsafe(content, 'svg')} - - `; - }; - const container = document.createElement('div'); - document.body.append(container); - render(container, getTemplate({ content: '' })); - assert(!!container.querySelector('#injected')); - assert(container.querySelector('#injected').getBoundingClientRect().height = 20); - assert(container.querySelector('#injected').getBoundingClientRect().width = 20); - render(container, getTemplate({ content: '' })); - assert(!!container.querySelector('#injected')); - assert(container.querySelector('#injected').getBoundingClientRect().height = 10); - assert(container.querySelector('#injected').getBoundingClientRect().width = 10); - container.remove(); - }); - it('unsafeSVG', () => { const getTemplate = ({ content }) => { return html` @@ -1476,28 +1451,10 @@ describe('rendering errors', () => { describe('unsafe', () => { - it('throws if used on an unexpected language', () => { - const expected = 'Unexpected unsafe language "css". Expected "html" or "svg".'; - const getTemplate = ({ maybe }) => { - return html`
`; - }; - const container = document.createElement('div'); - document.body.append(container); - let actual; - try { - render(container, getTemplate({ maybe: 'yes' })); - } catch (error) { - actual = error.message; - } - assert(!!actual, 'No error was thrown.'); - assert(actual === expected, actual); - container.remove(); - }); - it('throws if used on an "attribute"', () => { const expected = 'The unsafe update must be used on content, not on an attribute.'; const getTemplate = ({ maybe }) => { - return html`
`; + return html`
`; }; const container = document.createElement('div'); document.body.append(container); @@ -1515,7 +1472,7 @@ describe('rendering errors', () => { it('throws if used on a "boolean"', () => { const expected = 'The unsafe update must be used on content, not on a boolean attribute.'; const getTemplate = ({ maybe }) => { - return html`
`; + return html`
`; }; const container = document.createElement('div'); document.body.append(container); @@ -1533,7 +1490,7 @@ describe('rendering errors', () => { it('throws if used on a "defined"', () => { const expected = 'The unsafe update must be used on content, not on a defined attribute.'; const getTemplate = ({ maybe }) => { - return html`
`; + return html`
`; }; const container = document.createElement('div'); document.body.append(container); @@ -1551,7 +1508,7 @@ describe('rendering errors', () => { it('throws if used with a "property"', () => { const expected = 'The unsafe update must be used on content, not on a property.'; const getTemplate = ({ maybe }) => { - return html`
`; + return html`
`; }; const container = document.createElement('div'); document.body.append(container); @@ -1569,7 +1526,7 @@ describe('rendering errors', () => { it('throws if used with "text"', () => { const expected = 'The unsafe update must be used on content, not on text content.'; const getTemplate = ({ maybe }) => { - return html``; + return html``; }; const container = document.createElement('div'); document.body.append(container); @@ -1588,7 +1545,7 @@ describe('rendering errors', () => { const getTemplate = ({ content }) => { return html`
- ${unsafe(content, 'html')} + ${unsafe(content)}
`; }; diff --git a/x-element.js b/x-element.js index bc5ff8a..e99beb8 100644 --- a/x-element.js +++ b/x-element.js @@ -1194,23 +1194,19 @@ class TemplateEngine { } /** - * Updater to inject trusted “html” or “svg” into the DOM. - * Use with caution. The "unsafe" updater allows arbitrary input to be - * parsed and injected into the DOM. + * Updater to inject trusted “html” into the DOM. + * Use with caution. The "unsafe" updater allows arbitrary input to be parsed + * and injected into the DOM. * ```js - * html`
${unsafe(obj.trustedMarkup, 'html')}
`; + * html`
${unsafe(obj.trustedMarkup)}
`; * ``` * @param {any} value - * @param {'html'|'svg'} language * @returns {any} */ - static unsafe(value, language) { - if (language !== 'html' && language !== 'svg') { - throw new Error(`Unexpected unsafe language "${language}". Expected "html" or "svg".`); - } + static unsafe(value) { const symbol = Object.create(null); const updater = TemplateEngine.#unsafe; - const update = { updater, value, language }; + const update = { updater, value }; TemplateEngine.#symbolToUpdate.set(symbol, update); return symbol; } @@ -1342,19 +1338,13 @@ class TemplateEngine { } } - static #unsafe(node, startNode, value, lastValue, language) { + static #unsafe(node, startNode, value, lastValue) { if (value !== lastValue) { if (typeof value === 'string') { const template = document.createElement('template'); - if (language === 'html') { - template.innerHTML = value; - TemplateEngine.#removeBetween(startNode, node); - TemplateEngine.#insertAllBefore(node.parentNode, node, template.content.childNodes); - } else { - template.innerHTML = `${value}`; - TemplateEngine.#removeBetween(startNode, node); - TemplateEngine.#insertAllBefore(node.parentNode, node, template.content.firstChild.childNodes); - } + template.innerHTML = value; + TemplateEngine.#removeBetween(startNode, node); + TemplateEngine.#insertAllBefore(node.parentNode, node, template.content.childNodes); } else { throw new Error(`Unexpected unsafe value "${value}".`); } @@ -1835,7 +1825,7 @@ class TemplateEngine { TemplateEngine.#repeat(node, startNode, update.value, update.identify, update.callback); break; case TemplateEngine.#unsafe: - TemplateEngine.#unsafe(node, startNode, update.value, lastUpdate?.value, update.language); + TemplateEngine.#unsafe(node, startNode, update.value, lastUpdate?.value); break; case TemplateEngine.#unsafeHTML: TemplateEngine.#unsafeHTML(node, startNode, update.value, lastUpdate?.value);