Elegant Ways to Filter JavaScript Data Using HTML Form Controls (Without bind)
When building interactive data tables or dashboards, linking complex HTML forms directly to JavaScript’s Array.prototype.filter() is a common task. However, developers often find themselves writing verbose DOM queries, manually attaching functions to DOM elements, or relying on .bind() to maintain context.
The Problem with Traditional Approaches
Attaching custom filter methods directly onto DOM nodes with .bind() introduces tight coupling between your view (DOM) and your data-processing logic. Furthermore, manually looping through <option> elements to read a selected value is redundant when modern browser APIs provide clean alternatives like HTMLSelectElement.value or the FormData API.
Approach 1: Higher-Order Functions and Closures (Clean & Functional)
Instead of mutating DOM elements by adding custom properties, a functional approach uses a factory function (a higher-order function) that returns a filter predicate. This completely eliminates the need for .bind().
// A predicate generator for matching a specific key to an element's value
const createFilter = (element, key) => (item) => {
const value = element.value.trim();
return !value || item[key] === value;
};
const colourSelect = document.getElementById('colour');
const colourFilter = createFilter(colourSelect, 'colour');
// Usage in apply_filter:
const filteredData = data.filter(colourFilter);Approach 2: Declarative Multi-Filter with FormData (Best for Large Forms)
If you have an extensive form with multiple controls (e.g., text search, dropdowns, checkboxes), handling each input individually becomes difficult to maintain. A scalable approach is to wrap all inputs in a single <form> element and read active criteria using FormData.
HTML
<form id="filters-form">
<label for="colour">Colour:</label>
<select id="colour" name="colour">
<option value="">All colours</option>
<option value="blue">blue</option>
<option value="red">red</option>
<option value="yellow">yellow</option>
</select>
</form>JavaScript
const form = document.getElementById('filters-form');
const tbody = document.getElementById('tbody_output');
function applyFilter() {
// Extract non-empty criteria as key-value pairs
const activeFilters = Object.fromEntries(
Array.from(new FormData(form).entries()).filter(([_, value]) => value !== '')
);
// Filter dataset across all active filter fields
const filtered = data.filter((item) =>
Object.entries(activeFilters).every(([key, value]) => item[key] === value)
);
renderTable(filtered);
}
function renderTable(items) {
tbody.innerHTML = items
.map((item) => `<tr><td>${item.id}</td><td>${item.item}</td><td>${item.colour}</td></tr>`)
.join('');
}
// Single listener via event delegation on the form
form.addEventListener('input', applyFilter);
applyFilter();Why This Approach is More Elegant
- Separation of Concerns: Your data filtering logic is completely decoupled from DOM traversal.
- No Binding Required: Using arrow functions or extracting state via
FormDataavoids scope confusion andthismanipulation. - Scalability: To add a new filter (e.g., matching by
itemname), you only need to add an input withname="item"to the form; no extra JavaScript filter logic is needed. - Performance: Constructing HTML strings using
Array.prototype.map().join('')avoids repeated DOM reflows caused by updatinginnerHTMLin a loop.