Skip to content

Commit 9c71f4b

Browse files
committed
Additional updates for “2.x” release.
Changes: * Deprecate `unsafeHTML` and `usafeSVG`. * Allow binding `DocumentFragment` as a value. * Tag every line in the “CHANGELOG.md” with an issue ticket. * Simplified formatting related to bindings in “TEMPLATES.md”. * Emit deprecation warnings for soon-to-be-gone interfaces.
1 parent f920e80 commit 9c71f4b

File tree

5 files changed

+594
-623
lines changed

5 files changed

+594
-623
lines changed

CHANGELOG.md

+34-23
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
### Added
1010

11-
- You can now bind attributes with `??foo="${bar}"` syntax. This is functionally
12-
equivalent to the `nullish` updater and will replace that functionality later.
13-
- A new `unsafe` updater was added to replace `unsafeHTML` and `unsafeSVG`. You
14-
use it like `unsafe(value, 'html')` and `unsafe(value, 'svg')`.
11+
- You can now bind attributes with `??foo="${bar}"` syntax in the default
12+
template engine. This is functionally equivalent to the `nullish` updater from
13+
the default template engine and will replace that functionality later (#204).
1514

1615
### Changed
1716

1817
- Template errors now include approximate line numbers from the offending
19-
template. They also print the registered custom element tag name (#201).
18+
template in the default template engine. They also print the registered custom
19+
element tag name (#201).
2020
- The `ifDefined` updater now deletes the attribute on `null` in addition to
21-
`undefined`. This makes it behave identically to `nullish`. However, both
22-
updaters are deprecated and the `??attr` binding should be used instead.
23-
- Interpolation of `textarea` is stricter. This used to be handled with some
24-
leniency — `<textarea>\n ${value} \n</textarea>`. Now, you have to fit the
25-
interpolation exactly — `<textarea></textarea>`.
21+
`undefined` in the default template engine. This makes it behave identically
22+
to `nullish` in the default template engine. However, both updaters are
23+
deprecated — the `??attr` binding should be used instead when using the
24+
default template engine (#204).
25+
- Interpolation of `textarea` is more strict in the default template engine.
26+
This used to be handled with some leniency for newlines in templates —
27+
`<textarea>\n ${value} \n</textarea>`. Now, you have to interpolate exactly —
28+
`<textarea>${value}</textarea>` (#219).
29+
- You may now bind values of type `DocumentFragment` within the template engine.
30+
In particular, this was added to enable advanced flows without needing to
31+
bloat the default template engine interface (#207, #216).
2632

2733
### Deprecated
2834

2935
- The `ifDefined` and `nullish` updaters are deprecated, update templates to use
30-
syntax like `??foo="${bar}"`.
31-
- The `repeat` updater is deprecated, use `map` instead.
32-
- The `unsafeHTML` and `unsafeSVG` updaters are deprecated, use `unsafe`.
36+
syntax like `??foo="${bar}"` (#204).
37+
- The `repeat` updater is deprecated, use `map` instead (#204).
38+
- The `unsafeHTML` and `unsafeSVG` updaters are deprecated, bind a
39+
`DocumentFragment` value instead (#207, #216).
3340
- The `plaintext` tag is no longer handled. This is a deprecated html tag which
34-
required special handling… but it’s unlikely that anyone is using that.
41+
required special handling… but it’s unlikely that anyone is using that (#220).
42+
- The `live` updater is deprecated. Use a delegated event listener for the
43+
`change` event if you need tight control over DOM state in forms (#208).
3544

3645
### Fixed
3746

38-
- Transitions from different content values should all now work. For example,
39-
you previously could not change from a text value to an array. Additionally,
40-
state is properly cleared when going from one value type to another — e.g.,
41-
when going from `unsafe` back to `null`.
42-
- The `map` updater throws immediately when given non-array input. Previously,
43-
it only threw _just before_ it was bound as content.
44-
- Dummy content cursor is no longer appended to end of template. This was an
45-
innocuous off-by-one error when creating instrumented html from the tagged
46-
template strings.
47+
- Transitions from different content values should all now work for the default
48+
template engine. For example, you previously could not change from a text
49+
value to an array. Additionally, state is properly cleared when going from one
50+
value type to another — e.g., when going from `unsafe` back to `null` (#223).
51+
- The `map` updater throws immediately when given non-array input for the
52+
default template engine. Previously, it only threw when it was bound (#222).
53+
- The `map` updater throws if the return value from the provided `identify`
54+
callback returns a duplicate value (#218).
55+
- Dummy content cursor is no longer appended to end of template for the default
56+
template engine. This was an innocuous off-by-one error when creating
57+
instrumented html from the tagged template strings (#221).
4758

4859
## [1.1.1] - 2024-11-09
4960

doc/RECIPES.md

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Recipes
2+
3+
Part of the [philosophy](../README.md#project-philosophy) for `x-element` is to
4+
implement only a minimal set of functionality. Rather than build a bespoke
5+
feature to cover each-and-every use case — we simply document how to achieve
6+
some desired outcomes via “recipes” for less common situations.
7+
8+
## How do I instantiate trusted markup?
9+
10+
In certain, _rare_ occasions, it’s acceptable to instantiate a pre-defined
11+
markup string as DOM using `innerHTML`. Rather than supply some sort of special
12+
function (e.g., `carefulWhatYouAreDoingIsUnsafe`), we trust that authors will
13+
understand the hazards of `innerHTML` and will use with care. The basic pattern
14+
here is to instantiate your markup with a `<template>` and then pass its inner
15+
`.content` (a `DocumentFragment`) into the template engine.
16+
17+
```js
18+
class MyElement extends XElement {
19+
static get properties() {
20+
return {
21+
//
22+
markup: {
23+
type: String,
24+
input: [/**/],
25+
compute: (/**/) => {/* sanitize / purify / careful out there! */},
26+
},
27+
fragment: {
28+
type: DocumentFragment,
29+
input: ['markup'],
30+
compute: (markup) => {
31+
if (markup) {
32+
const template = document.createElement('template');
33+
template.innerHTML = markup;
34+
return template.content;
35+
}
36+
},
37+
},
38+
};
39+
}
40+
static template(html) {
41+
return ({ fragment }) => {
42+
return html`
43+
<div id="container">
44+
<div id="title">The following is injected…</div>
45+
${fragment}
46+
</div>
47+
`;
48+
};
49+
}
50+
}
51+
```
52+
53+
## How do I force application state to flow the way I want in forms?
54+
55+
A common pain point when building forms is managing the _flow of data_. Does the
56+
model act as the source of truth? Or, does the DOM? Well, that’s up to you! If
57+
you _are_ trying to control forms strictly from some application state, you will
58+
need to make sure that (1) your change events propagate the right information,
59+
(2) your state is guaranteed to flow back to your view, and (3) your DOM state
60+
is correct by the time a potential form submission occurs (e.g., a submit event
61+
can follow _directly_ behind a change event in certain situations). It’s not
62+
possible to predict how authors wish to manage such cases — so it’s not possible
63+
to encode this at a library level. Here’s one way you might go about managing
64+
this though!
65+
66+
```js
67+
class MyElement extends XElement {
68+
static get properties() {
69+
return {
70+
//
71+
foo: {
72+
type: String, // You probably want this to be a string for proper comparisons.
73+
},
74+
};
75+
}
76+
static get listeners() {
77+
return {
78+
change: (host, event) => this.onChange(host, event);
79+
};
80+
}
81+
static template(html, { connected }) {
82+
return ({ foo }) => {
83+
return html`
84+
<form id="container">
85+
<input id="foo" name="foo" .value="${foo}">
86+
</form>
87+
`;
88+
};
89+
}
90+
static onChange(host, event) {
91+
if (event.target.id === 'foo') {
92+
// The user has updated the input value. Wait for the next animation
93+
// frame and re-bind our value. Note that even in this case, if a submit
94+
// follows directly behind a change event — the DOM would still contain
95+
// possibly-stale state.
96+
requestAnimationFrame(() => {
97+
const foo = host.shadowRoot.getElementById('foo');
98+
foo.value = host.foo;
99+
});
100+
}
101+
}
102+
}
103+
```

doc/TEMPLATES.md

+13-91
Original file line numberDiff line numberDiff line change
@@ -21,49 +21,19 @@ static template(html, { map }) {
2121
}
2222
```
2323

24-
The following binding types are supported:
25-
26-
| Type | Example |
27-
| :------------------ | :----------------------------------------- |
28-
| attribute | `<span id="target" foo="${bar}"></span>` |
29-
| attribute (boolean) | `<span id="target" ?foo="${bar}"></span>` |
30-
| attribute (defined) | `<span id="target" ??foo="${bar}"></span>` |
31-
| property | `<span id="target" .foo="${bar}"></span>` |
32-
| content | `<span id="target">${foo}</span>` |
33-
34-
Emulates:
35-
36-
```javascript
37-
const el = document.createElement('div');
38-
el.attachShadow({ mode: 'open' });
39-
el.innerHTML = '<span id="target"></span>';
40-
const target = el.shadowRoot.getElementById('target');
41-
42-
// attribute value bindings set the attribute value
43-
target.setAttribute('foo', bar);
44-
45-
// attribute boolean bindings set the attribute to an empty string or remove
46-
target.setAttribute('foo', ''); // when bar is truthy
47-
target.removeAttribute('foo'); // when bar is falsy
48-
49-
// attribute defined bindings set the attribute if the value is non-nullish
50-
target.setAttribute('foo', bar); // when bar is non-nullish
51-
target.removeAttribute('foo'); // when bar is nullish
52-
53-
// property bindings assign the value to the property of the node
54-
target.foo = bar;
55-
56-
// content bindings create text nodes for basic content
57-
const text = document.createTextNode('');
58-
text.textContent = foo;
59-
target.append(text);
60-
61-
// content bindings append a child for singular, nested content
62-
target.append(foo);
63-
64-
// content binding maps and appends children for arrays of nested content
65-
target.append(...foo);
66-
```
24+
The following bindings are supported:
25+
26+
| Binding | Template | Emulates |
27+
| :------------------ | :--------------------------- | :------------------------------------------------------------ |
28+
| -- | -- | `const el = document.createElement('div');` |
29+
| attribute | `<div foo="${bar}"></div>` | `el.setAttribute('foo', bar);` |
30+
| attribute (boolean) | `<div ?foo="${bar}"></div>` | `el.setAttribute('foo', ''); // if “bar” is truthy` |
31+
| -- | -- | `el.removeAttribute('foo'); // if “bar” is falsy` |
32+
| attribute (defined) | `<div ??foo="${bar}"></div>` | `el.setAttribute('foo', bar); // if “bar” is non-nullish` |
33+
| -- | -- | `el.removeAttribute('foo'); // if “bar” is nullish` |
34+
| property | `<div .foo="${bar}"></div>` | `el.foo = bar;` |
35+
| content | `<div>${foo}</div>` | `el.append(document.createTextNode(foo)) // if “bar” is text` |
36+
| -- | -- | (see [content binding](#content-binding) for composition) |
6737

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

@@ -81,8 +51,6 @@ The following template languages are supported:
8151
The following value updaters are supported:
8252

8353
* `map` (can be used with content bindings)
84-
* `unsafe` (can be used with content bindings)
85-
* `live` (can be used with property bindings)
8654

8755
**A note on non-primitive data:**
8856

@@ -216,23 +184,6 @@ html`<div .foo="${bar}"></div>`;
216184
// el.foo = bar;
217185
```
218186

219-
#### The `live` property binding
220-
221-
You can wrap the property being bound in the `live` updater to ensure that each
222-
`render` call will sync the template‘s value into the DOM. This is primarily
223-
used to control form inputs.
224-
225-
```js
226-
const bar = 'something';
227-
html`<input .value="${live(bar)}">`;
228-
// <input>
229-
// el.value = bar;
230-
```
231-
232-
The key difference to note is that the basic property binding will not attempt
233-
to perform an update if `value === lastValue`. The `live` binding will instead
234-
check if `value === el.value` whenever a `render` is kicked off.
235-
236187
### Content binding
237188

238189
The content binding does different things based on the value type passed in.
@@ -324,35 +275,6 @@ html`<div>${bar}</div>`;
324275
// <div><span>♥1</span>…<span>♣A</span></div>
325276
```
326277

327-
#### The `unsafe` content binding
328-
329-
The `unsafe` content binding allows you to parse / instantiate text from a
330-
trusted source. This should _only_ be used to inject trusted content — never
331-
user content.
332-
333-
```js
334-
const bar = '<script>console.prompt("can you hear me now?")</script>';
335-
html`<div>${unsafe(bar, 'html')}</div>`;
336-
// <div><script>console.prompt("can you hear me now?")</script></div>
337-
// console.prompt('can you hear me now?');
338-
339-
const bar = '<circle cx="50" cy="50" r="50"></circle>';
340-
html`
341-
<svg
342-
xmlns="http://www.w3.org/2000/svg"
343-
viewBox="0 0 100 100">
344-
${unsafe(bar, 'svg')}
345-
</svg>
346-
`;
347-
//
348-
// <svg
349-
// xmlns="http://www.w3.org/2000/svg"
350-
// viewBox="0 0 100 100">
351-
// <circle cx="50" cy="50" r="50"></circle>
352-
// </svg>
353-
//
354-
```
355-
356278
## Customizing your base class
357279

358280
Following is a working example using [lit-html](https://lit.dev):

0 commit comments

Comments
 (0)