Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Validating a Form-Associated Custom Element

I've built a simple 2 text field custom element but I can't get validation to work correctly. There are no tutorials that cover this aspect.

Here is my custom element:


/* name.js */

class MyName extends HTMLElement {

    static formAssociated = true

    constructor() {
        super()
        this.internals = this.attachInternals()
        this.internals.formAssociated = true;
        var shadowRoot = this.attachShadow({mode: 'open', delegatesFocus: true})
        this.data = new FormData()
        this.data.set('firstname', '')
        this.data.set('lastname', '')
        shadowRoot.innerHTML = this.html
    }

    connectedCallback() {
        // any DOM manipulation goes here, run only if not initialised
        if (!this.initialized) {
            console.log('initialised');
            this.onUpdateValue();
            this.shadowRoot.querySelectorAll('input').forEach( el => el.addEventListener('input', event => {
                console.log('content changed')
                event.target.classList.add('show_error');
                this.data.set(event.target.name, event.target.value)
                this.internals.setFormValue(this.data)
                this.onUpdateValue(); // trigger the form validation
                this.internals.setValidity({ customError: true }, 'data not valid');
            }));
            // waits before displaying the element to prevent flickering
            setTimeout(() => this.shadowRoot.querySelector('div').classList.remove('hidden'), 100);
        }
    }

    onUpdateValue() {
        // The Constraint Validation API
        let valid = true;
        const p = this.shadowRoot.querySelector('p.error');
        this.shadowRoot.querySelectorAll('input').forEach(element => {
            if (element.validity.valid === false) valid = false;
        })
        if (valid) {
            this.internals.setValidity({});
        } else {
            console.log('invalid');
            this.internals.setValidity({ customError: true }, 'data not valid');
        }
    }

    get willValidate() { return this.internals.willValidate }
    checkValidity() { return this.internals.checkValidity() }
    reportValidity() {return this.internals.reportValidity() }

    get value() {
        return this.data
    }

    set value(v) {
        if (v instanceof FormData) {
            this.shadowRoot.querySelector('input[name="firstname"]').value = v.get('firstname')
            this.shadowRoot.querySelector('input[name="lastname"]').value = v.get('lastname')
            this.data = v
            this.internals.setFormValue(v)
        }
    }

    formResetCallback() {
        this.data.set('firstname', '')
        this.data.set('lastname', '')
        this.shadowRoot.querySelector('input[name="firstname"]').value = this.data.get('firstname')
        this.shadowRoot.querySelector('input[name="lastname"]').value = this.data.get('lastname')
    }

    formDisabledCallback(isDisabled) {
        this.shadowRoot.querySelector('input[name="firstname"]').disabled = isDisabled
        this.shadowRoot.querySelector('input[name="lastname"]').disabled = isDisabled
    }

    reportValidity() { // expose reportValidity on the CE's surface
        return this._internals.reportValidity();
    }

    static get observedAttributes() {
        return ['value'];
    }

    html = `
<link rel="stylesheet" href="common.css" />
<style>
// removes any inherited css
#container {
    all: initial;
}
// prevents the flicker as the style is applied
.hidden {
    visibility: hidden;
    // display: none;
}
input:invalid {
    border-color: red;
}
</style>
<div class="hidden">
<h2 part="title">Address Form</h2>
<p><input type="text" name="firstname" placeholder="firstname" minlength="5" /></p>
<p><input type="text" name="lastname" placeholder="lastname"  minlength="5" /></p>
<p class="error"></p>
</div>
`

}

customElements.define("my-name", MyName)

I have a simple page with this custom element (name="foobar") plus a submit button with the following event triggering the form submit:

document.querySelector('form').addEventListener('submit', event => {
    event.preventDefault();
    console.log(document.querySelector('my-name').checkValidity());
    // getting the value of the custom element
    const name = document.querySelector('my-name').value;
    document.querySelector('pre#formdata').textContent = JSON.stringify(Object.fromEntries(name.entries()), null, 2);
})

When I load the page and click the button I get the following error logged in the console:

An invalid form control with name='foobar' is not focusable.

I think I understand what is happening, the browser is trying to highlight the custom element (and failing). I'm assuming this is an issue faced by everyone building these FACE elements but can't find any solution posted.

Any help would be greatly appreciated...

like image 443
Mark Tyers Avatar asked Oct 16 '25 07:10

Mark Tyers


1 Answers

I'm in the same boat. I don't have all the answers, but I'm almost there. I haven't got the error message to go away, but have got the element to focus and show the error message popover despite it.

The main thing you're missing is including the inner input element as a 3rd arg to this.internals.setValidity. This tells it exactly where to focus.

It's also good to expose a validity getter on your component. This is used by the browser if you call checkValidity or reportValidity on the form element. Consumers of your component might want to set novalidate on the form and manually call checkValidity on submit to skip those native browser popover error messages if you have another way to show them.

You may be able to reverse engineer a more complete solution from Material 3's web components. Particularly look at their mixins for form associated elements and validation, and how individual components implement abstract methods on those mixins. The checkbox has minimal other considerations.

I found out about the setValidity call from a Material comment that reads:

Elements that delegatesFocus: true to an <input> will throw an error in Chrome and Safari when a form tries to submit or call form.reportValidity(): "An invalid form control with name='' is not focusable". The validity anchor MUST be provided in internals.setValidity() and MUST be the <input> element rendered.

like image 56
Patrick Stephansen Avatar answered Oct 18 '25 21:10

Patrick Stephansen



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!