Skip to content

Commit 9460060

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. * Deprecate remaining updaters (e.g., “map”). * Ditch unecessary “weak maps”.† * Some performance improvements. † The search set for the weak maps we’re using gets untenable since it’s a flat list of pointers for _all_ results. It feels like a reasonable concession to hang data off of a unique symbol key. These are non-enumerable (unless specifically trying to enumerate symbols), which feels internal enough. Additionally, returning the values passed in by the user doesn’t feel like much of a leaking abstraction.
1 parent a0adb0f commit 9460060

6 files changed

+843
-956
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

+29-108
Original file line numberDiff line numberDiff line change
@@ -8,62 +8,33 @@ Add a static template function in your `x-element` definition in order to
88
leverage automagical DOM generation and data binding:
99

1010
```javascript
11-
static template(html, { map }) {
11+
static template(html) {
1212
return ({ options, selectedId }) => {
1313
return html`
1414
<select name="my-options">
15-
${map(options, option => option.id, option => html`
16-
<option value="${option.value}" ?selected="${option.id === selectedId}">
17-
`)}
15+
${options.map(option => [
16+
option.id,
17+
html`<option value="${option.value}" ?selected="${option.id === selectedId}">`,
18+
])}
1819
</select>
1920
`;
2021
};
2122
}
2223
```
2324

24-
The following binding types are supported:
25+
The following bindings are supported:
2526

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-
```
27+
| Binding | Template | Emulates |
28+
| :------------------ | :--------------------------- | :------------------------------------------------------------ |
29+
| -- | -- | `const el = document.createElement('div');` |
30+
| attribute | `<div foo="${bar}"></div>` | `el.setAttribute('foo', bar);` |
31+
| attribute (boolean) | `<div ?foo="${bar}"></div>` | `el.setAttribute('foo', ''); // if “bar” is truthy` |
32+
| -- | -- | `el.removeAttribute('foo'); // if “bar” is falsy` |
33+
| attribute (defined) | `<div ??foo="${bar}"></div>` | `el.setAttribute('foo', bar); // if “bar” is non-nullish` |
34+
| -- | -- | `el.removeAttribute('foo'); // if “bar” is nullish` |
35+
| property | `<div .foo="${bar}"></div>` | `el.foo = bar;` |
36+
| content | `<div>${foo}</div>` | `el.append(document.createTextNode(foo)) // if “bar” is text` |
37+
| -- | -- | (see [content binding](#content-binding) for composition) |
6738

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

@@ -78,12 +49,6 @@ The following template languages are supported:
7849
* `html`
7950
* `svg`
8051

81-
The following value updaters are supported:
82-
83-
* `map` (can be used with content bindings)
84-
* `unsafe` (can be used with content bindings)
85-
* `live` (can be used with property bindings)
86-
8752
**A note on non-primitive data:**
8853

8954
Because DOM manipulation is *slow* — template engines do their best to avoid it
@@ -216,23 +181,6 @@ html`<div .foo="${bar}"></div>`;
216181
// el.foo = bar;
217182
```
218183

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-
236184
### Content binding
237185

238186
The content binding does different things based on the value type passed in.
@@ -283,7 +231,7 @@ html`<div>${bar}</div>`;
283231

284232
#### Array content binding
285233

286-
When the content being bound is an array of template results, you get a mapping.
234+
When the content being bound is an array of template results, you get a list.
287235

288236
```js
289237
const bar = [
@@ -300,14 +248,16 @@ html`<div>${bar}</div>`;
300248
// <div><span>one</span><span>two</span></div>
301249
```
302250

303-
#### The `map` content binding
251+
#### Map content binding
304252

305-
The `map` content binding adds some special behavior on top of the basic array
306-
content binding. In particular, it _keeps track_ of each child node based on
307-
an `identify` function declared by the caller. This enables the template engine
308-
to _move_ child nodes under certain circumstances (versus having to constantly
309-
destroy and recreate). And that shuffling behavior enables authors to animate
310-
DOM nodes across such transitions.
253+
When the content being bound is an array of key-value map entries (where the
254+
`key` is a unique string within the list and the `value` is a template result),
255+
you get also list. But, this value will come with some special behavior on top
256+
of the basic array content binding. In particular, it _keeps track_ of each
257+
child node based on the given `key` you declare. This enables the template
258+
engine to _move_ child nodes under certain circumstances (versus having to
259+
constantly destroy and recreate). And that shuffling behavior enables authors to
260+
animate DOM nodes across such transitions.
311261

312262
```js
313263
// Note that you can shuffle the deck without destroying / creating DOM.
@@ -318,41 +268,12 @@ const deck = [
318268
];
319269
const items = deck;
320270
const identify = item => item.id;
321-
const callback = item => html`<span>${item.text}</span>`;
322-
const bar = map(items, identify, callback);
271+
const template = item => html`<span>${item.text}</span>`;
272+
const bar = items.map(item => [identify(item), template(item)]);
323273
html`<div>${bar}</div>`;
324274
// <div><span>♥1</span>…<span>♣A</span></div>
325275
```
326276

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-
356277
## Customizing your base class
357278

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

0 commit comments

Comments
 (0)