# Extension Fundamentals > Data access, dialogs, events, formatting, and TypeScript for extensions. Shared extension mechanics. Data Apps packaging and administration are in llms-dataapps.txt. ## Extension Fundamentals --- ## Add a Configuration Popup Dialog Box If you want users to be able to configure settings for your extension, you can use an optional callback function when you initialize your dashboard or viz extension. The callback function creates a configuration option that can be used to open a popup window (dialog box) for your extension. The Extensions API supports different dialog box styles: modal, modeless, or window. You can use the dialog box to allow users to set and save settings for the extension. Starting with Tableau 2026.1 (and the v1.16 library), you can create multiple dialog boxes and send messages between them. :::info By design, there can be only one context configuration popup window or dialog box. For information about creating and using multiple popup dialog boxes or window, see [Add a Multiple Popup Dialog Boxes](trex_multiple_dialogs.md). ::: ### Add the context menu to the `.trex` file The first step is to add the `` option to the extension's manifest file (`.trex`). The `` element only contains one item: ``. * The context menu option must follow the `` and `` elements in the manifest file in the `` or `` section: ```xml ``` ### Create a configuration function When you initialize an extension, you can pass an optional `contextMenuCallbacks` object to the `initializeAsync()` function. This object maps a special ID or key (which must be `'configure'`) to a function you create. * For dashboard extensions, the function you create, in conjunction with adding a `` item to the manifest, adds a new **Configure...** context menu item to the zone of the extension inside a dashboard. * For viz extensions, the function you create, in conjunction with adding a `` item to the manifest, adds a new **Format Extension** button to the Marks card. When the user selects the context menu item, or selects **Format Extensions** button, the configuration function you specified is executed. **Dashboard extensions configuration menu** ![](../assets/extension_configure_menu.png) ---- **Viz extensions format button** ![](../assets/viz_format_btn_75.png) For example, you could use the UI namespace and have the configuration function call the `displayDialogAsync()` function. The function then creates a dialog box that can be used to change settings for the extension. The parent (or initial window) for your extension might have the following JavaScript code. This example uses an initial payload string value, *defaultIntervalInMin*, to pass to the configuration dialog. The payload value is modified in the configuration dialog and is returned in the `closeDialog()` method. Alternatively, you could use a `Settings` object to store the key/value pairs that configure your extension. The `popupURL` is the URL to your configuration window. You will need to create the HTML web page and JavaScript code for the configuration window. ```javascript // Wrap everything in an anonymous function to avoid polluting the global namespace (function () { const defaultIntervalInMin = '5'; $(document).ready(function () { // ... // pass the object to initializeAsync() to map 'configure' key to a function called configure() // ... tableau.extensions.initializeAsync({'configure': configure}).then(function() { // ... // ... code to set up event handlers for changes to configuration }); }); function configure() { // ... code to configure the extension // for example, set up and call displayDialogAsync() to create the configuration window // and set initial settings (defaultIntervalInMin) // and handle the return payload // Define the URL of the configuration popup window // This uses the window.location.origin property to retrieve the scheme, hostname, and // port where the parent extension is currently running, so this string doesn't have // to be updated if the extension is deployed to a new location. const popupUrl = `${window.location.origin}/Samples/Dashboard/UINamespace/uiNamespaceDialog.html`; // // Specify the style of dialog box: modal, modeless, or window let dialogStyle = tableau.DialogStyle.Modeless; // ... // initial payload string value, `defaultIntervalInMin` set tableau.extensions.ui.displayDialogAsync(popupUrl, defaultIntervalInMin, { height: 500, width: 500, dialogStyle }).then((closePayload) => { // The promise is resolved when the dialog has been expectedly closed, meaning that // the popup extension has called tableau.extensions.ui.closeDialog. // ... // The close payload is returned from the popup extension via the closeDialog() method. // .... }).catch((error) => { // ... // ... code for error handling }); } })(); ``` ### Create the HTML and JavaScript code for your configuration window In the JavaScript code for the popup dialog window, add your code for initializing the dialog (`initializeDialogAsync`) and code for setting and saving the configuration settings. Include this JavaScript file in your HTML code for the configuration window. ```javascript $(document).ready(function () { // The only difference between an extension in a dashboard and an extension // running in a popup is that the popup extension must use the method // initializeDialogAsync instead of initializeAsync for initialization. // This has no affect on the development of the extension but is used internally. tableau.extensions.initializeDialogAsync().then(function (openPayload) { // The openPayload sent from the parent extension, in this example, is the // default time interval for the refreshes. This could alternatively be stored // in settings, but is used in this sample to demonstrate open and close payloads. // code goes here }); }); ``` In your code to close the popup window, you must pass a *non-empty* string value as the return payload, even if you're using the `Settings` object to pass your configuration parameters. For example, you could specify a string with a single blank space (`" "`) as the return payload. ```javascript function closeDialog() { // Save the settings with tableau.extensions.settings.saveAsync() // Or pass the new configuration setting in the close payload tableau.extensions.ui.closeDialog('NewInterval'); console.log("Settings saved"); }); ``` To better understand how to use the context menu or configuration window, and to see it in action, check out the [UINamespace](https://github.com/tableau/extensions-api/tree/master/Samples/Dashboard/UINamespace?=target="_blank") sample. --- ## Events and Event Handling Using the Extensions API, you can listen for events on objects, such as worksheets, parameters, and settings. The Tableau Extension API supports a range of events and provides methods for adding and removing event listeners. To manage the event listener, each object has an *event listener manager*. The manager provides a way to add or remove multiple events on objects independently. In the Extensions API, the `sheet` is an abstract class that both `dashboard` and `worksheet` inherit from. Note that `sheet` inherits from the `EventListenerManager` (as do `parameter` and `settings` objects). You can add event listeners on the dashboard and on individual sheets. If you have an event listener, when the specified event is raised, the callback method you provide is called to handle the event. You can use event listeners to trigger specific actions based upon worksheet interactions. For example, a marks selection event can be raised for a particular sheet in a dashboard. Each event contains an anonymous object with information pertaining to that event, such as the type or name of event and object the event occurred on. Listening to an event is done by calling the `addEventListener(eventType, callback)` method and passing in a callback function to handle the event. ### Supported events The `addEventListener` method is available for specific objects (worksheet, parameter, settings, and dashboard). If the object does not support the specified `eventType`, the method throws an exception. The following table shows the event types supported by objects. | object | eventType | | ------ | ----- | | Worksheet | `FilterChanged` , `MarkSelectionChanged`, `SummaryDataChanged` | | Parameter | `ParameterChanged` | | Settings | `SettingsChanged` | | Dashboard | `DashboardLayoutChanged` , `WorkbookFormattingChanged` | ### Add an event listener 1. Get the object that you want to observe (this could be an individual sheet, or it could be a parameter change). 2. Add the event listener by calling `addEventListener` and specifying the type of event (`eventType`) and the callback method to call. The name of the event must be one of the supported types defined in the `TableauEventType` enumeration. The callback method must handle the event object raised. **Note:** For information about the Tableau Extensions API, see the [API Reference](pathname:///api/index.html). #### Example In most cases, you can create an event listener by chaining the methods to the sheet object. In the following example, the name of the event is `tableau.TableauEventType.FilterChanged` or a `FilterChanged` event. When a user changes filters in the sheet, a `FilterChanged` is raised, and the callback method `filterChangedHandler` is called to handle the event. ```javascript // ... // Add filter event to each worksheet. AddEventListener returns a function that will // remove the event listener when called. let unregisterHandlerFunction = worksheet.addEventListener(tableau.TableauEventType.FilterChanged, filterChangedHandler); unregisterHandlerFunctions.push(unregisterHandlerFunction); // ... ``` For more information, check out the sample extension, [Filtering](https://github.com/tableau/extensions-api/tree/main/Samples/Dashboard/Filtering). --- ## Add Tableau Workbook Formatting To help your dashboard extension match the look and feel of the dashboard in which it is placed, you can apply the formatting styles used in the workbook to the HTML elements in your extension. When the formatting changes in the workbook, the corresponding styles are updated in your extension. ---- ### Apply Tableau classes to HTML elements Starting with the Dashboard Extensions API v1.7 library, and supported with Tableau 2021.4 or later, you can apply workbook formatting styles by specifying the class on the HTML elements in your extension. The specific Tableau classes to use are defined in the [`ClassNameKey`](pathname:///api/enums/ClassNameKey.html) enum. | HTML style (string literal) | ClassNameKey enum| | :------------ | :---------- | | `tableau-dashboard-title` | `tableau.ClassNameKey.DashboardTitle` | | `tableau-story-title` | `tableau.ClassNameKey.StoryTitle` | | `tableau-tooltip` | `tableau.ClassNameKey.Tooltip` | | `tableau-worksheet` | `tableau.ClassNameKey.Worksheet` | | `tableau-worksheet-title` | `tableau.ClassNameKey.WorksheetTitle` | #### As an HTML class To apply the formatting in the body of your HTML page to HTML elements, use the string literal `tableau-*` for the `ClassNameKey` enum. For example, to apply the worksheet title formatting you set the `class` for the HTML element in your extension (`h1`, `h2`, etc.) to `"tableau-worksheet-title"`. ```html

