React when the dashboard changes
Re-run your code the instant a viewer changes a filter, selects marks, or moves a parameter — no polling, no refresh button.
Why it matters
A dashboard is alive — viewers click, filter, and highlight constantly — and an extension that reads data once at load is stale within seconds. The event system inverts the relationship: subscribe to FilterChanged, MarkSelectionChanged, or ParameterChanged, and Tableau calls you. Mark selection is the sleeper feature: it turns every viz into an input device for your page — the viewer clicks a bar, your extension shows the detail for exactly that bar. The discipline that keeps it working: every addEventListener returns an unregister function. Keep it and call it before re-subscribing, or reconfiguration will double-fire every handler you own.
Write the JavaScript for a Tableau dashboard extension that keeps a live status line current. Subscribe every worksheet to tableau.TableauEventType.FilterChanged and MarkSelectionChanged; on filter change, await event.getFilterAsync() and display which field changed on which worksheet; on selection change, await event.getMarksAsync() and display how many marks are selected plus the formattedValues of the first selected row. Also subscribe each parameter from dashboard.getParametersAsync() to ParameterChanged and display the new currentValue. Collect every unregister function returned by addEventListener into an array and expose a teardown() that calls them all — explain why that matters.
Worksheet events cover the two viewer actions that matter most: FilterChanged and MarkSelectionChanged. The event object is deliberately thin — it tells you that something changed and hands you an async getter for the details, so the pattern is always event arrives, await the getter, update your UI. Parameters subscribe individually: get each one, then listen on it.
Note what the code below does with the return values — every subscription’s unregister function goes into an array. That receipt is the difference between an extension that survives being reconfigured and one whose handlers stack up and fire in multiples.
const unregisters = [];
(async () => {
await tableau.extensions.initializeAsync();
const dashboard = tableau.extensions.dashboardContent.dashboard;
for (const ws of dashboard.worksheets) {
unregisters.push(ws.addEventListener(
tableau.TableauEventType.FilterChanged,
async (event) => {
const filter = await event.getFilterAsync();
console.log(ws.name + ': filter changed on ' + filter.fieldName);
}));
unregisters.push(ws.addEventListener(
tableau.TableauEventType.MarkSelectionChanged,
async (event) => {
const marks = await event.getMarksAsync();
const table = marks.data[0];
const count = table ? table.data.length : 0;
console.log(ws.name + ': ' + count + ' marks selected');
if (count > 0) {
console.log(table.data[0].map(c => c.formattedValue).join(' | '));
}
}));
}
// Parameters subscribe one by one.
for (const p of await dashboard.getParametersAsync()) {
unregisters.push(p.addEventListener(
tableau.TableauEventType.ParameterChanged,
async (event) => {
const param = await event.getParameterAsync();
console.log('parameter ' + param.name + ' -> ' +
param.currentValue.formattedValue);
}));
}
})();
// Call before re-subscribing — or handlers stack and double-fire.
function teardown() {
unregisters.forEach(un => un());
unregisters.length = 0;
}Got what you came for? Mark the stop and the line fills in beneath you.