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 FormData avoids scope confusion and this manipulation.
  • Scalability: To add a new filter (e.g., matching by item name), you only need to add an input with name="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 updating innerHTML in a loop.