Subheader, using tableau-worksheet-title class

``` #### In JavaScript code To reference the workbook formatting in your JavaScript code, use the enum directly. For example, to add the formatting used in the worksheet title, you could use the JQuery method (`.addClass`) to add the `tableau.ClassNameKey.WorksheetTitle` to an element on your page. The following example applies the formatting to a heading with the id `someTitle`. ```javascript $('#someTitle').addClass(tableau.ClassNameKey.WorksheetTitle); ``` You could also create a JavaScript function that has `ClassNameKey` as an parameter, and use that function to process and apply the formatting in some way. ```javascript myFormattingFunction(tableau.ClassNameKey.WorksheetTitle); // does something with the worksheet title ``` ### Access information about the formatting styles in the workbook You can access the formatting used in a Tableau workbook from the `tableau.extensions.environment.workbookFormatting` property. The `workbookFormatting` property, `formattingSheets` contains an array of CSS properties for the workbook, organized by `ClassNameKey`. For example, if you wanted to view the formatting styles available using the Chrome DevTools, you could add the following JavaScript code and then view the results in the Console window by navigating the array. ```javascript if (tableau.extensions.environment.workbookFormatting) { console.log(tableau.extensions.environment.workbookFormatting.formattingSheets); }; ``` Or to just print out the formatting information in the Console window you could do something like the following: ```javascript let formattingSheets = tableau.extensions.environment.workbookFormatting.formattingSheets; formattingSheets.forEach(function (formattingSheet) { console.log("The formatting sheet is " + formattingSheet.classNameKey + " " + JSON.stringify(formattingSheet.cssProperties)); }); ``` ### Set an event listener on workbook format changes You can set an event listener on changes to the formatting in the workbook. The new event type is `WorkbookFormattingChanged`. The `WorkbookFormattingChanged` event is triggered whenever the workbook formatting is changed in Tableau authoring mode. This includes changes in the font, the font size, whether it is bold, italic, or underlined, and changes in color. ```javascript dashboard.addEventListener(tableau.TableauEventType.WorkbookFormattingChanged, (event) => { console.log(event.formatting); // take some other actions based on the change }); ``` For more information about using event listeners, see [Events and Event Handling](./trex_events.md). ### What's Next? * To see a working sample dashboard extension that uses workbook formatting, see the JavaScript [Formatting](https://github.com/tableau/extensions-api/tree/main/Samples/Dashboard/Formatting) sample in the Samples folder, or the TypeScript [Formatting](https://github.com/tableau/extensions-api/tree/main/Samples-Typescript/Dashboard/Formatting) sample in the Samples-Typescript folder. * For information about accessing the formatting styles in a workbook, see [workbookFormatting](pathname:///api/interfaces/Environment.html#workbookformatting) in the API reference documentation. --- ## Getting Data from the Workbook The Tableau Extensions API provides methods that you can use to access the data in a workbook. The data you can access includes the summary or aggregated data, and also the underlying data (or full data). If your extension needs to access the full data, there are security implications and your extension needs to declare its intent, so that users of your extension can choose to allow or deny the extension access. See [Accessing Underlying Data](../security/trex_data_access.md) for more information. ::::note Tableau 2020.2 introduced a new data model. To support that model, the Extensions API provides new methods and data structures starting with version 1.4 of the Extensions API library. In the new data model, a data source could have multiple logical tables, and logical tables can contain one or more physical tables. If you have an existing extension that uses one of the deprecated methods to get underlying data, the method call could fail if the data source contains more than one logical table. You should update your extensions to use these new methods. The new methods are backward compatible with previous versions of Tableau. For information about the data model, see [The Tableau Data Model](https://help.tableau.com/v2020.2/pro/desktop/en-us/datasource_datamodel.html). :::: --- ### Get data from a worksheet The Extensions API provides several methods for accessing data from a dashboard. The method you use depends in part upon how you want to use the data, and on the version of Tableau and the Extensions API library you are using. When you have the worksheet object, you can get the summary data (aggregated data) or the full data (underlying data) directly from the worksheet, using these methods: | Method | Tableau Version | Extensions API Library | Status | | :------ | :---------------| :--------------------- | :------| | `Worksheet.getSummaryDataReaderAsync()` | Tableau 2022.4 and later | version 1.10 and later | Current | | `Worksheet.getSummaryDataAsync()` | Tableau 2018.2 and later | version 1.1 and later | Deprecated | | `Worksheet.getUnderlyingDataAsync()` | Tableau 2018.2 to 2020.1 | version 1.1 to 1.3 | Deprecated | | `Worksheet.getUnderlyingTablesAsync()` | Tableau 2018.2 and later | version 1.4 and later | Current | | `Worksheet.getUnderlyingTableDataAsync()` | Tableau 2018.2 and later | version 1.4 and later | Current | | `Worksheet.getUnderlyingTableDataReaderAsync()` | Tableau 2022.4 and later | version 1.10 and later | Current | The data is returned in a `DataTable` object. The `getSummaryDataReaderAsync` and `getUnderlyingTableDataReaderAsync` methods return a `DataTableReader` object, which you can use to return the pages of `DataTable` objects. The `DataTable` object contains the columns and data values, and information about the data, whether it is underlying or summary data, and in the case of underlying data, whether there is more data than can be returned. Starting in Tableau 2020.2, with the introduction of the new data model and logical tables, you need to use the `Worksheet.getUnderlyingTablesAsync` method to first return the array of `LogicalTable` objects. The `LogicalTable` objects returned are determined by the measures in the worksheet. The `LogicalTable` objects correspond to the tables shown in the default view on the **Data Source** page in Tableau. These tables make up the *logical layer*. You can retrieve the underlying or logical data from each `LogicalTable` in the worksheet or data source. | Object/Property| Description | | :------------ | :---------- | | `LogicalTable` | Object that represents a logical table in a data source, or logical table used in a worksheet | | `LogicalTable.caption` | The name of the logical table as it appears in Tableau. | | `LogicalTable.id` | The identifier used to specify the logical table. Use this value to call `getUnderlyingTableDataAsync` or `getUnderlyingTableDataReaderAsync`. | You can also get the data from the selected marks in the worksheet, or the marks that are currently highlighted in the worksheet. The following two functions return a `MarksCollection`, which is an array of `DataTable` objects. `Worksheet.getSelectedMarksAsync()` `Worksheet.getHighlightedMarksAsync()` ### Get the worksheet object The first step for accessing data of any kind is to get the worksheet object (or objects) that you want. ```javascript // After initialization, ask Tableau what sheets are available const worksheets = tableau.extensions.dashboardContent.dashboard.worksheets; // Find a specific worksheet var worksheet = worksheets.find(function (sheet) { return sheet.name === "Name of Worksheet I want"; }); // Or iterate through the array of worksheets worksheets.forEach(function (worksheet) { // process each worksheet... }); ``` After you have a worksheet object, you can call one of the methods to access the data for that worksheet. For summary data, or the data from the selected or highlighted marks, the steps are straight forward. If you want access the underlying data (or full data), there are additional steps and considerations. See [Accessing Underlying Data](../security/trex_data_access.md). ### Get summary data from a worksheet Starting with Tableau 2022.4 and the Dashboard Extensions API library v1.10, you should use the `getSummaryDataReaderAsync()` method to get data from a worksheet. This method returns a `DataTableReader` object that you can use to access the data. If your summary data is less than 4,000,000 rows (400 pages), you can use the `DataTableReader.getAllPagesAsync()` method to retrieve a single `DataTable` from the `DataTableReader`. The following example shows how to do this. ```javascript // Use `await` only inside an `async` method const dataTableReader = await worksheet.getSummaryDataReaderAsync(); const dataTable = await dataTableReader.getAllPagesAsync(); await dataTableReader.releaseAsync(); // ... process data table ... ``` If your summary data contains more than 4,000,000 rows (or 400 pages), use the summary `DataTableReader` to iterate through the pages of data for all rows in the worksheet. In this case, you process each `DataTable` sequentially. You can control the page size, using the optional `pageRowCount` parameter when you call `getSummaryDataReaderAsync`. The default page size is 10,000 rows. Using the `DataTableReader` you can create a loop to retrieve each page of summary data, using `DataTableReader.pageCount` or `DataTableReader.totalRowCount` property to determine the number of pages to process. Use the `getPageAsync` method to get the `DataTable` from each page. After you have retrieved all pages of the summary data, call the `releaseAsync` method to free up memory from the `DataTableReader`. ```javascript // Use `await` only inside an `async` method const dataTableReader = await worksheet.getSummaryDataReaderAsync(); try { for (let currentPage = 0; currentPage < dataTableReader.pageCount; currentPage++) { const dataTablePage = await dataTableReader.getPageAsync(currentPage); // ... process current page .... } } catch (e) { console.error(e); } finally { // free up resources await dataTableReader.releaseAsync(); } ``` ##### Deprecated method Prior to Tableau 2022.4 and the Dashboard Extensions API library v1.10, you would use the `getSummaryDataAsync` method. This method could fail if there is a large amount of summary data. Modify your code to use the `getSummaryDataReaderAsync` method instead. ```javascript // get the summary data for the sheet worksheet.getSummaryDataAsync().then(function (sumdata) { const worksheetData = sumdata; // The getSummaryDataAsync() method returns a DataTable // Map the DataTable (worksheetData) into a format for display, etc. }); ``` ### Get full data from a worksheet If your extension uses one of the functions that can access full data, you need to add an element to the manifest file (`.trex`) that declares that the extension requires `full data` permission. If the manifest file does not have this element, the extension can run, but the method to access full data will fail. See [Add permissions to access full data to manifest file](../security/trex_data_access.md#add-permissions-to-access-full-data-to-manifest-file). Note whoever uses your extension must have the appropriate download permissions (Download Full Data) so that extension can run. Starting in Tableau 2020.2, and using version 1.4 of the Extensions API v1.4 library, the methods for accessing the underlying data changed. You can use these methods in all versions of Tableau (starting with 2018.2) if you use the v1.4 library or later. #### Get full data using the v1.4 library (and later) Starting in Tableau 2020.2 and later, where the underlying data could include multiple logical tables, you first need to identify the logical table (or tables) you want data from. In previous versions of Tableau, logical tables did not exist. When you use the `getUnderlyingTablesAsync` in Tableau 2020.1 and earlier, the method returns a single table. You can use this single table identifier to call `getUnderlyingTableDataAsync` and `getUnderlyingTableDataReaderAsync`. ##### 1. Get the logical table(s) using getUnderlyingTablesAsync() The first step is to use the `Worksheet.getUnderlyingTablesAsync()` method to return the array of `LogicalTable` objects. The `LogicalTable` objects returned are determined by the measures in the worksheet. If a worksheet's data source contains multiple logical tables and the worksheet only uses measures from one logical table, the method will return one logical table. To get the underlying data for each logical table, you use the `LogicalTable.id` property of the table to call `Worksheet.getUnderlyingTableDataAsync()`. Note that when you use the `getUnderlyingTablesAsync()` in Tableau 2020.1 and earlier, the method will only return a single table, and that table uses `single-table-id-sentinel` as the `LogicalTable.id`. ```javascript // Call to get the underlying logical tables used by the worksheet worksheet.getUnderlyingTablesAsync().then(logicalTables => { // Get the first logical table's id // In Tableau <= 2020.1, the first table is the only table returned. const logicalTableId = logicalTables[0].id; // Use the logicalTableId to then get worksheet's underlying data // by calling worksheet.getUnderlyingTableDataAsync(logicalTableId) or // worksheet.getUnderlyingTableDataReaderAsync(logicalTableId) }); ``` ##### 2a. Get data from the logical table(s) (less than 10,000 rows) If the data doesn't contain more than 10,000 rows, you can use the `getUnderlyingTableDataAsync` method to get data from a worksheet. Using the `logicalTable.id`, you call the `getUnderlyingTableDataAsync` method to return a `DataTable` containing the underlying data (up to 10,000 rows) for the logical table. Repeat this step for each logical table. The following example returns data for the first logical table that is used by a worksheet called *"Sale Map"*. ```javascript var worksheet = tableau.extensions.dashboardContent.dashboard.worksheets.find(w => w.name === "Sale Map"); worksheet.getUnderlyingTablesAsync().then(logicalTables => { worksheet.getUnderlyingTableDataAsync(logicalTables[0].id).then(dataTable => { // process the dataTable... }); }); ``` You can specify the number of rows of data to return by setting `GetUnderlyingDataOptions.maxRows` property. If unspecified (`maxRows == '0'`), the call to `getUnderlyingTableDataAsync` requests all rows in the logical table. Note that the maximum number of rows returned from the `getUnderlyingTableDataAsync` method is limited to 10,000 rows. You can use the `DataTable` property, `isTotalRowCountLimited`, to test whether there is more data. A value of true indicates that the calling function requested more rows than the limit (10,000) and the underlying data source contains more rows than can be returned. If the data contains more than 10,000 rows, use `getUnderlyingTableDataReaderAsync` method instead. ##### 2b. Get data from the logical table(s) (more than 10,000 rows) Starting with Tableau 2022.4 and the Dashboard Extensions API library v1.10, if the data contains more than 10,000 rows, use the `getUnderlyingTableDataReaderAsync` method to get data from a worksheet. In this case, you create a `DataTableReader` to iterate through the pages of data. You process each `DataTable` sequentially. You can control the page size, using the optional `pageRowCount` parameter when you call `getUnderlyingTableDataReaderAsync`. The default page size is 10,000 rows. The basic steps are as follows: 1. Using the `logicalTable.id`, call the `getUnderlyingTableDataReaderAsync` method to create the `DataTableReader`. 1. Create a loop to retrieve each page of underlying data, using `DataTableReader.pageCount` or `DataTableReader.totalRowCount` property of the `DataTableReader` to determine the number of pages to process. 1. Use the `getPageAsync` method to get the DataTable from each page. 1. After you have retrieved all pages of the summary data, call the `releaseAsync()` method to free up memory from the `DataTableReader`. ```javascript // assumes this code is in an async method const worksheet = tableau.extensions.dashboardContent.dashboard.worksheets.find(w => w.name === "Sale Map"); // Call to get the underlying logical tables used by the worksheet const underlyingTablesData = await worksheet.getUnderlyingTablesAsync(); const logicalTableId = underlyingTablesData[0].id; // Use the above logicalTableId to get the underlying data reader on the specified worksheet const dataTableReader = await worksheet.getUnderlyingTableDataReaderAsync(logicalTableId); try { // loop through and process the pages for (let pageNumber = 0; pageNumber < dataTableReader.pageCount; pageNumber++) { let currentPageDataTable = await dataTableReader.getPageAsync(pageNumber); // process page ... console.log(currentPageDataTable); } } catch(e) { console.error(e); } finally { // free up resources await pageReader.releaseAsync(); } ``` The `getUnderlyingTableDataReaderAsync` method attempts to prepare all the rows of the table to be read as pages. There is a limit to the number of rows that can be prepared to conserve computing resources. The default limit is 1 million rows of data. You can change this default row limit with the Tableau Server, Tableau Cloud, or Tableau Desktop option: `ExtensionsAndEmbeddingReaderRowLimit`. For Tableau Desktop, you can set this with a command line option **`-DExtensionsAndEmbeddingReaderRowLimit=`***value*. If the underlying table has many columns, `getUnderlyingTableDataReaderAsync` can be sped up by only requesting native data values (`IncludeDataValuesOption.OnlyNativeValues`) in the `GetUnderlyingDataOptions`. --- #### Deprecated: Get full data (library v1.3 and earlier) If you were using 1.3 version of the Extensions API library (or earlier), you had to use the `getUnderlyingDataAsync` method to get the underlying data from a worksheet in Tableau 2020.1 and earlier. This method has been deprecated, but it shown here for completeness. If you want your extension to work in all versions of Tableau, you should use the latest library (version 1.4 or later) and the `getUnderlyingTablesAsync` and `getUnderlyingTableDataAsync` methods. ```javascript // the following example uses the Superstore workbook and gets the underlying data // for a specific worksheet. // The example writes the values for a single column (states names) to the console. tableau.extensions.dashboardContent.dashboard.worksheets.find(w => w.name === "Sale Map").getUnderlyingDataAsync().then(dataTable => { let field = dataTable.columns.find(column => column.fieldName === "State"); let list = []; for (let row of dataTable.data) { list.push(row[field.index].value); } let values = list.filter((el, i, arr) => arr.indexOf(el) === i); console.log(values) }); ``` --- ### Get data from a data source You can also get the underlying data from the data sources for the worksheet. To do that, you must acquire the data sources for the worksheet with a call to the `getDataSourcesAsync()` method, which returns an array of the primary and all the secondary data sources of a worksheet. Once you have the data source object, you can access the underlying data and access information about the data source, such as the names of tables and fields and information about the connection. Just like worksheet methods that access full data, the following methods for the data source also require that your extension specifies `full data` permissions in the `trex` file. See [Add permissions to access full data to manifest file](../security/trex_data_access.md#add-permissions-to-access-full-data-to-manifest-file). | Method | Tableau Version | Extensions API Library | Status | |:------ | :---------------| :--------------------- |:-------| | `Datasource.getConnectionSummariesAsync()`| Tableau 2018.2 and later | version 1.1 and later | Current | | `Datasource.getDataSourcesAsync()` | Tableau 2018.2 and later | version 1.1 and later | Current | | `Datasource.getActiveTablesAsync()` | Tableau 2018.2 to 2020.1 | version 1.1 to 1.3 | Deprecated | | `Datasource.getUnderlyingDataAsync()` | Tableau 2018.2 to 2020.1 | version 1.1 to 1.3 | Deprecated | | `Datasource.getLogicalTablesAsync()` | Tableau 2018.2 and later | version 1.4 and later | Current | | `Datasource.getLogicalTableDataAsync()` | Tableau 2018.2 and later | version 1.4 and later | Current | | `Datasource.getLogicalTableDataReaderAsync()` | Tableau 2022.4 and later | version 1.10 and later | Current | ### Get the data sources from a worksheet To get the data sources a worksheet uses, you call the `getDataSourcesAsync()` method on the worksheet object. The following code snippet shows how you might select a specific data source of a worksheet. ```javascript tableau.extensions.dashboardContent.dashboard.worksheets.find(w => w.name === "Sale Map").getDataSourcesAsync().then(datasources => { let dataSource = datasources.find(datasource => datasource.name === "Sample - Superstore"); // return dataSource for further processing }); ``` #### Get full data from a worksheet using the v1.4 library (and later) After you have the data source object, you can query the data source for the underlying data. Starting in Tableau 2020.2 and later, the underlying data could include multiple logical tables. Before accessing the data you need to identify the logical table (or tables) of the data source you want data from. In previous versions of Tableau (2020.1 and earlier), logical tables did not exist. When you use the `Datasource.getLogicalTablesAsync()` in Tableau 2020.1 and earlier, the method returns a single table. ##### 1. Get the logical table(s) using getLogicalTablesAsync() The first step in getting the underlying data is to call the `Datasource.getLogicalTablesAsync` method to return the array of `LogicalTable` objects. To get the underlying data for each logical table, use the `LogicalTable.id` property of the table to call `Datasource.getLogicalTableDataAsync()`. Note that when you use the `getLogicalTablesAsync` in Tableau 2020.1 and earlier, the method will only return a single table, and that table uses the `single-table-id-sentinel` as the `LogicalTable.id`. Example that uses a single table: ```javascript // Call to get the logical tables used by the worksheet dataSource.getLogicalTablesAsync().then(logicalTables => { // Get the first logical table's id // In Tableau <= 2020.1, the first table is the only table returned. const logicalTableId = logicalTables[0].id; // Use the logicalTableId to then get worksheet's underlying data // by calling worksheet.getUnderlyingTableDataAsync(logicalTableId) }); ``` Example that writes the names of all the logical tables in the data source to the console: ```javascript // Call to get the logical tables used by the worksheet dataSource.getLogicalTablesAsync().then(logicalTables => { // Loop through each table in this data source logicalTables.forEach( table => { console.log(table.caption); }); }); ``` ##### 2a. Get data from the logical table(s) for less than 10,000 rows If the data doesn't contain more than 10,000 rows, you can use the `getLogicalTableDataAsync` method to get data from a data source. After you have identified the logical tables you want, use the `LogicalTable.id` property of the table to call `Datasource.getLogicalTableDataAsync()`. You can then process the data for that logical table. The following example returns the column names of the first logical table that is in the data source. ```javascript dataSource.getLogicalTablesAsync().then(logicalTables => { // get the underlying data from the first logical table dataSource.getLogicalTableDataAsync(logicalTables[0].id).then(dataTable => { // get the names of the columns in the dataTable let list = []; for (let col of dataTable.columns) { list.push(col.fieldName); } console.log(list); }); }); ``` ##### 2b. Get data from the logical table(s) for more than 10,000 rows Starting with Tableau 2022.4 and the Dashboard Extensions API library v1.10, if the data contains more than 10,000 rows, use the `getLogicalTableDataReaderAsync` method to get data from a data source. In this case, you create a `DataTableReader` to iterate through the pages of data for all rows in the worksheet. You process each `DataTable` sequentially. You can control the page size, using the optional `pageRowCount` parameter when you call `getLogicalTableDataReaderAsync`. The default page size is 10,000 rows. The basic steps are as follows: Using the `logicalTable.id`, call the `getLogicalTableDataReaderAsync` method to create the `DataTableReader`. Create a loop to retrieve each page of logical data, using `DataTableReader.pageCount` or `DataTableReader.totalRowCount` property of the summary `DataTableReader` to determine the number of pages to process. Use the `getPageAsync()` method to get the `DataTable` from each page. After you have retrieved all pages of the data, call the `releaseAsync()` method to free up memory from the `DataTableReader`. ```javascript // assumes this code is in an async method const pageRowCount = 1000; // default is 10,000 const dataSources = await worksheet.getDataSourcesAsync(); const dataSource = dataSources.find(datasource => datasource.name === "Sample - Superstore"); const logicalTables = await dataSource.getLogicalTablesAsync() const dataTableReader = await dataSource.getLogicalTableDataReaderAsync(logicalTables[0].id, pageRowCount); try { // loop through and process each page for (let pageNumber = 0; pageNumber < dataTableReader.pageCount; pageNumber++;) { let currentPageDataTable = await dataTableReader.getPageAsync(pageNumber); // process page ... console.log(currentPageDataTable); } } catch (e) { console.error(e); } finally { // release resources await pageReader.releaseAsync(); } ``` #### Deprecated: Get data from a data source using the v1.3 library (and earlier) If you were using 1.3 version of the Extensions API library (or earlier), you had to use the `getUnderlyingDataAsync` method to get the underlying data from a data source in Tableau 2020.1 and earlier. This method has been deprecated, but it shown here for completeness. If you want your extension to work in all versions of Tableau, you should use the latest library (version 1.4 or later) and the `Datasource.getLogicalTablesAsync()` and `Datasource.getLogicalTableDataAsync` methods. ```javascript tableau.extensions.dashboardContent.dashboard.worksheets.find(w => w.name === "Sale Map").getDataSourcesAsync().then(datasources => { dataSource = datasources.find(datasource => datasource.name === "Sample - Superstore"); return dataSource.getUnderlyingDataAsync(); }).then(dataTable => { // process the dataTable... }); ``` --- ### Compatibility: methods for accessing underlying data To support the data model that was introduced in Tableau 2020.2, where a data source can have logical tables, the Tableau Dashboard Extensions API provides new methods for getting data. The new methods are available starting with version 1.4 of the Extensions API library. Starting with version 1.10 of the library, there are new methods that provide pagination using a `DataTableReader`. The following table shows the compatibility between the methods and the different versions of the Extensions API library and Tableau. If you have an existing Dashboard Extension that accesses underlying data, and you want your extension to work with Tableau 2020.2 or later, you should upgrade to the latest version of the library supported by your version of Tableau. | Methods | API Version | Tableau 2020.1 and earlier | Tableau 2020.2 (single logical table) | Tableau 2020.2 (multiple logical tables) | | :-------- | :---------- | :---------- | :--------- | :-------- | | `Datasource.getUnderlyingDataAsync` `Worksheet.getUnderlyingDataAsync` | v1.3 and earlier | Works as expected | Works, column order will be different | Fails with an exception: Not Supported (2) | | `Datasource.getUnderlyingDataAsync` `Worksheet.getUnderlyingDataAsync` | v1.4 | Works as expected | Works, column order will be different | Fails with an exception: Not Supported (2) | | `Datasource.getLogicalTablesAsync` `Datasource.getLogicalTableDataAsync` `Worksheet.getUnderlyingTablesAsync` `Worksheet.getUnderlyingTableDataAsync` | v1.4 and earlier | Works, maps to existing commands | Works, column order will be different | Works as expected | | `Datasource.getLogicalTableDataReaderAsync`, `Worksheet.getUnderlyingTableDataReaderAsync` | v1.10 and later | -- | -- | Works as expected | --- ### When there is more data than can be returned Some data sources can be very large and could contain thousands and thousands of rows. To minimize the impact of requests for data on Tableau performance, the `getUnderlyingDataAsync()`, `Worksheet.getUnderlyingTableDataAsync()`, and `Datasource.getLogicalTableDataAsync` methods are currently limited to returning 10,000 rows. If the method can't return the full number of rows in your data, the `DataTable` property `isTotalRowCountLimited` is set to TRUE. You can use this property to test whether there is more data than can be returned. If there are more than 10,000 rows of data, use one of the pagination methods, `Datasource.getLogicalTableDataReaderAsync`, `Worksheet.getUnderlyingTableDataReaderAsync`. These methods return a `DataTableReader` that you can use to page through and retrieve the data. Note that the limits do not apply to `getSummaryDataAsync()`. The `getSummaryDataAsync()` method could fail if there is a very large amount of summary data. You should use `getSummaryDataReaderAsync()` instead, the method is available starting with Tableau 2022.4 and the version 1.10 library. The following table illustrates what happens to calls to `getUnderlyingDataAsync()`, `Worksheet.getUnderlyingTableDataAsync()`, and `Datasource.getLogicalTableDataAsync()` for various sizes of data (shown as **Data Rows**). * `maxRows` represents the rows requested as one of the `GetUnderlyingDataOptions`. When `maxRows = 0` the method will attempt to return all rows of data. * `totalRowCount` represents the number of rows returned in the `DataTable`. * `isTotalRowCountLimited` is the Boolean that indicates if there is more rows of data than can be returned. **Rows of data returned** | Data Rows | maxRows (input) | totalRowCount (output) | isTotalRowCountLimited | Comments | |:----------|:-----------------------|:-----------------------|:-----------------------|---------------------------------------------------------| | 15,000 | 0 | 10,000 | TRUE |   | | 15,000 | any number > 10,000 | 10,000 | TRUE | Any number greater than 10,000 exceeds the return limit | | 15,000 | 10,000 | 10,000 | FALSE |   | | 15,000 | 200 | 200 | FALSE |   | | 500 | 0 | 500 | FALSE |   | | 500 | any number > 10,000 | 500 | FALSE |   | | 500 | 200 | 200 | FALSE |   | | 10,000 | 0 | 10,000 | FALSE |   | | 10,000 | any number > 10,000 | 10,000 | FALSE |   | | 10,000 | 10,000 | 10,000 | FALSE |   | | 10,000 | 200 | 200 | FALSE |   | --- ### Handle full data access and permission errors When an extension needs full data access and the user does not have full data permission on the workbook, Tableau currently allows the extension to run, but Tableau will call the promise failure function if the extension calls `getUnderlyingData()`, `Worksheet.getUnderlyingTableDataAsync()`, and `Datasource.getLogicalTableDataAsync()`. This is shown in the following example. ```javascript Worksheet.getUnderlyingTableDataAsync(logicalTables[0].id).then(function(success) { // called on success }, function (err) { // called on any error, such as when the extension // doesn’t have full data permission }); ``` An error is also printed to the console. If you use any of these methods to get the full data, be sure to add error handling for the promise in case of failure. --- ## Add Multiple Popup Dialog Boxes Starting with Tableau 2026.1, the Extensions API supports multiple popup windows or dialog boxes, and supports messaging between them. In addition to the optional configuration popup that you can create when you initialize the extension, you can open other dialog boxes, by adding buttons and controls to your extension. If you're creating multiple dialog boxes, you must consider how you want them to interact with each other and the user. The popup windows can be modal or modeless, and they can share information and payloads. The Extensions API provides two new methods for sending text messages to and from the extension to the popup windows, and from the popup windows to the extension or calling popup window. If you have multiple popup windows, you must track which popup window is active, so you can control their interactions. By design, there can be only one context configuration popup window or dialog box. This context menu item is explicitly declared in the `.trex` file of the extension. For additional popup windows, you don't need to modify the `.trex` file. For information about creating and using the configuration window, see [Add a Configuration Popup Dialog Box](./trex_configure.md). ### Create additional popup windows You can create more than one popup window to provide configuration options for your extension. If you’re creating more popup windows, you must call those popup windows from methods other than the `configure()` method, and you must add controls to call those methods. You can't display these other dialog boxes or windows directly when the user selects the configure context item. However, you can display the other popup windows when users click buttons and controls in your extension, or when users click controls in your configuration dialog box. #### Basic steps * Determine how you want the popup windows to interact (if at all). Should they be modal or modeless? A modal dialog box takes the focus and blocks all other windows until the dialog box is closed. A modeless dialog box does not block other windows, and the focus switches depending upon the user's selections. * Do the popup windows need to share information or content with the extension? The Extensions API provides methods for sending text (string) messages between the popup windows and the extension. * To make it easy to manage your popup windows, create separate HTML and JavaScript files for each window. * Determine where to place the controls to display the popup windows. For example, in your main extension window, or in the configuration dialog. Add the controls (buttons, toolbars) to your extension HTML code to create the additional dialog boxes or popup windows. * Use the [`displayDialogAsync()`](pathname:///api/interfaces/UI.html#displaydialogasync) method to display the popup window. You would probably call the `displayDialogAsync()` method from a wrapper that gets triggered when the user clicks a button or icon in a toolbar. In your extension JavaScript or TypeScript code, define your popup windows: the `popupURL`, the dialog style, and dimensions, and any initial payload you want to send to the popup window. And provide any necessary code for handling the payload that's returned from the popup window. Like the configuration popup, the URL of the additional popup windows must belong to the same domain as the parent extension. The relative paths must resolve to the directory, or a child directory, of the extension. Root-relative paths aren’t allowed. For example, `./config.html` or `config.html` are allowed, but not the root-relative path `/config.html`. * In your HTML and JavaScript code for your popup window, call the [`initializeDialogAsync()`](pathname:///api/interfaces/Extensions.html#initializedialogasync) method and add code to process the initial payload, if you’re using it. In addition to whatever features you want the popup window to do, add a control (for example, a Save button) that you can attach to the `closeDialog()` method. The `closeDialog()` method safely closes the dialog box and returns the payload to the calling extension (or window). * If you have more than one popup window, you must create a system for tracking the state of each popup window. For example, calling `displayDialogAsync()` method for an already-open dialog box throws an error. And if you only want one popup window open at a time, and you don't want them to be modal, you must track which popup is open and which popup is queued up. A simple Map or Flag variable can accomplish this tracking. #### Example: Add color selector dialog box For example, you could add another dialog box to the [uiNamespace](https://github.com/tableau/extensions-api/tree/main/Samples/Dashboard/UINamespace) sample that lets users set the background color of the extension. To do this, you could add a button to display the popup window in the `uiNamespace.html` file. When the user clicks the button, a color formatting dialog opens. The user can select the background color. When the user clicks a Save button, it calls the `closeDialog()` method and closes the popup window, which returns the new background color as the payload, and the new background color is applied. :::tip For the complete listing of the uiNamespace example that shows the addition of a second popup dialog box, see [Complete example source code](#complete-example-source-code). ::: HTML code (uiNamespace.html) ```html ``` JavaScript code (uiNamespace.js) ```javascript // in uiNamespace.js, a button click calls openColorDialog $('#openColorDialog').click(openColorDialog); ``` In the main extension file, `uiNamespace.js`, add code to set the default background color and then call the `displayDialogAsync()` method. To see the entire listing, see [Complete example source code](#complete-example-source-code) and select the **uiNamespace** tab. ```javascript /** * uiNamespace.js (main extension file) * Sets the default background color * Passes the current color to the dialog as the initial payload */ const defaultBgColor = '#FFFFFF'; let currentBgColor = defaultBgColor; ``` The `openColorDialog()` method calls `tableau.extensions.ui.displayDialogAsync()` method, and passes values for the popup window, including the URL, the initial payload, dimensions, and the `DialogStyle`. The `DialogStyle` determines how the popup window interacts with the extension and other elements. | `DialogStyle` value | Behavior | |---|---| | `tableau.DialogStyle.Modal` | Blocks all other windows; users must close this dialog before interacting with the extension. | | `tableau.DialogStyle.Modeless` | Doesn't block other windows; focus follows user interaction. | | `tableau.DialogStyle.Window` | Opens as a separate browser-style window; behavior depends on the browser. | In this code snippet, the dialog style is set to modeless. The complete example extends this to allow the user to choose the dialog style. See [Complete example source code](#complete-example-source-code) and select the **uiNamespace** tab ```javascript /** uiNamespace.js * Defines and displays the dialog box * Opens the background color picker dialog ( in this case, modeless). * Passes the current background color as the initial payload so the dialog * can pre-select it. The closePayload returned is the chosen hex color, * which is then applied to the extension body. * */ function openColorDialog () { const popupUrl = new URL('uiNamespaceColorDialog.html', window.location.href).href; let dialogStyle = tableau.DialogStyle.Modeless; tableau.extensions.ui .displayDialogAsync(popupUrl, currentBgColor, { height: 500, width: 360, dialogStyle: dialogStyle}) .then((closePayload) => { currentBgColor = closePayload; $('body').css('background-color', currentBgColor); }) .catch((error) => { if (error.errorCode === tableau.ErrorCodes.DialogClosedByUser) { console.log('Color dialog was closed by user'); } else { console.error(error.message); } }); } ``` JavaScript code (uiNameSpaceColorDialog.js) In the `uiNameSpaceColorDialog.js` file, add the `initializeDialogAsync()` method. The method processes the initial payload, in this case the current background color. If the user selects a color, that color is returned as the payload in the `closeDialog()` method. The `buildColorList()` and `updatePreview()` methods aren't shown here, but you can see the [Complete example source code](#complete-example-source-code). Those methods let users pick from the colors in a list and then see that color in a preview `div`. When the user clicks the **Save** button, the `sendToParent()` method is called. The `sendToParent()` method is defined in the `uiNameSpaceColorDialog.js` file, and is described under [Send the message to the parent extension or popup window](#send-the-message-to-the-parent-extension-or-popup-window). For information about messaging between windows, see [Send messages to and from the popup window or dialog box](#send-messages-to-and-from-the-popup-window-or-dialog-box). ```javascript // uiNameSpaceColorDialog.js $(document).ready(function () { tableau.extensions.initializeDialogAsync().then(function (openPayload) { // openPayload is the current background color passed from the parent. if (openPayload && openPayload.startsWith('#')) { selectedColor = openPayload; } buildColorList(); updatePreview(selectedColor); $('#saveButton').click(function () { sendToParent('Hello from Dialog box! - Save button clicked'); tableau.extensions.ui.closeDialog(selectedColor); }); }); }); ``` --- ### Send messages between popup dialog boxes and the extension Starting with Tableau 2026.1, the Extensions API provides two methods for sending messages between dialog boxes and between the dialog box and the calling extension or popup window. The messages provide a way of passing information between the popup windows. The messaging is in addition to the optional payload that is returned from the `displayDialogAsync()` method. When the popup window calls the `closeDialog()` method, the `displayDialogAsync()` method returns the payload. The `sendDialogMessageAsync()` and `sendDialogMessageToParentAsync()` methods provide another way to communicate between popup windows and the main extension window. #### Add the message received event listener To enable messaging between the popup windows, you must add an event listener for the `DialogMessageReceived` event. For two-way communication between popup windows and the extension, add the event listener in the code for both the popup window and the extension. For example, the following code snippet shows how a popup window could add the event listener for messages sent from the extension. These are the messages that the extension sends using the `sendDialogMessageAsync()` method. This example logs the message to the console window, and is viewable if you use the debugging tools on the popup window. Popup window or dialog box code: ```javascript tableau.extensions.ui.addEventListener(tableau.TableauEventType.DialogMessageReceived, (event) =>{ console.log(`Message received: ${event.message}`); }); ``` Extension code: In the extension, add an event listener for messages that get sent from the popup window. In this case, the code snippet appends the message from the popup window to the `messageLog` text block in the extension interface. ```javascript // Listen for messages from dialogs sent by sendDialogMessageToParentAsync) tableau.extensions.ui.addEventListener(tableau.TableauEventType.DialogMessageReceived, function (event) { const p = $('

