Skip to content

Commit f1bdb3a

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 10573a8 commit f1bdb3a

12 files changed

+107
-123
lines changed

README.md

+42-10
Original file line numberDiff line numberDiff line change
@@ -49,36 +49,68 @@ 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

58-
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.
66+
Like other form controls, the `<trix-editor>` element supports two styles of integrating with `<label>` elements:
67+
68+
1. render the `<trix-editor>` element with an `[id]` attribute that the `<label>` element references through its `[for]` attribute:
69+
70+
```html
71+
<label for="editor">Editor</label>
72+
<trix-editor id="editor"></trix-editor>
73+
```
74+
75+
2. render the `<trix-editor>` element as a child of the `<label>` element:
76+
77+
```html
78+
<trix-toolbar id="editor-toolbar"></trix-toolbar>
79+
<label>
80+
Editor
81+
82+
<trix-editor toolbar="editor-toolbar"></trix-editor>
83+
</label>
84+
```
85+
86+
> [!WARNING]
87+
> When rendering the `<trix-editor>` element as a child of the `<label>` element, [explicitly render](#creating-an-editor) the corresponding `<trix-toolbar>` element outside of the `<label>` element.
88+
89+
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.
5990

6091
```html
6192
<form >
62-
<input id="x" type="hidden" name="content">
63-
<trix-editor input="x"></trix-editor>
93+
<trix-editor name="content"></trix-editor>
6494
</form>
6595
```
6696

67-
Trix will automatically update the value of the hidden input field with each change to the editor.
97+
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]`:
98+
99+
```html
100+
<form id="a-form-element" ></form>
101+
<trix-editor name="content" form="a-form-element"></trix-editor>
102+
```
68103

69104
## Populating With Stored Content
70105

71-
To populate a `<trix-editor>` with stored content, include that content in the associated input element’s `value` attribute.
106+
To populate a `<trix-editor>` with stored content, include that content as HTML inside the element’s inner HTML.
72107

73108
```html
74109
<form >
75-
<input id="x" value="Editor content goes here" type="hidden" name="content">
76-
<trix-editor input="x"></trix-editor>
110+
<trix-editor>Editor content goes here</trix-editor>
77111
</form>
78112
```
79113

80-
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.
81-
82114
## Styling Formatted Content
83115

84116
To ensure what you see when you edit is what you see when you save, use a CSS class name to scope styles for Trix formatted content. Apply this class name to your `<trix-editor>` element, and to a containing element when you render stored Trix content for display in your application.

assets/index.html

+7-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
Trix.Inspector.install(event.target);
4242
});
4343

44+
document.addEventListener("trix-change", function(event) {
45+
var input = document.getElementById("input")
46+
input.value = event.target.value
47+
})
48+
4449
document.addEventListener("trix-attachment-add", function(event) {
4550
var attachment = event.attachment;
4651
if (attachment.file) {
@@ -72,7 +77,8 @@
7277
</head>
7378
<body>
7479
<main>
75-
<trix-editor autofocus class="trix-content" input="input"></trix-editor>
80+
<label for="editor">Input</label>
81+
<trix-editor autofocus id="editor" class="trix-content"></trix-editor>
7682
<details id="output">
7783
<summary>Output</summary>
7884
<textarea readonly id="input"></textarea>

src/test/system/custom_element_test.js

+19-22
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ testGroup("Custom element API", { template: "editor_empty" }, () => {
442442

443443
test("editor resets to its original value on form reset", async () => {
444444
const element = getEditorElement()
445-
const { form } = element.inputElement
445+
const { form } = element
446446

447447
await typeCharacters("hello")
448448
form.reset()
@@ -451,7 +451,7 @@ testGroup("Custom element API", { template: "editor_empty" }, () => {
451451

452452
test("editor resets to last-set value on form reset", async () => {
453453
const element = getEditorElement()
454-
const { form } = element.inputElement
454+
const { form } = element
455455

456456
element.value = "hi"
457457
await typeCharacters("hello")
@@ -461,7 +461,7 @@ testGroup("Custom element API", { template: "editor_empty" }, () => {
461461

462462
test("editor respects preventDefault on form reset", async () => {
463463
const element = getEditorElement()
464-
const { form } = element.inputElement
464+
const { form } = element
465465
const preventDefault = (event) => event.preventDefault()
466466

467467
await typeCharacters("hello")
@@ -473,27 +473,24 @@ testGroup("Custom element API", { template: "editor_empty" }, () => {
473473
})
474474
})
475475

476-
testGroup("<label> support", { template: "editor_with_labels" }, () => {
477-
test("associates all label elements", () => {
478-
const labels = [ document.getElementById("label-1"), document.getElementById("label-3") ]
479-
assert.deepEqual(getEditorElement().labels, labels)
480-
})
476+
testGroup("HTML sanitization", { template: "editor_unsafe_html" }, () => {
477+
test("editor sanitizes initial value", async () => {
478+
const element = getEditorElement()
481479

482-
test("focuses when <label> clicked", () => {
483-
document.getElementById("label-1").click()
484-
assert.equal(getEditorElement(), document.activeElement)
485-
})
480+
expectDocument("safe\n")
486481

487-
test("focuses when <label> descendant clicked", () => {
488-
document.getElementById("label-1").querySelector("span").click()
489-
assert.equal(getEditorElement(), document.activeElement)
482+
assert.equal(element.innerHTML, "<div><!--block-->safe</div>")
490483
})
484+
})
485+
486+
testGroup("<label> support", { template: "editor_with_labels" }, () => {
487+
test("associates all label elements", () => {
488+
const element = getEditorElement()
489+
const labels = Array.from(element.labels)
490+
const controls = labels.map((label) => label.control)
491491

492-
test("does not focus when <label> controls another element", () => {
493-
const label = document.getElementById("label-2")
494-
assert.notEqual(getEditorElement(), label.control)
495-
label.click()
496-
assert.notEqual(getEditorElement(), document.activeElement)
492+
assert.deepEqual(labels, [ document.getElementById("label-1"), document.getElementById("label-3") ])
493+
assert.deepEqual(controls, [ element, element ])
497494
})
498495
})
499496

@@ -505,8 +502,8 @@ testGroup("form property references its <form>", { template: "editors_with_forms
505502
})
506503

507504
test("transitively accesses its related <input> element's <form>", () => {
508-
const form = document.getElementById("input-form")
509-
const editor = document.getElementById("editor-with-input-form")
505+
const form = document.getElementById("attribute-form")
506+
const editor = document.getElementById("editor-with-attribute-form")
510507
assert.equal(editor.form, form)
511508
})
512509

src/test/system/installation_process_test.js

+1-9
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,21 @@ testGroup("Installation process", { template: "editor_html" }, () => {
2121
})
2222

2323
testGroup("Installation process without specified elements", { template: "editor_empty" }, () =>
24-
test("creates identified toolbar and input elements", () => {
24+
test("creates identified toolbar", () => {
2525
const editorElement = getEditorElement()
2626

2727
const toolbarId = editorElement.getAttribute("toolbar")
2828
assert.ok(/trix-toolbar-\d+/.test(toolbarId), `toolbar id not assert.ok ${JSON.stringify(toolbarId)}`)
2929
const toolbarElement = document.getElementById(toolbarId)
3030
assert.ok(toolbarElement, "toolbar element not assert.ok")
3131
assert.equal(editorElement.toolbarElement, toolbarElement)
32-
33-
const inputId = editorElement.getAttribute("input")
34-
assert.ok(/trix-input-\d+/.test(inputId), `input id not assert.ok ${JSON.stringify(inputId)}`)
35-
const inputElement = document.getElementById(inputId)
36-
assert.ok(inputElement, "input element not assert.ok")
37-
assert.equal(editorElement.inputElement, inputElement)
3832
})
3933
)
4034

4135
testGroup("Installation process with specified elements", { template: "editor_with_toolbar_and_input" }, () => {
4236
test("uses specified elements", () => {
4337
const editorElement = getEditorElement()
4438
assert.equal(editorElement.toolbarElement, document.getElementById("my_toolbar"))
45-
assert.equal(editorElement.inputElement, document.getElementById("my_input"))
4639
assert.equal(editorElement.value, "<div>Hello world</div>")
4740
})
4841

@@ -58,7 +51,6 @@ testGroup("Installation process with specified elements", { template: "editor_wi
5851

5952
const editorElement = getEditorElement()
6053
assert.equal(editorElement.toolbarElement, document.getElementById("my_toolbar"))
61-
assert.equal(editorElement.inputElement, document.getElementById("my_input"))
6254
assert.equal(editorElement.value, "<div>Hello world</div>")
6355
})
6456
})
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
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>`
2+
`<trix-editor autofocus placeholder="Say hello..."><div>Hello world</div></trix-editor>
3+
`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export default () => `<trix-editor autofocus><div>safe</div><script>alert("unsafe")</script></trix-editor>
2+
`
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { TEST_IMAGE_URL } from "./test_image_url"
22

