Skip to content

Commit

Permalink
Additional updates for “2.x” release.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
theengineear committed Nov 20, 2024
1 parent f920e80 commit 69c8936
Show file tree
Hide file tree
Showing 5 changed files with 463 additions and 495 deletions.
57 changes: 34 additions & 23 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,53 @@ 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 (#204).

### 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>` (#219).
- You may now bind values of type `DocumentFragment` within the template engine.
In particular, this was added to enable advanced flows without needing to
bloat the default template engine interface (#207, #216).

### 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`.
syntax like `??foo="${bar}"` (#204).
- The `repeat` updater is deprecated, use `map` instead (#204).
- The `unsafeHTML` and `unsafeSVG` updaters are deprecated, bind a
`DocumentFragment` value instead (#207, #216).
- 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.
required special handling… but it’s unlikely that anyone is using that (#220).
- The `live` updater is deprecated. Use a delegated event listener for the
`change` event if you need tight control over DOM state in forms (#208).

### 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` (#223).
- The `map` updater throws immediately when given non-array input for the
default template engine. Previously, it only threw when it was bound (#222).
- The `map` updater throws if the return value from the provided `identify`
callback returns a duplicate value (#218).
- 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 (#221).

## [1.1.1] - 2024-11-09

Expand Down
103 changes: 103 additions & 0 deletions doc/RECIPES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Recipes

Part of the [philosophy](../README.md#project-philosophy) for `x-element` is to
implement only a minimal set of functionality. Rather than build a bespoke
feature to cover each-and-every use case — we simply document how to achieve
some desired outcomes via “recipes” for less common situations.

## How do I instantiate trusted markup?

In certain, _rare_ occasions, it’s acceptable to instantiate a pre-defined
markup string as DOM using `innerHTML`. Rather than supply some sort of special
function (e.g., `carefulWhatYouAreDoingIsUnsafe`), we trust that authors will
understand the hazards of `innerHTML` and will use with care. The basic pattern
here is to instantiate your markup with a `<template>` and then pass its inner
`.content` (a `DocumentFragment`) into the template engine.

```js
class MyElement extends XElement {
static get properties() {
return {
//
markup: {
type: String,
input: [/**/],
compute: (/**/) => {/* sanitize / purify / careful out there! */},
},
fragment: {
type: DocumentFragment,
input: ['markup'],
compute: (markup) => {
if (markup) {
const template = document.createElement('template');
template.innerHTML = markup;
return template.content;
}
},
},
};
}
static template(html) {
return ({ fragment }) => {
return html`
<div id="container">
<div id="title">The following is injected…</div>
${fragment}
</div>
`;
};
}
}
```

## How do I force application state to flow the way I want in forms?

A common pain point when building forms is managing the _flow of data_. Does the
model act as the source of truth? Or, does the DOM? Well, that’s up to you! If
you _are_ trying to control forms strictly from some application state, you will
need to make sure that (1) your change events propagate the right information,
(2) your state is guaranteed to flow back to your view, and (3) your DOM state
is correct by the time a potential form submission occurs (e.g., a submit event
can follow _directly_ behind a change event in certain situations). It’s not
possible to predict how authors wish to manage such cases — so it’s not possible
to encode this at a library level. Here’s one way you might go about managing
this though!

```js
class MyElement extends XElement {
static get properties() {
return {
//
foo: {
type: String, // You probably want this to be a string for proper comparisons.
},
};
}
static get listeners() {
return {
change: (host, event) => this.onChange(host, event);
};
}
static template(html, { connected }) {
return ({ foo }) => {
return html`
<form id="container">
<input id="foo" name="foo" .value="${foo}">
</form>
`;
};
}
static onChange(host, event) {
if (event.target.id === 'foo') {
// The user has updated the input value. Wait for the next animation
// frame and re-bind our value. Note that even in this case, if a submit
// follows directly behind a change event — the DOM would still contain
// possibly-stale state.
requestAnimationFrame(() => {
const foo = host.shadowRoot.getElementById('foo');
foo.value = host.foo;
});
}
}
}
```
104 changes: 13 additions & 91 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 All @@ -81,8 +51,6 @@ The following template languages are supported:
The following value updaters are supported:

* `map` (can be used with content bindings)
* `unsafe` (can be used with content bindings)
* `live` (can be used with property bindings)

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

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

#### The `live` property binding

You can wrap the property being bound in the `live` updater to ensure that each
`render` call will sync the template‘s value into the DOM. This is primarily
used to control form inputs.

```js
const bar = 'something';
html`<input .value="${live(bar)}">`;
// <input>
// el.value = bar;
```

The key difference to note is that the basic property binding will not attempt
to perform an update if `value === lastValue`. The `live` binding will instead
check if `value === el.value` whenever a `render` is kicked off.

### Content binding

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

#### The `unsafe` content binding

The `unsafe` content binding allows you to parse / instantiate text from a
trusted source. This should _only_ be used to inject trusted content — never
user content.

```js
const bar = '<script>console.prompt("can you hear me now?")</script>';
html`<div>${unsafe(bar, 'html')}</div>`;
// <div><script>console.prompt("can you hear me now?")</script></div>
// console.prompt('can you hear me now?');

const bar = '<circle cx="50" cy="50" r="50"></circle>';
html`
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100">
${unsafe(bar, 'svg')}
</svg>
`;
//
// <svg
// xmlns="http://www.w3.org/2000/svg"
// viewBox="0 0 100 100">
// <circle cx="50" cy="50" r="50"></circle>
// </svg>
//
```

## Customizing your base class

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

0 comments on commit 69c8936

Please sign in to comment.