').text(event.message); $('#messageLog').append(p); }); ``` #### Send the message to the parent extension or popup window The popup windows can use the `sendDialogMessageToParentAsync()` method to send a message to the extension or the popup window that opened it (that is, the "parent" of the popup dialog). The `sendDialogMessageToParentAsync()` method takes just one argument, the message to send (a string value). The method only works if it’s called from the popup window. For example, the following code snippets show how you might tie the send message method to a button in the popup window. In this case, there’s a **Save** button (`saveButton`). When the user clicks the **Save** button, a message gets sent to the extension. The `closeDialog()` method is also called when the user clicks the **Save** button, which closes the popup window and then returns the payload to the extension. We wrap the `sendDialogMessageToParentAsync()` method in another function with the `await` keyword, so that the message is guaranteed to be sent before the dialog box is closed. The parent extension's event handler function for the `DialogMessageReceived` event processes the message. ```javascript // ... $('#saveButton').click(function () { sendToParent('Hello from Dialog box! - Save button clicked'); tableau.extensions.ui.closeDialog(selectedColor); }); // ... // send message method is placed in a wrapper function to guarantee // that the message is sent before the dialog is closed async function sendToParent(message) { await tableau.extensions.ui.sendDialogMessageToParentAsync(message); } ``` #### Send messages to and from the popup window or dialog box The extension can use the `sendDialogMessageAsync()` method to send a message to the popup window or dialog box. The method takes two arguments, the message (string) and the optional target URL (`targetDialogUrl`). The `sendDialogMessageAsync()` method can also be used to send messages to the extension from the popup window or dialog box. You might do this if the dialog box was opened by another dialog box (the "parent"), and not by the extension. However, in most cases, to send a message to the extension or to the dialog box that opened the dialog box sending the message (the "parent"), use the `sendDialogMessageToParentAsync()` method. If you are using the `sendDialogMessageAsync()` method to send a message to the extension from the popup window or dialog box, you can omit the target URL. When the target URL isn't specified, the message gets sent to the extension. For example, in the following code snippet, a button in the extension code is tied to a `sendToColorDialog` method. To be able to click the button to send a message, the colorDialog can't have the focus, so the popup dialog box must be modeless. ```javascript // Assumes the extension HTML code specifies a button // with the id 'sendToColorDialog' $('#sendToColorDialog').click(sendToColorDialog); // ... // sends message to the popup window function sendToColorDialog() { let colorDialogUrl = new URL('uiNamespaceColorDialog.html', window.location.href).href; tableau.extensions.ui.sendDialogMessageAsync('Hello from extension!', colorDialogUrl); } ``` --- ### Track the active popup dialog box Depending upon your implementation, and whether your dialog boxes are modal or not, if you have multiple popup dialog boxes you will likely need to track which popup is active. At a minimum, you'll want to prevent errors that result if a user tries to open a dialog box that is already open. This can be done with a simple mapping variable. In this case, we declare a set of objects called `openDialog`. The popup dialog boxes can then be added or deleted from the list, depending upon whether they are opened or closed. ```javascript // Track which dialogs are currently open const openDialogs = new Set(); ``` #### Add the opened popup window to the list In your JavaScript code that calls the `displayDialogAsync()` method, check if the popup dialog box is currently open. If it is, return without calling the `displayDialogAsync()` method. If it is not open, add the URL to the list and continue. ```javascript // code excerpt from openColorDialog() method const popupUrl = new URL('uiNamespaceColorDialog.html', window.location.href).href; if (openDialogs.has(popupUrl)) { return; // Dialog is already open; don't open a second instance } openDialogs.add(popupUrl); ``` #### Remove the closed popup window from the list In your function that handles the promise returned from the `displayDialogAsync()` method, remove the popup URL from the set of open dialog boxes, using the `delete()` method (`openDialogs.delete(popupUrl)`). Be sure to delete the popup dialog method in your error handling code. In the case of errors, where the dialog box isn't closed properly, for example, the Extensions API `closeDialog()` method isn't called, the dialog box is no longer open and needs to be removed from the list. ```javascript tableau.extensions.ui .displayDialogAsync(popupUrl, currentBgColor, { height: 500, width: 360, dialogStyle: dialogStyle }) .then((closePayload) => { openDialogs.delete(popupUrl); currentBgColor = closePayload; $('body').css('background-color', currentBgColor); }) .catch((error) => { openDialogs.delete(popupUrl); if (error.errorCode === tableau.ErrorCodes.DialogClosedByUser) { console.log('Color dialog was closed by user'); } else { console.error(error.message); } }); ``` --- ### Complete example source code This example code adds a second dialog box to the uiNamespace sample. The second popup dialog lets users change the background color of the extension window. The sample also shows how you can use the messaging methods to communicate between the popup dialog boxes and the extension. A simple mapping object is used to track the state of the second popup dialog box. Click the name of the module to see the HTML and JavaScript code. --- * [HTML](#uinamespacehtml) * [JavaScript](#uinamespacejs) * [.trex](#uinamespacetrex) ##### uiNamespace.html ```html Settings Sample

