I need to trigger a callback if the value of a dcc.Dropdown component in dash is modified by the user. Essentially, I am looking to avoid the cases where the value component of a prop is modified by a different callback (anything other than the user).
It seems that dash does not have any method for distinguishing whether a user/something else modified a prop. As such, I have been attempting to use event listeners to detect when the dcc.Dropdown is changed due to a click or keydown event.
document.addEventListener('DOMContentLoaded', function() {
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes.length) {
const dropdown = document.getElementById('my-dropdown');
if (dropdown) {
dropdown.addEventListener('click', function(event) {
if (event.target && event.target.matches("div.VirtualizedSelectOption")) {
console.log(event.target.innerText);
}
});
observer.disconnect(); }
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
});
This does detect selections of a value from the dcc.Dropdown, but does not detect if the user removes the value.
The reason I am looking for a solution to this is I need to modify the values in this list dependent on future selected values. For example if there ends up being a duplicate entry added I need to remove the value in the dcc.Dropdown as it is no longer a valid entry. This cannot be done statically (eg. disabling options which lead to duplicates) as the options change frequently.
Issue: But does not detect if the user removes the value.
This is likely because when the user removes the value (by clicking on the Remove ('x') icon), the event propagation is stopped. So the click
event on the dropdown is not getting triggered.
Solution: Instead of click
event, use the mousedown
event. You also need to use the keydown
event, because the user can clear the selected value by pressing the Backspace key.
Example: I've only included the part of your code where you need to make the changes.
...
// Check for both addedNodes and removedNodes
if (mutation.addedNodes.length || mutation.removedNodes.length) {
const dropdown = document.getElementById('my-dropdown');
if (dropdown) {
['mousedown', 'keydown'].forEach((eventType) => {
dropdown.addEventListener(eventType, function(event) {
...
});
})
...
}
}
...
Update: You can also achieve what you want using without mutation observers. The idea is simple: when the user modifies the dropdown it must be preceded by either mousedown
or keydown
event. So in the event listeners compare the previous and current dropdown values and if there are added/removed values then trigger your callback.
Code:
const dropdown = document.getElementById("demo-dropdown");
let selectedValues = [...document.getElementsByClassName('Select-value')];
let selectedValuesLength = selectedValues.length;
['mousedown', 'keydown'].forEach((eventType) => {
dropdown.addEventListener(eventType, (event) => {
setTimeout(() => {
if (document.getElementsByClassName('Select-value').length !== selectedValuesLength) {
console.log('value changed...')
let newSelectedValues = [...document.getElementsByClassName('Select-value')];
let addedNodes, removedNodes;
addedNodes = [...newSelectedValues].filter(node => {
return ! [...selectedValues].some(node2 => {
return node.textContent === node2.textContent;
})
})
removedNodes = [...selectedValues].filter(node => {
return ! [...newSelectedValues].some(node2 => {
return node.textContent === node2.textContent;
});
})
console.log(addedNodes)
console.log(removedNodes)
if (addedNodes.length || removedNodes.length) {
// Trigger your callback here
}
selectedValues = [...document.getElementsByClassName('Select-value')];
selectedValuesLength = document.getElementsByClassName('Select-value').length;
}
}, 500)
})
})