A serchable ‹select› element replacement using Alpine.js and Bootstrap 5.3.
*The searchable part of this control is provided by the backend.
Live Example.
This example requires a working endpoint to search for users based on name or email address. Use the following DTO model to create your object:
Your Model
public class KeyValuePair
{
public string DataTextField { get; set; }
public string DataValueField { get; set; }
public string Title { get; set; }
}
While this control is "searchable", the search portion is actually provided by the backend. It could be adapted to search client-side, but we won't be doing that here.
Why is the control needed? Well, it's 2024, and the ‹datalist› element works great, until it doesn't. It turns out that while most browsers support the ‹datalist› fairly well now, they do not all support dynamically updating the dropdown list. For example, searching for a user and updating the dropdown list works fine on Firefox. It does not work on Edge. On Edge, the dropdown list stops functioning after an update.
The Core Javascript
document.addEventListener("alpine:init", () => {
Alpine.data('cntrlUser', () => ({
userGuid: {
value: null
},
txtUserName: {
value: null
},
txtUserSearch: {
value: null
},
ddlSelectedItems: {
value: null
},
isFetching: false,
userSearch: [{ "DataValueField": "27b90273-a6c4-4771-b9d2-c8ca36e66af2", "DataTextField": "John Doe - jdoe@wired4.com" }, { "DataValueField": "7b2dcc9c-f04a-40d6-ba95-67bb24fe287e", "DataTextField": "John bigboote - jdoe@wired4.com" }, { "DataValueField": "f58052fa-2db1-487a-9acd-a97fe27496e9", "DataTextField": "John YaYa - jyaya@wired4.com" }],
async getUserSearch() {
if (this.txtUserSearch.value.length < 3) {
return;
}
const url = '/api/users/search/';
const params = {
q: this.txtUserSearch.value
};
this.isFetching = true;
const data = await request(url, params, 'GET');
this.isFetching = false;
this.userSearch = data;
// Toggle the dropdown open on every search.
this.showSearchResults();
},
async init() {
// await this.getUserRoles();
this.form = document.getElementById('cntrlUser');
this.pristine = new Pristine(this.form, {
errorTextClass: 'text-danger',
errorClass: 'has-error',
successClass: 'has-success'
});
},
async showSearchResults() {
let toggleBtn = document.getElementById('multiSelectDropdown')
let dropdownEl = new bootstrap.Dropdown(toggleBtn);
dropdownEl.show();
},
async resetSearch() {
this.userSearch = [];
}
}))
});
Your HTML
‹div class="container-fluid"›
‹div id="cntrlUser" x-data="cntrlUser"›
‹label x-text="userGuid.value"› ‹/label›
‹label x-show="isFetching"›...Fetching ‹/label›
‹div class="mt-5 mb-3 dropdown"›
‹input class="dropdown-toggle w-50" type="text" id="multiSelectDropdown" x-model="txtUserSearch.value"
@input.debounce="getUserSearch()" data-bs-toggle="dropdown" aria-expanded="false"
placeholder="Search - Name or Email"›
‹ul class="dropdown-menu w-50" aria-labelledby="multiSelectDropdown"›
‹template x-for="option in userSearch" :key="option.DataValueField"›
‹li style="padding-left: 5px"›
‹label :id="option.DataValueField"
@click="txtUserSearch.value=($el.innerText).trim(); resetSearch(); userGuid.value=$el.id"›
‹span x-text="option.DataTextField"› ‹/span›
‹/label›
‹/li›
‹/template›
‹/ul›
‹/div›
‹/div›
‹/div›
The working example is not doing a live search. The backend will have to be configured for that. As is, it's basically a dropdown select list attached to a text input. With the backend configured, it's functions more like a datalist. It has the advantage of not actually being a datalist, which means it's possible to style the dropdown list with CSS.
When a name is selected from the dropdown, it's getting the user's ID, similar to the way a select list would operate. Unlike a select list, the element's title attribute can be set to get another value, in addition to the user's ID.
For example, if you wanted to pass the user's ID and email address into a function, you could pass the user's email address into the "Title" field of your model. You could then access the title attribute of the element to retrieve the user's email address.
A few noteworthy items. If you use a search input, in theory it would give you a way to clear the searchbox after a search. However, on supporting browsers, clicking the "x" will just toggle the dropdown. A way to clear the search would probably make for a better end-user experience.
Proper credit.
While it now bears little resemblance to it, this code came about by making an Alpine.js multi-select dropdown list. The idea for it was this Bootstrap multi-select dropdown, from geeksforgeeks.org.