Configure extension to proceed.

Dialog Style
Messages from dialogs
``` ##### uiNamespace.js ```javascript 'use strict'; /** * UINamespace Sample Extension * * This sample extension demonstrates how to use the UI namespace * to create a popup dialog with additional UI that the user can interact with. * The content in this dialog is actually an extension as well (see the * uiNamespaceDialog.js for details). * * This sample is an extension that auto refreshes datasources in the background of * a dashboard. The extension has little need to take up much dashboard space, except * when the user needs to adjust settings, so the UI namespace is used for that. */ // Wrap everything in an anonymous function to avoid polluting the global namespace (function () { const defaultIntervalInMin = '5'; const defaultBgColor = '#FFFFFF'; let activeDatasourceIdList = []; let currentBgColor = defaultBgColor; // Track which dialogs are currently open const openDialogs = new Set(); $(document).ready(function () { // When initializing an extension, an optional object is passed that maps a special ID (which // must be 'configure') to a function. This, in conjunction with adding the correct context menu // item to the manifest, will add a new "Configure..." context menu item to the zone of extension // inside a dashboard. When that context menu item is clicked by the user, the function passed // here will be executed. tableau.extensions.initializeAsync({ configure: configure }).then(function () { // This event allows for the parent extension and popup extension to keep their // settings in sync. This event will be triggered any time a setting is // changed for this extension, in the parent or popup (i.e. when settings.saveAsync is called). tableau.extensions.settings.addEventListener(tableau.TableauEventType.SettingsChanged, (settingsEvent) => { updateExtensionBasedOnSettings(settingsEvent.newSettings); }); // Listen for messages from dialogs (for example, the background color dialog box via sendDialogMessageToParentAsync) tableau.extensions.ui.addEventListener(tableau.TableauEventType.DialogMessageReceived, function (event) { const p = $('

