Skip to content

Commit

Permalink
Support Constraint validation
Browse files Browse the repository at this point in the history
Add support for integrating with [Constraint validation][] through the
support for the `[required]` attribute and the
`setCustomValidity(message)` method.

[Constraint validation]: https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation
  • Loading branch information
seanpdoyle committed Oct 3, 2024
1 parent 2f8c59d commit ebdd64f
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 0 deletions.
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,68 @@ To populate a `<trix-editor>` with stored content, include that content in the a

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.

## Validating the Editor

Out of the box, `<trix-editor>` elements support browsers' built-in [Constraint
validation][]. When rendered with the [required][] attribute, editors will be
invalid when they're completely empty. For example, consider the following HTML:

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

Since the `<trix-editor>` element is `[required]`, it is invalid when its value
is empty:

```js
const editor = document.querySelector("trix-editor")

editor.validity.valid // => false
editor.validity.valueMissing // => true
editor.matches(":valid") // => false
editor.matches(":invalid") // => true

editor.value = "A value that isn't empty"

editor.validity.valid // => true
editor.validity.valueMissing // => false
editor.matches(":valid") // => true
editor.matches(":invalid") // => false
```

In addition to the built-in `[required]` attribute, `<trix-editor>`
elements support custom validation through their [setCustomValidity][] method.
For example, consider the following HTML:

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

Custom validation can occur at any time. For example, validation can occur after
a `trix-change` event fired after the editor's contents change:

```js
addEventListener("trix-change", (event) => {
const editorElement = event.target
const trixDocument = editorElement.editor.getDocument()
const isValid = (trixDocument) => {
// determine the validity based on your custom criteria
}

if (isValid(trixDocument)) {
editorElement.setCustomValidity("The document is not valid.")
} else {
editorElement.setCustomValidity("")
}
}
```
[Constraint validation]: https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation
[required]: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/required
[setCustomValidity]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectElement/setCustomValidity
## Disabling the Editor
To disable the `<trix-editor>`, render it with the `[disabled]` attribute:
Expand Down
4 changes: 4 additions & 0 deletions assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
max-width: 700px;
}

trix-editor:invalid {
border: solid 1px red;
}

#output {
margin: 1rem 0 0;
}
Expand Down
56 changes: 56 additions & 0 deletions src/test/system/custom_element_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,7 @@ testGroup("integrates with its <form>", { template: "editors_with_forms", contai
assert.equal(editor.inputElement.disabled, false, "enabled input")
assert.equal(editor.disabled, false, "updates .disabled property")
assert.equal(editor.hasAttribute("contenteditable"), true, "adds [contenteditable] attribute")
:xa
})

test("removes [contenteditable] and disables input when editor element is :disabled", () => {
Expand Down Expand Up @@ -630,4 +631,59 @@ testGroup("integrates with its <form>", { template: "editors_with_forms", contai

assert.deepEqual({}, Object.fromEntries(new FormData(form).entries()), "does not write to FormData")
})

test("validates with [required] attribute as invalid", () => {
const editor = document.getElementById("editor-with-ancestor-form")
const form = editor.form
let invalidEvent, submitEvent = null

editor.addEventListener("invalid", event => invalidEvent = event, { once: true })
form.addEventListener("submit", event => submitEvent = event, { once: true })

editor.required = true
form.requestSubmit()

// assert.equal(document.activeElement, editor, "editor receives focus")
assert.equal(editor.required, true, ".required property retrurns true")
assert.equal(editor.validity.valid, false, "validity.valid is false")
assert.equal(editor.validationMessage, "Please fill out this field.", "sets .validationMessage")
assert.equal(invalidEvent.target, editor, "dispatches 'invalid' event on editor")
assert.equal(submitEvent, null, "does not dispatch a 'submit' event")
})

test("does not validate with [disabled] attribute", () => {
const editor = document.getElementById("editor-with-ancestor-form")
let invalidEvent = null

editor.disabled = true
editor.required = true
editor.addEventListener("invalid", event => invalidEvent = event, { once: true })
editor.reportValidity()

assert.equal(invalidEvent, null, "does not dispatch an 'invalid' event")
})

test("re-validates when the value changes", async () => {
const editor = document.getElementById("editor-with-ancestor-form")
editor.required = true
editor.focus()

assert.equal(editor.validity.valid, false, "validity.valid is initially false")

await typeCharacters("a")

assert.equal(editor.validity.valid, true, "validity.valid is true after re-validating")
assert.equal(editor.validity.valueMissing, false, "validity.valueMissing is false")
assert.equal(editor.validationMessage, "", "clears the validationMessage")
})

test("accepts a customError validation message", () => {
const editor = document.getElementById("editor-with-ancestor-form")

editor.setCustomValidity("A custom validation message")

assert.equal(editor.validity.valid, false)
assert.equal(editor.validity.customError, true)
assert.equal(editor.validationMessage, "A custom validation message")
})
})
51 changes: 51 additions & 0 deletions src/trix/elements/trix_editor_element.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ installDefaultCSSForTagName("trix-editor", `\
export default class TrixEditorElement extends HTMLElement {
static formAssociated = true

#customValidationMessage
#internals

constructor() {
Expand Down Expand Up @@ -257,6 +258,8 @@ export default class TrixEditorElement extends HTMLElement {
if (this.inputElement) {
this.inputElement.value = value
}

this.#synchronizeValidation()
}

// Element lifecycle
Expand All @@ -276,6 +279,7 @@ export default class TrixEditorElement extends HTMLElement {
requestAnimationFrame(() => triggerEvent("trix-initialize", { onElement: this }))
}
this.editorController.registerSelectionManager()
this.#synchronizeValidation()
autofocus(this)
}
}
Expand All @@ -284,6 +288,43 @@ export default class TrixEditorElement extends HTMLElement {
this.editorController?.unregisterSelectionManager()
}

// Constraint validation

set required(value) {
this.toggleAttribute("required", value)
this.#synchronizeValidation()
}

get required() {
return this.hasAttribute("required")
}

get validity() {
return this.#internals.validity
}

get validationMessage() {
return this.#internals.validationMessage
}

get willValidate() {
return this.#internals.willValidate
}

checkValidity() {
return this.#internals.checkValidity()
}

reportValidity() {
return this.#internals.reportValidity()
}

setCustomValidity(customValidationMessage) {
this.#customValidationMessage = customValidationMessage

this.#synchronizeValidation()
}

// Form support

formDisabledCallback(disabled) {
Expand All @@ -298,4 +339,14 @@ export default class TrixEditorElement extends HTMLElement {
reset() {
this.value = this.defaultValue
}

#synchronizeValidation() {
const { required, value } = this
const valueMissing = required && !value
const customError = !!this.#customValidationMessage
const input = Object.assign(document.createElement("input"), { required })
const validationMessage = this.#customValidationMessage || input.validationMessage

this.#internals.setValidity({ valueMissing, customError }, validationMessage)
}
}

0 comments on commit ebdd64f

Please sign in to comment.