Bradley Kirton's Blog

Published on Jan. 12, 2024

Go home

HTMX and web components

This is a bit of a work in progress to build a searchable dropdown using HTMX and web components.

<script>
    class SearchableDropdown extends HTMLElement {
        static formAssociated = true;
        static observedAttributes = ['href', 'value', 'name'];

        #internals;
        #fetchDataList;
        #searchInput;
        #dataList;

        constructor() {
            super();
            this.#internals = this.attachInternals();
            this.#fetchDataList = true;
            this.onInputHandler = this.createOnInput();
            this.onHtmxConfirmHandler = this.createOnHtmxConfirm();
        }
        createOnInput() {
            return (e) => {             
                e.stopPropagation();
                if ( e.inputType !== 'insertReplacementText' ) {
                    return;
                }
                this.value = e.target.value;
                this.#searchInput.value = this.#dataList.querySelector(`option[value="${this.value}"]`).innerText;
                this.#fetchDataList = false;

                if ( this.name ) {              
                    let formData = new FormData();
                    formData.append(this.name, this.value);
                    this.#internals.setFormValue(formData);
                }
                let event = new CustomEvent('input');
                this.dispatchEvent(event);
            }
        }
        createOnHtmxConfirm() {
            return (e) => {
                e.stopPropagation();
                if ( this.#fetchDataList === false ) {
                    this.#fetchDataList = true;
                    e.preventDefault();
                }
            }
        }
        disconnectedCallback () {
            this.#searchInput.removeEventListener(this.onInputHandler);
            this.#searchInput.removeEventListener(this.onHtmxConfirmHandler);
        }
        connectedCallback() {
            this.innerHTML = `
                <input
                    id="search"
                    name="q"
                    type="input"
                    list="datalist"
                    class="rounded w-full"
                    autocomplete="off"
                    ${this.value ? `value=${this.value}`  : '' }
                    hx-get="${this.href}"
                    hx-trigger="input delay:250ms"
                    hx-target="#datalist"
                    hx-swap="innerHTML" />
                <datalist id="datalist"></datalist>
            `;
            this.#searchInput = this.querySelector('#search');
            this.#dataList = this.querySelector('#datalist');       
            this.#searchInput.addEventListener('input', this.onInputHandler);
            this.#searchInput.addEventListener('htmx:confirm', this.onHtmxConfirmHandler);
        }
        attributeChangedCallback(name, oldValue, newValue) {
            if (name == 'href') {
                this.href = newValue;
            } else if (name == 'value') {
                this.value = newValue;
            } else if (name == 'name') {
                this.name = newValue;
            }
        }
        get type() {
            return this.localName;
        }
        checkValidity() { return this.#internals.checkValidity(); }
        reportValidity() { return this.#internals.reportValidity(); }
        get form() {
            return this.#internals.form;
        }
        get validity() {
            return this.#internals.validity;
        }
        get validationMessage() {
            return this.#internals.validationMessage;
        }
        get willValidate() {
            return this.#internals.willValidate; 
        }
    }
    customElements.define('searchable-dropdown', SearchableDropdown );
</script>

<searchable-dropdown
    id="searchable-dropdown"
    name="my-search"
    href="/my-search-path/">
</searchable-dropdown>