').text(event.message); $('#messageLog').append(p); }); $('#openColorDialog').click(openColorDialog); $('#sendToColorDialog').click(sendToColorDialog); $('#sendToColorDialog').prop('disabled', true); }); }); function configure () { // This uses the window.location.origin property to retrieve the scheme, hostname, and // port where the parent extension is currently running, so this string doesn't have // to be updated if the extension is deployed to a new location. const popupUrl = `${window.location.origin}/uiNamespaceDialog.html`; // This checks for the selected dialog style in the radio form. let dialogStyle; const dialogStyleOptions = document.getElementsByName('dialogStyleRadio'); if (dialogStyleOptions[0].checked) { dialogStyle = tableau.DialogStyle.Modal; } else if (dialogStyleOptions[1].checked) { dialogStyle = tableau.DialogStyle.Modeless; } else { dialogStyle = tableau.DialogStyle.Window; } /** * This is the API call that actually displays the popup extension to the user. * The only required parameter is the URL of the popup, * which must be the same domain, port, and scheme as the parent extension. * The dialog style (modal, modeless, or window) is selected by the user. * * The developer can optionally control the initial size of the extension by passing in * an object with height and width properties. The developer can also pass a string as the * 'initial' payload to the popup extension. This payload is made available immediately to * the popup extension. In this example, the value '5' is passed, which will serve as the * default interval of refresh. */ tableau.extensions.ui .displayDialogAsync(popupUrl, defaultIntervalInMin, { height: 500, width: 500, dialogStyle }) .then((closePayload) => { // The promise is resolved when the dialog has been expectedly closed, meaning that // the popup extension has called tableau.extensions.ui.closeDialog. $('#inactive').hide(); $('#active').show(); // The close payload is returned from the popup extension via the closeDialog method. $('#interval').text(closePayload); setupRefreshInterval(closePayload); }) .catch((error) => { // One expected error condition is when the popup is closed by the user (meaning the user // clicks the 'X' in the top right of the dialog). This can be checked for like so: switch (error.errorCode) { case tableau.ErrorCodes.DialogClosedByUser: console.log('Dialog was closed by user'); break; default: console.error(error.message); } }); } /** * This function sets up a JavaScript interval based on the time interval selected * by the user. This interval will refresh all selected datasources. */ function setupRefreshInterval (interval) { setInterval(function () { const dashboard = tableau.extensions.dashboardContent.dashboard; dashboard.worksheets.forEach(function (worksheet) { worksheet.getDataSourcesAsync().then(function (datasources) { datasources.forEach(function (datasource) { if (activeDatasourceIdList.indexOf(datasource.id) >= 0) { datasource.refreshAsync(); } }); }); }); }, interval * 60 * 1000); } /** * Opens the background color picker dialog. * Passes the current background color as the initial payload so the dialog * can pre-select it. The closePayload returned is the chosen hex color, * which is then applied to the extension body. */ function openColorDialog () { const popupUrl = new URL('uiNamespaceColorDialog.html', window.location.href).href; if (openDialogs.has(popupUrl)) { return; // Dialog is already open; don't open a second instance } openDialogs.add(popupUrl); // This checks for the selected dialog style in the radio form. // To send messages to the colorDialog, the colorDialog must be modeless. let dialogStyle; const dialogStyleOptions = document.getElementsByName('dialogStyleRadio'); if (dialogStyleOptions[0].checked) { dialogStyle = tableau.DialogStyle.Modal; } else if (dialogStyleOptions[1].checked) { dialogStyle = tableau.DialogStyle.Modeless; } else { dialogStyle = tableau.DialogStyle.Window; } $('#sendToColorDialog').prop('disabled', false); tableau.extensions.ui .displayDialogAsync(popupUrl, currentBgColor, { height: 500, width: 360, dialogStyle: dialogStyle }) .then((closePayload) => { openDialogs.delete(popupUrl); $('#sendToColorDialog').prop('disabled', true); currentBgColor = closePayload; $('body').css('background-color', currentBgColor); }) .catch((error) => { openDialogs.delete(popupUrl); $('#sendToColorDialog').prop('disabled', true); if (error.errorCode === tableau.ErrorCodes.DialogClosedByUser) { console.log('Color dialog was closed by user'); } else { console.error(error.message); } }); } async function sendToColorDialog() { let colorDialogUrl = new URL('uiNamespaceColorDialog.html', window.location.href).href; await tableau.extensions.ui.sendDialogMessageAsync(`Hello from the extension! ${new Date().toLocaleTimeString()}`, colorDialogUrl); } /** * Helper that is called to set state anytime the settings are changed. */ function updateExtensionBasedOnSettings (settings) { if (settings.selectedDatasources) { activeDatasourceIdList = JSON.parse(settings.selectedDatasources); $('#datasourceCount').text(activeDatasourceIdList.length); } } })(); ``` ##### uiNamespace.trex This example and `.trex` is for a dashboard extension, however the same principals apply to viz extensions. If you use this code, replace the `` with the location of where you are hosting your extension. The `` is required for the configuration context menu or the Format Extension button (for viz extensions). For additional popup windows or dialog boxes, no other changes are required to the `.trex` file. ```xml en_US UI Namespace Sample 1.10 http://localhost:8765/uinamespace.html iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAlhJREFUOI2Nkt9vy1EYh5/3bbsvRSySCZbIxI+ZCKsN2TKtSFyIrV2WuRCJuBiJWxfuxCVXbvwFgiEtposgLFJElnbU1SxIZIIRJDKTrdu+53Uhra4mce7Oe57Pcz7JOULFisViwZ+29LAzOSjQYDgz1ZcCvWuXV11MJpN+OS/lm6179teqH0yDqxPTCyKSA8DcDsyOmOprnCaeP7459pdgy969i0LTC3IO/RQMyoHcQN+3cnljW3dNIFC47qDaK3g7BwdTkwBaBELT4ZPOUVWgKl4ZBnjxJPUlMDnTDrp0pmr6RHFeEjjcUUXPDGeSEwDN0Xg8sivxMhJNjGzbHd8PkM3eHRfkrBM5NkcQaY2vUnTlrDIA0NoaX+KLXFFlowr14tvVpqb2MICzmQcKqxvbumv+NAhZGCCIPwEw6QWXKYRL/VUXO0+rAUJiPwAk5MIlgVfwPjjHLCL1APmHN94ZdqeYN+NW/mn6I4BvwQYchcLnwFhJMDiYmlRxAzjpKWZkYkUCcZ2I61wi37tLbYyjiN0fHk5Oz3nGSLSzBbNHCF35R7f6K1/hN9PRhek11FrymfQQQKB4+Gl05P2qNRtmETlXW7e+b2z01dfycGNbfFMAbqNyKp9Jp4rzOT8RYFs0njJkc2iqsCObvTsOsDWWqA5C1uFy+Uz/oXJeKwVT4h0RmPUXhi79vuC0Ku6yOffTK3g9lfxfDQAisY516sg5kfOCiJk7HoLt2cf9b/9LANAc7dznm98PagG1fUOZ9IP5uMB8Q4CPoyNvausapkTt3rNMuvdf3C/o6+czhtdwmwAAAABJRU5ErkJggg== UI Namespace Sample ``` * [HTML](#uinamespacecolordialoghtml) * [JavaScript](#uinamespacecolordialogjs) ##### uiNamespaceColorDialog.html ```html Background Color