33
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;">`
4+
`<trix-editor autofocus placeholder="Say hello...">ab<img src="${TEST_IMAGE_URL}" width="10" height="10"></trix-editor>`
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
export default () =>
22
`<ul id="my_editor">
33
<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>
4+
<li><trix-editor toolbar="my_toolbar" autofocus placeholder="Say hello..."><div>Hello world</div></trix-editor></li>
65
</ul>`

src/test/test_helpers/fixtures/editors_with_forms.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ export default () =>
33
<trix-editor id="editor-with-ancestor-form"></trix-editor>
44
</form>
55
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>
6+
<form id="attribute-form"></form>
7+
<trix-editor id="editor-with-attribute-form" form="attribute-form"></trix-editor>
108
119
<trix-editor id="editor-with-no-form"></trix-editor>`

src/test/test_helpers/fixtures/fixtures.js

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import StringPiece from "trix/models/string_piece"
1111
import editorDefaultAriaLabel from "./editor_default_aria_label"
1212
import editorEmpty from "./editor_empty"
1313
import editorHtml from "./editor_html"
14+
import editorUnsafeHtml from "./editor_unsafe_html"
1415
import editorInTable from "./editor_in_table"
1516
import editorWithBlockStyles from "./editor_with_block_styles"
1617
import editorWithBoldStyles from "./editor_with_bold_styles"
@@ -25,6 +26,7 @@ export const fixtureTemplates = {
2526
"editor_default_aria_label": editorDefaultAriaLabel,
2627
"editor_empty": editorEmpty,
2728
"editor_html": editorHtml,
29+
"editor_unsafe_html": editorUnsafeHtml,
2830
"editor_in_table": editorInTable,
2931
"editor_with_block_styles": editorWithBlockStyles,
3032
"editor_with_bold_styles": editorWithBoldStyles,

src/trix/controllers/editor_controller.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ export default class EditorController extends Controller {
503503
updateInputElement() {
504504
const element = this.compositionController.getSerializableElement()
505505
const value = serializeToContentType(element, "text/html")
506-
return this.editorElement.setInputElementValue(value)
506+
return this.editorElement.setFormValue(value)
507507
}
508508

509509
notifyEditorElement(message, data) {

0 commit comments

Comments
 (0)