A real settings UI: the config dialog
Give your extension a proper configuration window — opened from its context menu, saving through the settings API, closing with a result.
Why it matters
Cramming a settings form into the extension zone steals the pixels your actual feature lives in, and it shows configuration to viewers who should never touch it. The dialog pattern is how every polished extension on the Exchange solves this: a configure entry on the zone's context menu opens a separate page in a Tableau-managed window, that page saves through the settings API, and the parent re-renders when SettingsChanged fires. The parts people miss: the dialog is just another page from your same host, the full Extensions API works inside it, and the viewer closing the window arrives as a promise rejection — a cancel to handle, not an error to log.
Add a configuration dialog to my Tableau dashboard extension. In the .trex, add a context-menu block with configure-context-menu-item inside the dashboard-extension element. In the extension's JavaScript, pass a configure callback to tableau.extensions.initializeAsync that calls tableau.extensions.ui.displayDialogAsync with a config.html page served from the same host, an opening payload string, and { height: 420, width: 520 } — catching the rejection thrown when the user closes the window and treating it as cancel. In config.html, call tableau.extensions.ui.initializeDialogAsync() to receive the payload, save form values via tableau.extensions.settings.set plus saveAsync, and close with tableau.extensions.ui.closeDialog('saved'). The parent re-reads its config in its SettingsChanged listener.
Step 1 — The parent: declare it, open it
Two additions on the extension’s side. In the manifest, a context-menu block with configure-context-menu-item puts a Configure… entry on the zone’s context menu. In the code, initializeAsync takes an object whose configure callback Tableau invokes when that entry is clicked — and inside it, displayDialogAsync opens a page of yours in a Tableau-managed window, sized by you, primed with a payload string.
The promise resolves with whatever the dialog passes to closeDialog — and rejects if the viewer closes the window with the X. That rejection is the cancel path of your UI. Catch it silently.
<!-- In the .trex, inside <dashboard-extension>,
alongside source-location and icon: -->
<context-menu>
<configure-context-menu-item/>
</context-menu>(async () => {
await tableau.extensions.initializeAsync({
configure: () => {
openConfigDialog(); // fired by the zone's Configure… menu entry
},
});
})();
async function openConfigDialog() {
const url = window.location.origin + '/config.html';
try {
const result = await tableau.extensions.ui.displayDialogAsync(
url, 'opening-payload', { height: 420, width: 520 });
console.log('dialog closed with:', result);
} catch (e) {
// Viewer hit the X — a cancel, not an error.
}
}Step 2 — The dialog: a page with superpowers
Inside the dialog you call initializeDialogAsync() — the dialog’s variant of the handshake — and it resolves with the payload the parent sent. From there the page has the full Extensions API: read the dashboard to populate a worksheet dropdown, and, critically, the settings API — so the dialog saves with the same set + saveAsync you already know, and the parent’s SettingsChanged listener re-renders the moment the save lands.
closeDialog(string) ends the session and becomes the parent’s resolved value — useful for distinguishing saved from cancelled without re-reading anything.
// config.html — the dialog page's script.
(async () => {
const payload = await tableau.extensions.ui.initializeDialogAsync();
console.log('opened with:', payload);
// Full API in here: e.g. populate a dropdown from the dashboard.
const sheets = tableau.extensions.dashboardContent.dashboard.worksheets;
console.log('configurable targets:', sheets.map(w => w.name).join(', '));
document.getElementById('save').onclick = async () => {
const threshold = document.getElementById('threshold').value;
tableau.extensions.settings.set('config',
JSON.stringify({ threshold: Number(threshold) }));
await tableau.extensions.settings.saveAsync();
tableau.extensions.ui.closeDialog('saved');
};
document.getElementById('cancel').onclick =
() => tableau.extensions.ui.closeDialog('cancelled');
})();Got what you came for? Mark the stop and the line fills in beneath you.