Skip to content

Commit 5a00b3a

Browse files
committed
Validate Checkbox Groups on invalid events from other controls
Share `isAriaInvalid` across files by extracting it into the utility module. Next, report checkbox group validity when other elements dispatch `invalid` events.
1 parent 69a62ae commit 5a00b3a

File tree

9 files changed

+145
-31
lines changed

9 files changed

+145
-31
lines changed

app/assets/javascripts/constraint_validations.es.js

+36-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/assets/javascripts/constraint_validations.es.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/assets/javascripts/constraint_validations.js

+35-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/assets/javascripts/constraint_validations.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/javascript/constraint_validations/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isFieldElement, readValidationMessages } from "./util"
1+
import { isAriaInvalid, isFieldElement, readValidationMessages } from "./util"
22
import CheckboxValidator from "./validators/checkbox_validator"
33

44
const defaultOptions = {
@@ -83,7 +83,7 @@ export default class ConstraintValidations {
8383

8484
for (const form of forms) {
8585
for (const element of Array.from(form.elements).filter(isFieldElement)) {
86-
const serverRenderedInvalid = /true/i.test(element.getAttribute("aria-invalid"))
86+
const serverRenderedInvalid = isAriaInvalid(element)
8787
const id = element.getAttribute("aria-errormessage")
8888
const errorMessageElement = document.getElementById(id)
8989
const validationMessage = errorMessageElement?.textContent

app/javascript/constraint_validations/util.js

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ export function isFieldElement(element) {
22
return !element.disabled && "validity" in element && element.willValidate
33
}
44

5+
export function isAriaInvalid(element) {
6+
return element.getAttribute("aria-invalid") === "true"
7+
}
8+
59
export function readValidationMessages(input) {
610
try {
711
return JSON.parse(input.getAttribute("data-validation-messages")) || {}

app/javascript/constraint_validations/validators/checkbox_validator.js

+32-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { isFieldElement, readValidationMessages } from "../util"
1+
import { isAriaInvalid, isFieldElement, readValidationMessages } from "../util"
22

33
export default class {
4-
selector = "input[type=checkbox]:required"
4+
selector = "input[type=checkbox]"
55
ignoringMutations = false
66

77
constructor(constraintValidations, predicate) {
@@ -13,15 +13,17 @@ export default class {
1313
}
1414

1515
connect() {
16+
this.element.addEventListener("invalid", this.handleInvalid, { capture: true, passive: true })
1617
this.mutationObserver.observe(this.element, {
1718
attributeFilter: ["required"],
1819
childList: true,
1920
subtree: true
2021
})
21-
this.reportValidationMessages(this.element.querySelectorAll(this.selector))
22+
this.reportValidationMessages(this.element.querySelectorAll(this.selector), isAriaInvalid)
2223
}
2324

2425
disconnect() {
26+
this.element.removeEventListener("invalid", this.handleInvalid, { capture: true, passive: true })
2527
this.mutationObserver.disconnect()
2628
}
2729

@@ -30,8 +32,8 @@ export default class {
3032
}
3133

3234
validate(target) {
33-
const checkboxesInGroup = checkboxGroup(target).filter(isFieldElement)
34-
const allRequired = checkboxesInGroup.every((checkbox) => checkbox.getAttribute("aria-required") === "true")
35+
const checkboxesInGroup = checkboxGroup(target).filter(isCheckboxElement)
36+
const allRequired = checkboxesInGroup.every((checkbox) => isRequired(checkbox))
3537
const someChecked = checkboxesInGroup.some((checkbox) => checkbox.checked)
3638

3739
if (allRequired && someChecked) {
@@ -50,6 +52,18 @@ export default class {
5052

5153
// Private
5254

55+
handleInvalid = ({ target }) => {
56+
const checkboxes = new Set
57+
58+
for (const element of target.form.elements) {
59+
if (isCheckboxElement(element) && this.willValidate(element)) {
60+
checkboxes.add(element)
61+
}
62+
}
63+
64+
this.reportValidationMessages(checkboxes)
65+
}
66+
5367
handleMutation = (mutationRecords) => {
5468
if (this.ignoringMutations) return
5569

@@ -61,22 +75,23 @@ export default class {
6175
target.removeAttribute("aria-required")
6276
}
6377
} else if (addedNodes.length) {
64-
this.reportValidationMessages(addedNodes)
78+
this.reportValidationMessages(addedNodes, isAriaInvalid)
6579
}
6680
}
6781
}
6882

69-
reportValidationMessages(nodes) {
83+
reportValidationMessages(nodes, willReport = () => true) {
7084
const requiredCheckboxes = querySelectorAllNodes(this.selector, nodes)
7185

7286
for (const checkbox of requiredCheckboxes) {
73-
if (checkbox.required) {
87+
if (isRequired(checkbox)) {
7488
const group = checkboxGroup(checkbox)
7589

7690
if (this.willValidateGroup(group)) {
7791
for (const checkboxInGroup of group) {
7892
this.swapRequiredWithAriaRequired(checkboxInGroup)
79-
if (checkboxInGroup.getAttribute("aria-invalid") === "true") {
93+
94+
if (willReport(checkboxInGroup)) {
8095
this.validate(checkboxInGroup)
8196
}
8297
}
@@ -138,3 +153,11 @@ function querySelectorAllNodes(selector, nodes, elements = new Set) {
138153

139154
return Array.from(elements)
140155
}
156+
157+
function isCheckboxElement(element) {
158+
return isFieldElement(element) && element.type === "checkbox"
159+
}
160+
161+
function isRequired(element) {
162+
return element.required || element.getAttribute("aria-required") === "true"
163+
}

test/dummy/app/views/forms/new.html.erb

+1
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,6 @@
5151
<% end %>
5252
</fieldset>
5353

54+
<button>Validate and Submit</button>
5455
<button formnovalidate>Skip Validations</button>
5556
<% end %>

0 commit comments

Comments
 (0)