Skip to content

Commit d6aefb9

Browse files
committed
Integrate with ElementInternals
Closes [#1023][] Replace the requirement for an `<input type="hidden">` element with direct `<form>` integration through built-in support for [ElementInternals][]. According to the [Form-associated custom elements][] section of [More capable form controls][], various behaviors that the `<trix-editor>` element was recreating are provided out of the box. For example, the `<input type="hidden">`-`[input]` attribute pairing can be achieved through [ElementInternals.setFormValue][]. Similarly, the `<label>` element support can be achieved through [ElementInternals.labels][]. [#1023]: #1023 [ElementInternals]: https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals [Form-associated custom elements]: https://web.dev/articles/more-capable-form-controls#form-associated_custom_elements [More capable form controls]: https://web.dev/articles/more-capable-form-controls [ElementInternals.setFormValue]: https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/setFormValue [ElementInternals.labels]: https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/labels
1 parent bc24ac5 commit d6aefb9

17 files changed

+227
-31
lines changed

.github/workflows/ci.yml

+6-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ on:
88

99
jobs:
1010
build:
11-
name: Browser tests
11+
name: "Browser tests (formAssociated: ${{ matrix.formAssociated }})"
1212
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
elementInternals: [false, true]
16+
env:
17+
EDITOR_FORM_ASSOCIATED: "${{ matrix.formAssociated }}"
1318
steps:
1419
- uses: actions/checkout@v3
1520
- uses: actions/setup-node@v3

README.md

+55-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ This is the approach that all modern, production ready, WYSIWYG editors now take
1919

2020
<details><summary>Trix supports all evergreen, self-updating desktop and mobile browsers.</summary><img src="https://app.saucelabs.com/browser-matrix/basecamp_trix.svg"></details>
2121

22-
Trix is built with established web standards, notably [Custom Elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements), [Mutation Observer](https://dom.spec.whatwg.org/#mutation-observers), and [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
22+
Trix is built with established web standards, notably [Custom Elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements), [Element Internals](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals), [Mutation Observer](https://dom.spec.whatwg.org/#mutation-observers), and [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
2323

2424
# Getting Started
2525

@@ -49,12 +49,26 @@ document.addEventListener("trix-before-initialize", () => {
4949

5050
## Creating an Editor
5151

52-
Place an empty `<trix-editor></trix-editor>` tag on the page. Trix will automatically insert a separate `<trix-toolbar>` before the editor.
52+
Place an empty `<trix-editor></trix-editor>` tag on the page. If the `<trix-editor>` element is rendered with a `[toolbar]` attribute that references the element by its `[id]`, it will treat that element as its toolbar:
53+
54+
```html
55+
<trix-toolbar id="editor_toolbar"></trix-toolbar>
56+
57+
<trix-editor toolbar="editor_toolbar"></trix-editor>
58+
```
59+
60+
Otherwise, Trix will automatically insert a separate `<trix-toolbar>` before the editor.
5361

5462
Like an HTML `<textarea>`, `<trix-editor>` accepts `autofocus` and `placeholder` attributes. Unlike a `<textarea>`, `<trix-editor>` automatically expands vertically to fit its contents.
5563

5664
## Integrating With Forms
5765

66+
There are two styles of integrating with `<form>` element submissions.
67+
68+
### Legacy integration with `<input type="hidden">`
69+
70+
Legacy support is provided through an `<input type="hidden">` element paired with an `[input]` attribute on the `<trix-editor>` element.
71+
5872
To submit the contents of a `<trix-editor>` with a form, first define a hidden input field in the form and assign it an `id`. Then reference that `id` in the editor’s `input` attribute.
5973

6074
```html
@@ -66,7 +80,7 @@ To submit the contents of a `<trix-editor>` with a form, first define a hidden i
6680

6781
Trix will automatically update the value of the hidden input field with each change to the editor.
6882

69-
## Populating With Stored Content
83+
#### Populating With Stored Content
7084

7185
To populate a `<trix-editor>` with stored content, include that content in the associated input element’s `value` attribute.
7286

@@ -79,6 +93,44 @@ To populate a `<trix-editor>` with stored content, include that content in the a
7993

8094
Always use an associated input element to safely populate an editor. Trix won’t load any HTML content inside a `<trix-editor>…</trix-editor>` tag.
8195

96+
### Integration with Element Internals
97+
98+
Trix can also be configured to integrate with forms through the `<trix-editor>` element's `ElementInternals` instance.
99+
100+
First, configure Trix to opt-into its Element Internals support by rendering a `<meta>` element into the document's `<head>`:
101+
102+
```html
103+
<head>
104+
<!---->
105+
<meta name="trix-editor-formAssociated" content="true">
106+
</head>
107+
```
108+
109+
Then, to submit the contents of a `<trix-editor>` with a `<form>`, render the element with a `[name]` attribute and its initial value as its inner HTML.
110+
111+
```html
112+
<form >
113+
<trix-editor name="content"></trix-editor>
114+
</form>
115+
```
116+
117+
To associate the element with a `<form>` that isn't an ancestor, render the element with a `[form]` attribute that references the `<form>` element by its `[id]`:
118+
119+
```html
120+
<form id="a-form-element" ></form>
121+
<trix-editor name="content" form="a-form-element"></trix-editor>
122+
```
123+
124+
#### Populating With Stored Content
125+
126+
To populate a `<trix-editor>` with stored content, include that content as HTML inside the element’s inner HTML.
127+
128+
```html
129+
<form >
130+
<trix-editor>Editor content goes here</trix-editor>
131+
</form>
132+
```
133+
82134
## Providing an Accessible Name
83135

84136
Like other form controls, `<trix-editor>` elements should have an accessible name. The `<trix-editor>` element integrates with `<label>` elements and The `<trix-editor>` supports two styles of integrating with `<label>` elements:

assets/index.html

+20-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
box-sizing: border-box;
1313
}
1414

15-
main {
15+
main, nav {
1616
margin: 20px auto;
1717
max-width: 700px;
1818
}
@@ -69,8 +69,27 @@
6969
}
7070
});
7171
</script>
72+
<script>
73+
const searchParams = new URLSearchParams(location.search)
74+
75+
if (searchParams.get("editor") === "formAssociated") {
76+
document.head.insertAdjacentHTML(
77+
"beforeend",
78+
`<meta name="trix-editor-formAssociated" content="true">`
79+
)
80+
81+
document.addEventListener("trix-change", function(event) {
82+
var input = document.getElementById("input")
83+
input.value = event.target.value
84+
})
85+
}
86+
</script>
7287
</head>
7388
<body>
89+
<nav>
90+
<a href="/">Legacy support</a>
91+
<a href="/?editor=formAssociated">ElementInternals support</a>
92+
</nav>
7493
<main>
7594
<label for="editor">Input</label>
7695
<trix-editor autofocus class="trix-content" input="input" id="editor"></trix-editor>

karma.conf.js

+4
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ if (process.env.SAUCE_ACCESS_KEY) {
101101
}
102102
}
103103

104+
if (process.env.EDITOR_FORM_ASSOCIATED === "true") {
105+
config.files.unshift({ pattern: "src/test/test_helpers/fixtures/form_associated.js", watched: false, included: true })
106+
}
107+
104108
function buildId() {
105109
const { GITHUB_WORKFLOW, GITHUB_RUN_NUMBER, GITHUB_RUN_ID } = process.env
106110
return GITHUB_WORKFLOW && GITHUB_RUN_NUMBER && GITHUB_RUN_ID

src/test/system/accessibility_test.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { assert, test, testGroup, triggerEvent } from "test/test_helper"
1+
import * as config from "trix/config"
2+
import { assert, test, testGroup, testIf, testUnless, triggerEvent } from "test/test_helper"
23

34
testGroup("Accessibility attributes", { template: "editor_default_aria_label" }, () => {
45
test("sets the role to textbox", () => {
@@ -22,17 +23,26 @@ testGroup("Accessibility attributes", { template: "editor_default_aria_label" },
2223
assert.equal(editor.getAttribute("aria-labelledby"), "aria-labelledby-id")
2324
})
2425

25-
test("assigns aria-label to the text of the element's <label> elements", () => {
26+
testUnless(config.editor.formAssociated, "assigns aria-label to the text of the element's <label> elements", () => {
2627
const editor = document.getElementById("editor-with-labels")
2728
assert.equal(editor.getAttribute("aria-label"), "Label 1 Label 2 Label 3")
2829
})
2930

30-
test("updates the aria-label on focus", () => {
31+
testUnless(config.editor.formAssociated, "updates the aria-label on focus", () => {
3132
const editor = document.getElementById("editor-with-modified-label")
3233
const label = document.getElementById("modified-label")
3334

3435
label.innerHTML = "<span>New Value</span>"
3536
triggerEvent(editor, "focus")
3637
assert.equal(editor.getAttribute("aria-label"), "New Value")
3738
})
39+
40+
testIf(config.editor.formAssociated, "does not set [aria-label] for a <label> element", () => {
41+
const editor = document.getElementById("editor-with-labels")
42+
const labels = Array.from(editor.labels)
43+
const text = labels.map((label) => label.textContent.trim())
44+
45+
assert.deepEqual(text, [ "Label 1", "Label 2", "Label 3" ])
46+
assert.equal(editor.getAttribute("aria-label"), null)
47+
})
3848
})

src/test/system/custom_element_test.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as config from "trix/config"
12
import { rangesAreEqual } from "trix/core/helpers"
23

34
import {
@@ -13,6 +14,7 @@ import {
1314
test,
1415
testGroup,
1516
testIf,
17+
testUnless,
1618
triggerEvent,
1719
typeCharacters,
1820
typeInToolbarDialog,
@@ -510,12 +512,12 @@ testGroup("<label> support", { template: "editor_with_labels" }, () => {
510512
assert.deepEqual(Array.from(getEditorElement().labels), labels)
511513
})
512514

513-
test("focuses when <label> clicked", () => {
515+
testUnless(config.editor.formAssociated, "focuses when <label> clicked", () => {
514516
document.getElementById("label-1").click()
515517
assert.equal(getEditorElement(), document.activeElement)
516518
})
517519

518-
test("focuses when <label> descendant clicked", () => {
520+
testUnless(config.editor.formAssociated, "focuses when <label> descendant clicked", () => {
519521
document.getElementById("label-1").querySelector("span").click()
520522
assert.equal(getEditorElement(), document.activeElement)
521523
})

src/test/system/installation_process_test.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import * as config from "trix/config"
12
import EditorController from "trix/controllers/editor_controller"
23

3-
import { assert, test, testGroup } from "test/test_helper"
4+
import { assert, test, testGroup, testUnless } from "test/test_helper"
45
import { nextFrame } from "../test_helpers/timing_helpers"
56

67
testGroup("Installation process", { template: "editor_html" }, () => {
@@ -31,7 +32,7 @@ testGroup("Installation process without specified elements", { template: "editor
3132
assert.equal(editorElement.toolbarElement, toolbarElement)
3233
})
3334

34-
test("creates identified input elements", () => {
35+
testUnless(config.editor.formAssociated, "creates identified input elements", () => {
3536
const editorElement = getEditorElement()
3637

3738
const inputId = editorElement.getAttribute("input")
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import * as config from "trix/config"
2+
13
export default () =>
2-
`<input id="my_input" type="hidden" value="&lt;div&gt;Hello world&lt;/div&gt;">
3-
<trix-editor input="my_input" autofocus placeholder="Say hello..."></trix-editor>`
4+
config.editor.formAssociated ?
5+
`<trix-editor autofocus placeholder="Say hello..."><div>Hello world</div></trix-editor>
6+
` :
7+
`<input id="my_input" type="hidden" value="&lt;div&gt;Hello world&lt;/div&gt;">
8+
<trix-editor input="my_input" autofocus placeholder="Say hello..."></trix-editor>`
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import * as config from "trix/config"
12
import { TEST_IMAGE_URL } from "./test_image_url"
23

34
export default () =>
4-
`<trix-editor input="my_input" autofocus placeholder="Say hello..."></trix-editor>
5-
<input id="my_input" type="hidden" value="ab&lt;img src=&quot;${TEST_IMAGE_URL}&quot; width=&quot;10&quot; height=&quot;10&quot;&gt;">`
5+
config.editor.formAssociated ?
6+
`<trix-editor autofocus placeholder="Say hello...">ab<img src="${TEST_IMAGE_URL}" width="10" height="10"></trix-editor>
7+
` :
8+
`<trix-editor input="my_input" autofocus placeholder="Say hello..."></trix-editor>
9+
<input id="my_input" type="hidden" value="ab&lt;img src=&quot;${TEST_IMAGE_URL}&quot; width=&quot;10&quot; height=&quot;10&quot;&gt;">`
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1+
import * as config from "trix/config"
2+
13
export default () =>
2-
`<ul id="my_editor">
3-
<li><trix-toolbar id="my_toolbar"></trix-toolbar></li>
4-
<li><trix-editor toolbar="my_toolbar" input="my_input" autofocus placeholder="Say hello..."></trix-editor></li>
5-
<li><input id="my_input" type="hidden" value="&lt;div&gt;Hello world&lt;/div&gt;"></li>
6-
</ul>`
4+
config.editor.formAssociated ?
5+
`<ul id="my_editor">
6+
<li><trix-toolbar id="my_toolbar"></trix-toolbar></li>
7+
<li><trix-editor toolbar="my_toolbar" autofocus placeholder="Say hello..."><div>Hello world</div></trix-editor></li>
8+
</ul>
9+
` :
10+
`<ul id="my_editor">
11+
<li><trix-toolbar id="my_toolbar"></trix-toolbar></li>
12+
<li><trix-editor toolbar="my_toolbar" input="my_input" autofocus placeholder="Say hello..."></trix-editor></li>
13+
<li><input id="my_input" type="hidden" value="&lt;div&gt;Hello world&lt;/div&gt;"></li>
14+
</ul>`
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1+
import * as config from "trix/config"
2+
13
export default () =>
2-
`<form id="ancestor-form">
3-
<trix-editor id="editor-with-ancestor-form"></trix-editor>
4-
</form>
4+
config.editor.formAssociated ?
5+
`<form id="ancestor-form">
6+
<trix-editor id="editor-with-ancestor-form"></trix-editor>
7+
</form>
8+
<form id="input-form"></form>
9+
<trix-editor id="editor-with-input-form" form="input-form"></trix-editor>
10+
<trix-editor id="editor-with-no-form"></trix-editor>
11+
` :
12+
`<form id="ancestor-form">
13+
<trix-editor id="editor-with-ancestor-form"></trix-editor>
14+
</form>
515
6-
<form id="input-form">
7-
<input type="hidden" id="hidden-input">
8-
</form>
9-
<trix-editor id="editor-with-input-form" input="hidden-input"></trix-editor>
16+
<form id="input-form">
17+
<input type="hidden" id="hidden-input">
18+
</form>
19+
<trix-editor id="editor-with-input-form" input="hidden-input"></trix-editor>
1020
11-
<trix-editor id="editor-with-no-form"></trix-editor>`
21+
<trix-editor id="editor-with-no-form"></trix-editor>`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
document.head.insertAdjacentHTML(
2+
"beforeend",
3+
`<meta name="trix-editor-formAssociated" content="true">
4+
`)

src/test/test_helpers/test_helpers.js

+4
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ export const testIf = function (condition, ...args) {
5656
}
5757
}
5858

59+
export const testUnless = function(condition, ...args) {
60+
testIf(!condition, ...args)
61+
}
62+
5963
export const { skip, test } = QUnit
6064

6165
const waitForTrixInit = async () => {

src/trix/config/editor.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const config = {
2+
formAssociated: false
3+
}
4+
5+
export default config

src/trix/config/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { default as attachments } from "./attachments"
22
export { default as blockAttributes } from "./block_attributes"
33
export { default as browser } from "./browser"
44
export { default as css } from "./css"
5+
export { default as editor } from "./editor"
56
export { default as fileSize } from "./file_size_formatting"
67
export { default as input } from "./input"
78
export { default as keyNames } from "./key_names"

0 commit comments

Comments
 (0)