Skip to content

Commit

Permalink
Restrict “unsafe” usage for initial “2.x” release.
Browse files Browse the repository at this point in the history
Changes:
* So far, we only use “unsafe” for html — so we just keep that for now.
* Stopped assuming that the “template engine” discussed in the
  “CHANGELOG.md” is the “default template engine” — i.e., we explicitly
  write that now.
* Simplified formatting related to bindings in “TEMPLATES.md”.
  • Loading branch information
theengineear committed Nov 19, 2024
1 parent f920e80 commit bc23055
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 252 deletions.
48 changes: 27 additions & 21 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 — `<textarea>\n ${value} \n</textarea>`. Now, you have to fit the
interpolation exactly — `<textarea></textarea>`.
`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 —
`<textarea>\n ${value} \n</textarea>`. Now, you have to interpolate exactly —
`<textarea>${value}</textarea>`.

### 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

Expand Down
56 changes: 13 additions & 43 deletions doc/TEMPLATES.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,49 +21,19 @@ static template(html, { map }) {
}
```

The following binding types are supported:

| Type | Example |
| :------------------ | :----------------------------------------- |
| attribute | `<span id="target" foo="${bar}"></span>` |
| attribute (boolean) | `<span id="target" ?foo="${bar}"></span>` |
| attribute (defined) | `<span id="target" ??foo="${bar}"></span>` |
| property | `<span id="target" .foo="${bar}"></span>` |
| content | `<span id="target">${foo}</span>` |

Emulates:

```javascript
const el = document.createElement('div');
el.attachShadow({ mode: 'open' });
el.innerHTML = '<span id="target"></span>';
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 | `<div foo="${bar}"></div>` | `el.setAttribute('foo', bar);` |
| attribute (boolean) | `<div ?foo="${bar}"></div>` | `el.setAttribute('foo', ''); // if “bar” is truthy` |
| -- | -- | `el.removeAttribute('foo'); // if “bar” is falsy` |
| attribute (defined) | `<div ??foo="${bar}"></div>` | `el.setAttribute('foo', bar); // if “bar” is non-nullish` |
| -- | -- | `el.removeAttribute('foo'); // if “bar” is nullish` |
| property | `<div .foo="${bar}"></div>` | `el.foo = bar;` |
| content | `<div>${foo}</div>` | `el.append(document.createTextNode(foo)) // if “bar” is text` |
| -- | -- | (see [content binding](#content-binding) for composition) |

**Important note on serialization during data binding:**

Expand Down
59 changes: 8 additions & 51 deletions test/test-template-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@ describe('html updaters', () => {

it('unsafe html', () => {
const getTemplate = ({ content }) => {
return html`<div id="target">${unsafe(content, 'html')}</div>`;
return html`<div id="target">${unsafe(content)}</div>`;
};
const container = document.createElement('div');
document.body.append(container);
Expand Down Expand Up @@ -892,7 +892,7 @@ describe('html updaters', () => {
const resolve = (type, value) => {
switch(type) {
case 'map': return map(value, item => item.id, item => html`<div id="${item.id}"></div>`);
case 'html': return unsafe(value, 'html');
case 'html': return unsafe(value);
default: return value; // E.g., an array, some text, null, undefined, etc.
}
};
Expand Down Expand Up @@ -1032,31 +1032,6 @@ describe('svg rendering', () => {
});

describe('svg updaters', () => {
it('unsafe svg', () => {
const getTemplate = ({ content }) => {
return html`
<svg
id="target"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
style="width: 100px; height: 100px;">
${unsafe(content, 'svg')}
</svg>
`;
};
const container = document.createElement('div');
document.body.append(container);
render(container, getTemplate({ content: '<circle id="injected" r="10" cx="50" cy="50"></circle>' }));
assert(!!container.querySelector('#injected'));
assert(container.querySelector('#injected').getBoundingClientRect().height = 20);
assert(container.querySelector('#injected').getBoundingClientRect().width = 20);
render(container, getTemplate({ content: '<circle id="injected" r="5" cx="50" cy="50"></circle>' }));
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`
Expand Down Expand Up @@ -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`<div id="target" maybe="${unsafe(maybe, 'css')}"></div>`;
};
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`<div id="target" maybe="${unsafe(maybe, 'html')}"></div>`;
return html`<div id="target" maybe="${unsafe(maybe)}"></div>`;
};
const container = document.createElement('div');
document.body.append(container);
Expand All @@ -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`<div id="target" ?maybe="${unsafe(maybe, 'html')}"></div>`;
return html`<div id="target" ?maybe="${unsafe(maybe)}"></div>`;
};
const container = document.createElement('div');
document.body.append(container);
Expand All @@ -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`<div id="target" ??maybe="${unsafe(maybe, 'html')}"></div>`;
return html`<div id="target" ??maybe="${unsafe(maybe)}"></div>`;
};
const container = document.createElement('div');
document.body.append(container);
Expand All @@ -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`<div id="target" .maybe="${unsafe(maybe, 'html')}"></div>`;
return html`<div id="target" .maybe="${unsafe(maybe)}"></div>`;
};
const container = document.createElement('div');
document.body.append(container);
Expand All @@ -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`<textarea id="target">${unsafe(maybe, 'html')}</textarea>`;
return html`<textarea id="target">${unsafe(maybe)}</textarea>`;
};
const container = document.createElement('div');
document.body.append(container);
Expand All @@ -1588,7 +1545,7 @@ describe('rendering errors', () => {
const getTemplate = ({ content }) => {
return html`
<div id="target">
${unsafe(content, 'html')}
${unsafe(content)}
</div>
`;
};
Expand Down
Loading

0 comments on commit bc23055

Please sign in to comment.