I am working on a dynamic table filtering feature using vanilla JavaScript, where I use dropdowns to filter data displayed in a table. Each dropdown has a default option (with an empty string value) and several other options corresponding to the data columns. My issue is that the change event does not seem to trigger when the default option is re-selected, after a different option had been selected. This prevents the table from resetting to its original, unfiltered state.
Here's a simplified version of my dropdown HTML before and after making a selection:
<th>
<select id="filter-findingStatus" class="filter-dropdown" data-columnname="findingStatus">
<option value="">All findingStatus</option>
<option value="in_progress">in_progress</option>
<option value="false_positive">false_positive</option>
<option value="closed">closed</option>
<option value="ignored">ignored</option>
<option value="new">new</option>
</select>
After making a selection:
<th>
<select id="filter-findingStatus" class="filter-dropdown" data-columnname="findingStatus">
<option value="">All findingStatus</option>
<option value="ignored">ignored</option>
</select>
</th>
And here's the JavaScript part where I listen for the change event and attempt to filter the table: it's apart of the constructor table class.
class Table {
constructor(tableId, headers) {
this.tableId = tableId;
this.headers = headers;
this.uniqueValues = {};
this.currentFilters = {};
this.originalData = [];
this.loadedData = [];
this.currentlyLoadedCount = 0;
this.loadIncrement = 20;
console.info(`Table ID: ${this.tableId}`);
const tableIdElement = document.getElementById(tableId);
// Check if the table element exists
if (!tableIdElement) {
console.error(`Table not found for tableId: ${tableId}`);
return;
}
if (tableIdElement) {
const scrollContainer = tableIdElement.closest('.table-scroll-container');
if (scrollContainer) {
scrollContainer.addEventListener('scroll', () => {
if ((scrollContainer.offsetHeight + scrollContainer.scrollTop) >= scrollContainer.scrollHeight) {
this.loadMoreRows();
}
});
} else {
console.error(`Scroll container not found for table with id: ${this.tableId}`);
}
} else {
console.error(`Table with id: ${this.tableId} not found`);
}
const dataBody = document.getElementById(this.tableId).querySelector('tbody');
if (!dataBody) {
console.error(`Table body not found for ID: ${this.tableId}`);
}
dataBody.innerHTML = '';
// Get all filter dropdowns
const filterDropdowns = document.querySelectorAll('.filter-dropdown');
filterDropdowns.forEach(dropdown => {
console.log(`Adding event listener to dropdown with column name: ${dropdown.dataset.columnname}`);
dropdown.addEventListener('change', (event) => {
console.log('Dropdown change event triggered', event);
// Get the selected value
const selectedValue = event.target.value;
// Get the column name from the dropdown's data-columnName attribute
const columnName = dropdown.dataset.columnname;
// Call the filterTables method with the column name and selected value
this.filterTables(columnName, selectedValue);
});
});
}
My filters table method:
filterTables(columnName, selectedValue) {
console.log(`Filtering tables for column: ${columnName}, selected value: ${selectedValue}`);
// If the selected value is the default value, remove the filter for this column
if (selectedValue === '') {
console.log(`Default (empty) value selected for column: ${columnName}. Removing filter for this column.`);
delete this.currentFilters[columnName];
// Reset the loadedData to the originalData
this.loadedData = [...this.originalData];
} else {
// Otherwise, update the current filters
console.log(`Non-default value selected for column: ${columnName}. Updating filter for this column.`);
this.currentFilters[columnName] = selectedValue;
}
console.log(`Current filters:`, this.currentFilters);
// Update the dropdown to show the currently selected value
const dropdown = document.querySelector(`.filter-dropdown[data-columnName="${columnName}"]`);
if (dropdown) {
dropdown.value = selectedValue;
}
// Apply all filters to the originalData array
this.loadedData = this.originalData.filter(item => {
for (let column in this.currentFilters) {
const cellValue = item[column] || '';
const filterValue = this.currentFilters[column];
if (filterValue !== '' && !cellValue.includes(filterValue)) {
return false; // Exclude this item if it doesn't match the filter
}
}
return true; // Include this item if it matches all filters
});
console.log(`Loaded data after applying filters:`, this.loadedData);
// Clear the table
const dataBody = document.getElementById(this.tableId).querySelector('tbody');
dataBody.innerHTML = '';
// Reset the currentlyLoadedCount
this.currentlyLoadedCount = 0;
// Load the first batch of rows
this.loadMoreRows();
// Store the unique filter options based on the currently filtered data
this.storeAllUniqueFilterOptions(this.loadedData);
}
These 2 methods are how I get the dropdown options and append them:
storeAllUniqueFilterOptions(data) {
// Reset the uniqueValues object
this.uniqueValues = {};
data.forEach(item => {
this.headers.forEach(header => {
const cellValue = item[header] || '';
if (!this.uniqueValues[header]) {
this.uniqueValues[header] = new Set();
}
this.uniqueValues[header].add(cellValue);
});
});
this.populateFilterDropdowns();
}
populateFilterDropdowns() {
for (const [key, valueSet] of Object.entries(this.uniqueValues)) {
const dropdown = document.querySelector(`.filter-dropdown[data-columnName="${key}"]`);
if (dropdown) {
// Preserve the default option so it can be re-added after clearing
const defaultOption = dropdown.querySelector('option[value=""]');
// Clear old options
dropdown.innerHTML = '';
// Re-append the default option
if (defaultOption) {
dropdown.appendChild(defaultOption.cloneNode(true)); // Append a cloned node of the default option
}
// Populate filter dropdowns with unique values
valueSet.forEach(value => {
const option = document.createElement('option');
option.value = value;
option.textContent = value;
dropdown.appendChild(option);
});
}
}
}
However, no logs are triggered when I select the default option, including logs inside the change event. meaning the event does not fire in this case, although it works fine for all other options. This behavior prevents the data from resetting as intended.
What I've tried:
Verifying the event listener is correctly attached to all other options Checking for JavaScript errors in the console (none found). Manually triggering the event when the default option is selected (not ideal and couldn't get it to work as intended).
My question: How can I ensure the change event fires even when the default option is re-selected, so that my table can reset to its original, unfiltered state?
Any insights or suggestions would be greatly appreciated!
was able to find a solution, the default value was staying selected even after the filter was applied making it un-selectable. so I just needed to reset the previously selected value at the end of populating the filterDropdowns.
populateFilterDropdowns() {
for (const [key, valueSet] of Object.entries(this.uniqueValues)) {
const dropdown = document.querySelector(`.filter-dropdown[data-columnName="${key}"]`);
if (dropdown) {
// Remember the currently selected value
const selectedValue = dropdown.value;
// Preserve the default option so it can be re-added after clearing
// Clear old options
dropdown.innerHTML = '';
// Create and append the default option
this.createDefaultOption(dropdown, key);
// Populate filter dropdowns with unique values
valueSet.forEach(value => {
const option = document.createElement('option');
option.value = value;
option.textContent = value;
dropdown.appendChild(option);
});
// Reselect the previously selected value
dropdown.value = selectedValue;
}
}
}