import BaseCustomElement from "./BaseCustomElement"
import RichString from "./RichString"
import AjaxSubmit from "./AjaxSubmit"
import ConstraintViolationMessages from "./ConstraintViolationMessages"
/** A web component that enhances a form it contains to make constraint validations
* easier to manage and control.
*
* This provides two main features:
*
* * While the `:user-invalid` selector allows you to target inputs that have been interacted
* with (thus avoiding issues when using `:invalid`), this still creates the experience of a
* user tabbing off of a control and getting an error message. If, instead, you only
* want to show these errors when a submit has been attempted, this element will
* set `submitted-invalid` on itself when that happens, thus allowing you to target invalid
* fields only after a submission attempt.
* * You may wish to control the messaging of client-side constraint violations
* beyond what the browser gives you. Assuming your `INPUT` tags are inside a container
* like `LABEL`, a `brut-cv` tag found in that container
* (i.e. a sibling of your `INPUT`) will be modified to contain error messages specific
* to the {@link external:ValidityState} of the control.
*
* @fires brut:invalid Fired when any element is found to be invalid
* @fires brut:valid Fired when no element is found to be invalid. This should be reliable to know
* when constraint violations have cleared.
*
* @example <caption>Basic Structure Required</caption>
* <brut-form>
* <form ...>
* <label>
* <input type="text" required name="username">
* <brut-cv-messages>
* </brut-cv-messages>
* </label>
* <div> <!-- container need not be a label -->
* <input type="text" required minlength="4" name="alias">
* <brut-cv-messages>
* </brut-cv-messages>
* </div>
* <button>Submit</button>
* </form>
* </brut-form>
* <!-- after a submit of this form, the HTML will effectively be as follows -->
* <brut-form submitted-invalid>
* <form ...>
* <label>
* <input type="text" required name="username">
* <brut-cv-messages>
* <brut-cv>This field is required</brut-cv>
* </brut-cv-messages>
* </label>
* <div> <!-- container need not be a label -->
* <input type="text" required minlength="4" name="alias">
* <brut-cv-messages>
* <brut-cv>This field is required</brut-cv>
* </brut-cv-messages>
* </div>
* <button>Submit</button>
* </form>
* </brut-form>
*
* @property {boolean} submitted-invalid - set by this element when the form is submitted. Does not trigger any behavior and can be used in CSS.
* @see ConstraintViolationMessages
*
* @customElement brut-form
*/
class Form extends BaseCustomElement {
static tagName = "brut-form"
static observedAttributes = [
"submitted-invalid",
"show-warnings",
]
#markFormSubmittedInvalid = (event) => {
this.setAttribute("submitted-invalid","")
}
#updateValidity = (event) => {
this.#updateErrorMessages(event)
}
#sendValid = () => {
this.dispatchEvent(new CustomEvent("brut:valid"))
}
#sendInvalid = () => {
this.dispatchEvent(new CustomEvent("brut:invalid"))
}
submittedInvalidChangedCallback() {}
update() {
const forms = this.querySelectorAll("form")
if (forms.length == 0) {
this.logger.warn("Didn't find any forms. Ignoring")
return
}
forms.forEach( (form) => {
Array.from(form.elements).forEach( (formElement) => {
formElement.addEventListener("invalid", this.#updateValidity)
formElement.addEventListener("invalid", this.#markFormSubmittedInvalid)
formElement.addEventListener("input", this.#updateValidity)
})
form.querySelectorAll(AjaxSubmit.tagName).forEach( (ajaxSubmits) => {
ajaxSubmits.addEventListener("brut:submitok", this.#sendValid)
ajaxSubmits.addEventListener("brut:submitinvalid", this.#sendInvalid)
})
})
}
#updateErrorMessages(event) {
const element = event.target
const selector = ConstraintViolationMessages.tagName
let errorLabels = element.parentNode.querySelectorAll(selector)
if (errorLabels.length == 0) {
if (element.name && element.form) {
const moreGeneralSelector = `${ConstraintViolationMessages.tagName}[input-name='${element.name}']`
errorLabels = element.form.querySelectorAll(moreGeneralSelector)
if (errorLabels.length == 0) {
this.logger.warn(`Did not find any elements matching ${selector} or ${moreGeneralSelector}, so no error messages will be shown`)
}
}
else {
this.logger.warn("Did not find any elements matching %s and the form element has %s %s",
selector,
element.name ? "no name" : "a name, but",
element.form ? "no form" : "though has a form")
}
}
if (errorLabels.length == 0) {
return
}
let anyErrors = false
errorLabels.forEach( (errorLabel) => {
if (element.validity.valid) {
errorLabel.clearClientSideMessages()
}
else {
anyErrors = true
errorLabel.createMessages({
validityState: element.validity,
inputName: element.name
})
}
})
if (anyErrors) {
this.#sendInvalid()
}
else {
this.#sendValid()
}
}
}
export default Form