Background Color

Select a background color for the extension window.


Preview:
Tableau 10 Colors + Neutrals:
Messages from the extension

``` ##### uiNamespaceColorDialog.js ```javascript 'use strict'; /** * Background Color Picker Dialog * * This dialog lets the user pick a background color for the parent extension * window from the Tableau 10 color palette plus two neutrals (12 total). * * Flow: * - initializeDialogAsync receives the current background color as openPayload. * - The matching color is pre-selected in the dropdown. * - Clicking Save calls closeDialog with the chosen hex value as the closePayload. * - The parent extension applies the color to its body background. */ (function () { // The 10 canonical Tableau 10 colors plus White and Light Gray (12 total). const TABLEAU_COLORS = [ { name: 'Blue', hex: '#4E79A7' }, { name: 'Orange', hex: '#F28E2B' }, { name: 'Red', hex: '#E15759' }, { name: 'Teal', hex: '#76B7B2' }, { name: 'Green', hex: '#59A14F' }, { name: 'Yellow', hex: '#EDC948' }, { name: 'Purple', hex: '#B07AA1' }, { name: 'Pink', hex: '#FF9DA7' }, { name: 'Brown', hex: '#9C755F' }, { name: 'Gray', hex: '#BAB0AC' }, { name: 'White', hex: '#FFFFFF' }, { name: 'Light Gray', hex: '#F0F0F0' }, ]; let selectedColor = TABLEAU_COLORS[10].hex; // default: White $(document).ready(function () { tableau.extensions.initializeDialogAsync().then(function (openPayload) { // openPayload is the current background color passed from the parent. if (openPayload && openPayload.startsWith('#')) { selectedColor = openPayload; } buildColorDropdown(); updatePreview(selectedColor); tableau.extensions.ui.addEventListener(tableau.TableauEventType.DialogMessageReceived, (event) =>{ console.log(`Message received: ${event.message}`); const p = $('

').text(event.message); $('#messageLog').append(p); }); $('#saveButton').click(function () { sendToParent(`Message From the Color Dialog at ${new Date().toLocaleTimeString()}`); tableau.extensions.ui.closeDialog(selectedColor); }); }); }); /** * Builds swatch + name + hex for one color (shared by trigger label and menu items). */ function buildColorRowContent ($container, color) { $('

', { class: 'color-swatch', css: { backgroundColor: color.hex } }).appendTo($container); $('', { class: 'color-name', text: color.name }).appendTo($container); $('', { class: 'color-hex', text: color.hex }).appendTo($container); } /** * Updates the dropdown button to show the current selection. */ function updateDropdownLabel (color) { const $label = $('#colorDropdownLabel'); $label.empty(); buildColorRowContent($label, color); } /** * Builds the Bootstrap dropdown menu and attaches selection handlers. */ function buildColorDropdown () { const $menu = $('#colorDropdownMenu'); $menu.empty(); let selectedMeta = TABLEAU_COLORS.find(function (c) { return c.hex.toUpperCase() === selectedColor.toUpperCase(); }); if (!selectedMeta) { selectedMeta = { name: 'Custom', hex: selectedColor }; } updateDropdownLabel(selectedMeta); TABLEAU_COLORS.forEach(function (color) { const isSelected = color.hex.toUpperCase() === selectedColor.toUpperCase(); const $li = $('
  • ', { role: 'presentation', class: isSelected ? 'active' : '' }); const $a = $('', { href: '#', class: 'color-dropdown-item', role: 'menuitem', tabindex: '-1' }); buildColorRowContent($a, color); $a.on('click', function (e) { e.preventDefault(); selectedColor = color.hex; $menu.find('li').removeClass('active'); $li.addClass('active'); updateDropdownLabel(color); updatePreview(selectedColor); $('#colorDropdownToggle').dropdown('toggle'); }); $li.append($a); $menu.append($li); }); } /** * Updates the preview box to reflect the currently selected color. */ function updatePreview (hex) { $('#colorPreview').css('background-color', hex); } async function sendToParent(message) { await tableau.extensions.ui.sendDialogMessageToParentAsync(message); } })(); ``` * [HTML](#uinamespacedialoghtml) * [JavaScript](#uinamespacedialogjs) ##### uiNamespaceDialog.html ```html Settings Sample

    Auto Data Source Refresh Extension

    This Extension refreshes the selected datasources at the selected interval.


    Apply to these datasources:
    Refresh interval in minutes:

    ``` ##### uiNamespaceDialog.js ```javascript 'use strict'; /** * UINamespace Sample Extension * * This is the popup extension portion of the UINamespace sample, please see * uiNamespace.js in addition to this for context. This extension is * responsible for collecting configuration settings from the user and communicating * that info back to the parent extension. * * This sample demonstrates two ways to do that: * 1) The suggested and most common method is to store the information * via the settings namespace. The parent can subscribe to notifications when * the settings are updated, and collect the new info accordingly. * 2) The popup extension can receive and send a string payload via the open * and close payloads of initializeDialogAsync and closeDialog methods. This is useful * for information that does not need to be persisted into settings. */ // Wrap everything in an anonymous function to avoid polluting the global namespace (function () { /** * This extension collects the IDs of each datasource the user is interested in * and stores this information in settings when the popup is closed. */ const datasourcesSettingsKey = 'selectedDatasources'; let selectedDatasources = []; $(document).ready(function () { // The only difference between an extension in a dashboard and an extension // running in a popup is that the popup extension must use the method // initializeDialogAsync instead of initializeAsync for initialization. // This has no affect on the development of the extension but is used internally. tableau.extensions.initializeDialogAsync().then(function (openPayload) { // The openPayload sent from the parent extension in this sample is the // default time interval for the refreshes. This could alternatively be stored // in settings, but is used in this sample to demonstrate open and close payloads. $('#interval').val(openPayload); $('#closeButton').click(closeDialog); const dashboard = tableau.extensions.dashboardContent.dashboard; const visibleDatasources = []; selectedDatasources = parseSettingsForActiveDataSources(); // Loop through datasources in this sheet and create a checkbox UI // element for each one. The existing settings are used to // determine whether a datasource is checked by default or not. dashboard.worksheets.forEach(function (worksheet) { worksheet.getDataSourcesAsync().then(function (datasources) { datasources.forEach(function (datasource) { const isActive = selectedDatasources.indexOf(datasource.id) >= 0; if (visibleDatasources.indexOf(datasource.id) < 0) { addDataSourceItemToUI(datasource, isActive); visibleDatasources.push(datasource.id); } }); }); }); }); }); /** * Helper that parses the settings from the settings namespace and * returns a list of IDs of the datasources that were previously * selected by the user. */ function parseSettingsForActiveDataSources () { let activeDatasourceIdList = []; const settings = tableau.extensions.settings.getAll(); if (settings.selectedDatasources) { activeDatasourceIdList = JSON.parse(settings.selectedDatasources); } return activeDatasourceIdList; } /** * Helper that updates the internal storage of datasource IDs * any time a datasource checkbox item is toggled. */ function updateDatasourceList (id) { const idIndex = selectedDatasources.indexOf(id); if (idIndex < 0) { selectedDatasources.push(id); } else { selectedDatasources.splice(idIndex, 1); } } /** * UI helper that adds a checkbox item to the UI for a datasource. */ function addDataSourceItemToUI (datasource, isActive) { const containerDiv = $('
    '); $('', { type: 'checkbox', id: datasource.id, value: datasource.name, checked: isActive, click: function () { updateDatasourceList(datasource.id); } }).appendTo(containerDiv); $('