Published on Jan. 12, 2024
Go homeHTMX 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>