/*{ "parent": "component", "description": "Form validation for tosijs form-associated custom elements via ElementInternals: validity, validation messages, and constraint helpers." }*/ /*# # Validation Form validation for custom elements using `ElementInternals`. ## Overview When building form-associated custom elements, you want them to behave like native `` elements - participating in form submission, validation, and lifecycle events. With `static formAssociated = true` and a `value` property, your Component subclass automatically gets: - Form submission (value appears in FormData) - Validation API (`checkValidity()`, `reportValidity()`, `setCustomValidity()`) - Automatic validation for `required`, `minlength`, `maxlength`, `pattern` attributes - Form lifecycle callbacks (reset, disabled state, browser restore) - Focusability for validation UI ## Quick Start Here's a minimal form-associated component: ```js import { Component, elements } from 'tosijs' class SimpleInput extends Component { static preferredTagName = 'simple-input' static formAssociated = true value = '' content = ({input}) => input({part: 'input', style: 'padding: 8px'}) connectedCallback() { super.connectedCallback() this.parts.input.addEventListener('input', (e) => { this.value = e.target.value }) } render() { super.render() if (this.parts.input.value !== this.value) { this.parts.input.value = this.value } } } const simpleInput = SimpleInput.elementCreator() const { form, button, div } = elements const output = div() const myForm = form( simpleInput({name: 'username', required: true}), button({type: 'submit'}, 'Submit') ) myForm.addEventListener('submit', (e) => { e.preventDefault() const fd = new FormData(e.target) output.textContent = 'FormData: ' + [...fd.entries()].map(([k,v]) => `${k}=${v}`).join(', ') }) preview.append(myForm, output) ``` ```css .preview form { display: flex; gap: 8px; align-items: center; } .preview simple-input { display: inline-block; } ``` ## Validation API Form-associated components expose the standard validation API: ### Properties - `validity: ValidityState` - Current validity state (readonly) - `validationMessage: string` - Current validation message (readonly) - `willValidate: boolean` - Whether element will be validated (readonly) ### Methods - `checkValidity()` - Returns `true` if valid; fires `invalid` event if not - `reportValidity()` - Like `checkValidity()` but shows browser validation UI - `setCustomValidity(message)` - Set custom error (empty string clears) - `setValidity(flags, message?, anchor?)` - Low-level validity control - `setFormValue(value, state?)` - Explicitly set form value - `validateValue()` - Run constraint validation (called automatically) ### ValidityStateFlags When calling `setValidity()`, you can set these flags: - `valueMissing` - required but empty - `typeMismatch` - wrong type (email, url, etc.) - `patternMismatch` - doesn't match pattern attribute - `tooLong` - exceeds maxlength - `tooShort` - below minlength - `rangeUnderflow` / `rangeOverflow` - outside min/max range - `stepMismatch` - doesn't match step - `badInput` - browser can't parse input - `customError` - custom error via setCustomValidity ## Automatic Validation Component automatically validates against standard HTML constraint attributes when `value` changes: - `required` - value cannot be empty - `minlength` - minimum string length - `maxlength` - maximum string length - `pattern` - regex pattern (anchored to full string) ```js import { Component, elements } from 'tosijs' class ValidatedInput extends Component { static preferredTagName = 'validated-input' static formAssociated = true value = '' content = ({input}) => input({part: 'input', style: 'padding: 8px'}) connectedCallback() { super.connectedCallback() this.parts.input.addEventListener('input', (e) => { this.value = e.target.value }) } render() { super.render() if (this.parts.input.value !== this.value) { this.parts.input.value = this.value } } } const validatedInput = ValidatedInput.elementCreator() const { form, button, div } = elements const output = div() const myForm = form( validatedInput({ name: 'code', required: true, minlength: '3', maxlength: '10', pattern: '[a-z]+' }), button({type: 'submit'}, 'Submit') ) myForm.addEventListener('submit', (e) => { e.preventDefault() output.textContent = 'Valid! Value: ' + e.target.elements.code.value }) preview.append(div('Enter 3-10 lowercase letters:'), myForm, output) ``` ```css .preview form { display: flex; gap: 8px; align-items: center; margin-top: 8px; } .preview validated-input { display: inline-block; } ``` ### Custom Validation Override `validateValue()` for custom logic. Call `super.validateValue()` first to include standard constraint validation: validateValue() { super.validateValue() // check required, minlength, etc. // Add custom validation if (this.value && !/^[a-z][a-z0-9_]*$/.test(this.value)) { this.setValidity( { patternMismatch: true }, 'Must start with letter, only lowercase letters/numbers/underscores', this ) } } ## Form Lifecycle Callbacks Component provides default implementations for form lifecycle callbacks: ### formResetCallback() Called when the containing `