Skip to content

Commit

Permalink
Integrate with ElementInternals
Browse files Browse the repository at this point in the history
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
  • Loading branch information
seanpdoyle committed Feb 2, 2024
1 parent 10573a8 commit f1bdb3a
Show file tree
Hide file tree
Showing 12 changed files with 107 additions and 123 deletions.
52 changes: 42 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,36 +49,68 @@ document.addEventListener("trix-before-initialize", () => {

## Creating an Editor

Place an empty `<trix-editor></trix-editor>` tag on the page. Trix will automatically insert a separate `<trix-toolbar>` before the editor.
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:

```html
<trix-toolbar id="editor_toolbar"></trix-toolbar>

<trix-editor toolbar="editor_toolbar"></trix-editor>
```

Otherwise, Trix will automatically insert a separate `<trix-toolbar>` before the editor.

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

## Integrating With Forms

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.
Like other form controls, the `<trix-editor>` element supports two styles of integrating with `<label>` elements:

1. render the `<trix-editor>` element with an `[id]` attribute that the `<label>` element references through its `[for]` attribute:

```html
<label for="editor">Editor</label>
<trix-editor id="editor"></trix-editor>
```

2. render the `<trix-editor>` element as a child of the `<label>` element:

```html
<trix-toolbar id="editor-toolbar"></trix-toolbar>
<label>
Editor

<trix-editor toolbar="editor-toolbar"></trix-editor>
</label>
```

> [!WARNING]
> 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.
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.

```html
<form >
<input id="x" type="hidden" name="content">
<trix-editor input="x"></trix-editor>
<trix-editor name="content"></trix-editor>
</form>
```

Trix will automatically update the value of the hidden input field with each change to the editor.
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]`:

```html
<form id="a-form-element" ></form>
<trix-editor name="content" form="a-form-element"></trix-editor>
```

## Populating With Stored Content

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

```html
<form >
<input id="x" value="Editor content goes here" type="hidden" name="content">
<trix-editor input="x"></trix-editor>
<trix-editor>Editor content goes here</trix-editor>
</form>
```

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.

## Styling Formatted Content

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.
Expand Down
8 changes: 7 additions & 1 deletion assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@
Trix.Inspector.install(event.target);
});

document.addEventListener("trix-change", function(event) {
var input = document.getElementById("input")
input.value = event.target.value
})

document.addEventListener("trix-attachment-add", function(event) {
var attachment = event.attachment;
if (attachment.file) {
Expand Down Expand Up @@ -72,7 +77,8 @@
</head>
<body>
<main>
<trix-editor autofocus class="trix-content" input="input"></trix-editor>
<label for="editor">Input</label>
<trix-editor autofocus id="editor" class="trix-content"></trix-editor>
<details id="output">
<summary>Output</summary>
<textarea readonly id="input"></textarea>
Expand Down
41 changes: 19 additions & 22 deletions src/test/system/custom_element_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ testGroup("Custom element API", { template: "editor_empty" }, () => {

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

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

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

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

test("editor respects preventDefault on form reset", async () => {
const element = getEditorElement()
const { form } = element.inputElement
const { form } = element
const preventDefault = (event) => event.preventDefault()

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

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

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

test("focuses when <label> descendant clicked", () => {
document.getElementById("label-1").querySelector("span").click()
assert.equal(getEditorElement(), document.activeElement)
assert.equal(element.innerHTML, "<div><!--block-->safe</div>")
})
})

testGroup("<label> support", { template: "editor_with_labels" }, () => {
test("associates all label elements", () => {
const element = getEditorElement()
const labels = Array.from(element.labels)
const controls = labels.map((label) => label.control)

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

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

test("transitively accesses its related <input> element's <form>", () => {
const form = document.getElementById("input-form")
const editor = document.getElementById("editor-with-input-form")
const form = document.getElementById("attribute-form")
const editor = document.getElementById("editor-with-attribute-form")
assert.equal(editor.form, form)
})

Expand Down
10 changes: 1 addition & 9 deletions src/test/system/installation_process_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,21 @@ testGroup("Installation process", { template: "editor_html" }, () => {
})

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

const toolbarId = editorElement.getAttribute("toolbar")
assert.ok(/trix-toolbar-\d+/.test(toolbarId), `toolbar id not assert.ok ${JSON.stringify(toolbarId)}`)
const toolbarElement = document.getElementById(toolbarId)
assert.ok(toolbarElement, "toolbar element not assert.ok")
assert.equal(editorElement.toolbarElement, toolbarElement)

const inputId = editorElement.getAttribute("input")
assert.ok(/trix-input-\d+/.test(inputId), `input id not assert.ok ${JSON.stringify(inputId)}`)
const inputElement = document.getElementById(inputId)
assert.ok(inputElement, "input element not assert.ok")
assert.equal(editorElement.inputElement, inputElement)
})
)

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

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

const editorElement = getEditorElement()
assert.equal(editorElement.toolbarElement, document.getElementById("my_toolbar"))
assert.equal(editorElement.inputElement, document.getElementById("my_input"))
assert.equal(editorElement.value, "<div>Hello world</div>")
})
})
4 changes: 2 additions & 2 deletions src/test/test_helpers/fixtures/editor_html.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export default () =>
`<input id="my_input" type="hidden" value="&lt;div&gt;Hello world&lt;/div&gt;">
<trix-editor input="my_input" autofocus placeholder="Say hello..."></trix-editor>`
`<trix-editor autofocus placeholder="Say hello..."><div>Hello world</div></trix-editor>
`
2 changes: 2 additions & 0 deletions src/test/test_helpers/fixtures/editor_unsafe_html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export default () => `<trix-editor autofocus><div>safe</div><script>alert("unsafe")</script></trix-editor>
`
3 changes: 1 addition & 2 deletions src/test/test_helpers/fixtures/editor_with_image.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { TEST_IMAGE_URL } from "./test_image_url"

export default () =>
`<trix-editor input="my_input" autofocus placeholder="Say hello..."></trix-editor>
<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;">`
`<trix-editor autofocus placeholder="Say hello...">ab<img src="${TEST_IMAGE_URL}" width="10" height="10"></trix-editor>`
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export default () =>
`<ul id="my_editor">
<li><trix-toolbar id="my_toolbar"></trix-toolbar></li>
<li><trix-editor toolbar="my_toolbar" input="my_input" autofocus placeholder="Say hello..."></trix-editor></li>
<li><input id="my_input" type="hidden" value="&lt;div&gt;Hello world&lt;/div&gt;"></li>
<li><trix-editor toolbar="my_toolbar" autofocus placeholder="Say hello..."><div>Hello world</div></trix-editor></li>
</ul>`
6 changes: 2 additions & 4 deletions src/test/test_helpers/fixtures/editors_with_forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ export default () =>
<trix-editor id="editor-with-ancestor-form"></trix-editor>
</form>
<form id="input-form">
<input type="hidden" id="hidden-input">
</form>
<trix-editor id="editor-with-input-form" input="hidden-input"></trix-editor>
<form id="attribute-form"></form>
<trix-editor id="editor-with-attribute-form" form="attribute-form"></trix-editor>
<trix-editor id="editor-with-no-form"></trix-editor>`
2 changes: 2 additions & 0 deletions src/test/test_helpers/fixtures/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import StringPiece from "trix/models/string_piece"
import editorDefaultAriaLabel from "./editor_default_aria_label"
import editorEmpty from "./editor_empty"
import editorHtml from "./editor_html"
import editorUnsafeHtml from "./editor_unsafe_html"
import editorInTable from "./editor_in_table"
import editorWithBlockStyles from "./editor_with_block_styles"
import editorWithBoldStyles from "./editor_with_bold_styles"
Expand All @@ -25,6 +26,7 @@ export const fixtureTemplates = {
"editor_default_aria_label": editorDefaultAriaLabel,
"editor_empty": editorEmpty,
"editor_html": editorHtml,
"editor_unsafe_html": editorUnsafeHtml,
"editor_in_table": editorInTable,
"editor_with_block_styles": editorWithBlockStyles,
"editor_with_bold_styles": editorWithBoldStyles,
Expand Down
2 changes: 1 addition & 1 deletion src/trix/controllers/editor_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ export default class EditorController extends Controller {
updateInputElement() {
const element = this.compositionController.getSerializableElement()
const value = serializeToContentType(element, "text/html")
return this.editorElement.setInputElementValue(value)
return this.editorElement.setFormValue(value)
}

notifyEditorElement(message, data) {
Expand Down

0 comments on commit f1bdb3a

Please sign in to comment.