Request data table

Request a data table from the app's own Data Connector using requestDataTable.

Running this example

To run this example locally:

  1. If you haven't already, create a new app in the Developer Portal(opens in a new tab or window). For more information, refer to our Quickstart guide.

  2. In your app's configuration on the Developer Portal(opens in a new tab or window), ensure the "Development URL" is set to http://localhost:8080.

  3. Clone the starter kit:

    git clone https://github.com/canva-sdks/canva-apps-sdk-starter-kit.git
    cd canva-apps-sdk-starter-kit
    SHELL
  4. Install dependencies:

    npm install
    SHELL
  5. Run the example:

    npm run start request_data_table
    SHELL
  6. Click the Preview URL link shown in the terminal to open the example in the Canva editor.

Example app source code

{
"$schema": "https://www.canva.dev/schemas/app/v1/manifest-schema.json",
"manifest_schema_version": 1,
"runtime": {
"permissions": [
{
"name": "canva:design:content:read",
"type": "mandatory"
},
{
"name": "canva:design:content:write",
"type": "mandatory"
}
]
},
"intent": {
"data_connector": {
"enrolled": true
},
"design_editor": {
"enrolled": true
}
}
}
JSON
// For usage information, see the README.md file.
import type {
ColumnConfig,
DataTable,
DataTableRow,
} from "@canva/intents/data";
export type TeamMember = {
name: string;
role: string;
location: string;
};
// A small hardcoded dataset. A real Data Connector would fetch data like this
// from an external API, using the request's `dataSourceRef` to identify what
// to fetch.
export const TEAM_MEMBERS: TeamMember[] = [
{ name: "Ana Silva", role: "Product Designer", location: "Sydney" },
{ name: "Wei Chen", role: "Engineer", location: "Singapore" },
{ name: "Priya Nair", role: "Engineer", location: "Bengaluru" },
{ name: "Tom Baker", role: "Marketing Lead", location: "London" },
{ name: "Sofia Reyes", role: "Customer Success", location: "Austin" },
];
const COLUMN_CONFIGS: ColumnConfig[] = [
{ name: "Name", type: "string" },
{ name: "Role", type: "string" },
{ name: "Location", type: "string" },
];
export function buildDataTable(members: TeamMember[]): DataTable {
const rows: DataTableRow[] = members.map((member) => ({
cells: [
{ type: "string", value: member.name },
{ type: "string", value: member.role },
{ type: "string", value: member.location },
],
}));
return { columnConfigs: COLUMN_CONFIGS, rows };
}
TYPESCRIPT
// For usage information, see the README.md file.
// This root index file contains the prepare function calls that initialize each intent.
// Each intent entrypoint is responsible for exporting the intent contract implementation.
import { prepareDataConnector } from "@canva/intents/data";
import { prepareDesignEditor } from "@canva/intents/design";
import dataConnector from "./intents/data_connector";
import designEditor from "./intents/design_editor";
prepareDataConnector(dataConnector);
prepareDesignEditor(designEditor);
TYPESCRIPT
// For usage information, see the README.md file.
import type {
DataConnectorIntent,
GetDataTableRequest,
GetDataTableResponse,
RenderSelectionUiRequest,
} from "@canva/intents/data";
import { AppUiProvider } from "@canva/app-ui-kit";
import "@canva/app-ui-kit/styles.css";
import { createRoot } from "react-dom/client";
import { buildDataTable, TEAM_MEMBERS } from "../../data";
import { SelectionUI } from "./selection_ui";
/**
* Returns the app's fixed team-directory dataset.
*
* A real Data Connector would use `request.dataSourceRef.source` to identify
* which data to fetch from an external source.
*/
async function getDataTable(
request: GetDataTableRequest,
): Promise<GetDataTableResponse> {
return {
status: "completed",
dataTable: buildDataTable(TEAM_MEMBERS),
metadata: {
description: "Team directory",
providerInfo: { name: "Team Directory" },
},
};
}
function renderSelectionUi(request: RenderSelectionUiRequest) {
const root = createRoot(document.getElementById("root") as Element);
root.render(
<AppUiProvider>
<SelectionUI {...request} />
</AppUiProvider>,
);
}
const dataConnector: DataConnectorIntent = { getDataTable, renderSelectionUi };
export default dataConnector;
TYPESCRIPT
// For usage information, see the README.md file.
import { Alert, Button, Rows, Text, Title } from "@canva/app-ui-kit";
import type { RenderSelectionUiRequest } from "@canva/intents/data";
import { useState } from "react";
import * as styles from "styles/components.css";
import { TEAM_MEMBERS } from "../../data";
const TEAM_DATA_SOURCE_REF = {
source: "team_members",
title: "Team members",
};
export function SelectionUI({ updateDataRef }: RenderSelectionUiRequest) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleLoadData = async () => {
setIsLoading(true);
setError(null);
const result = await updateDataRef(TEAM_DATA_SOURCE_REF);
if (result.status !== "completed") {
setError("Failed to load data.");
}
setIsLoading(false);
};
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Title>Team directory</Title>
<Text>
A fixed set of {TEAM_MEMBERS.length} team members. Loading them makes
this table available to `requestDataTable` in the design editor.
</Text>
<Button
variant="primary"
onClick={handleLoadData}
loading={isLoading}
stretch
>
Load team data
</Button>
{error && <Alert tone="critical">{error}</Alert>}
</Rows>
</div>
);
}
TYPESCRIPT
// For usage information, see the README.md file.
import { Alert, Button, Rows, Text, Title } from "@canva/app-ui-kit";
import type { DataTable } from "@canva/intents/data";
import { requestDataTable } from "@canva/intents/data";
import { useState } from "react";
import * as styles from "styles/components.css";
export function App() {
const [dataTable, setDataTable] = useState<DataTable | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleGetDataTable = async () => {
setIsLoading(true);
setError(null);
try {
// Opens the selection UI for this app's own Data Connector (see
// intents/data_connector/index.tsx) and resolves with the DataTable the
// user loads there.
const table = await requestDataTable({
withDataConnector: "self",
dataSelectionDisplay: "show",
});
setDataTable(table);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to get data.");
} finally {
setIsLoading(false);
}
};
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Title>Request data table</Title>
<Text>
Calls requestDataTable to open this app's own Data Connector and fetch
the table the user loads.
</Text>
<Button
variant="primary"
onClick={handleGetDataTable}
loading={isLoading}
stretch
>
Get data
</Button>
{error && <Alert tone="critical">{error}</Alert>}
{dataTable && <DataTablePreview dataTable={dataTable} />}
</Rows>
</div>
);
}
function DataTablePreview({ dataTable }: { dataTable: DataTable }) {
const columnNames = dataTable.columnConfigs?.map((c) => c.name ?? "") ?? [];
return (
<Rows spacing="1u">
<Text size="small" tone="secondary">
{dataTable.rows.length} row{dataTable.rows.length !== 1 ? "s" : ""}{" "}
returned
</Text>
<div style={{ overflowX: "auto" }}>
<table style={{ borderCollapse: "collapse", width: "100%" }}>
{columnNames.length > 0 && (
<thead>
<tr>
{columnNames.map((name, i) => (
<th
key={i}
style={{
textAlign: "left",
padding: "4px 8px",
borderBottom: "1px solid var(--ui-kit-border-standard)",
}}
>
{name}
</th>
))}
</tr>
</thead>
)}
<tbody>
{dataTable.rows.map((row, rowIndex) => (
<tr key={rowIndex}>
{row.cells.map((cell, cellIndex) => (
<td
key={cellIndex}
style={{
padding: "4px 8px",
borderBottom: "1px solid var(--ui-kit-border-standard)",
}}
>
{cell.type === "string" ? (cell.value ?? "") : ""}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</Rows>
);
}
TYPESCRIPT
// For usage information, see the README.md file.
import "@canva/app-ui-kit/styles.css";
import type { DesignEditorIntent } from "@canva/intents/design";
import { AppUiProvider } from "@canva/app-ui-kit";
import { createRoot } from "react-dom/client";
import { App } from "./app";
async function render() {
const root = createRoot(document.getElementById("root") as Element);
root.render(
<AppUiProvider>
<App />
</AppUiProvider>,
);
}
const designEditor: DesignEditorIntent = { render };
export default designEditor;
// Hot Module Replacement for development (automatically reloads the app when changes are made)
if (module.hot) {
module.hot.accept("./app", render);
}
TYPESCRIPT
# Request data table
This example demonstrates how to use `requestDataTable` from `@canva/intents/data` to
fetch a `DataTable` from an app's own Data Connector directly from the design editor
intent, without the user having to leave the design editor experience.
The app implements two intents:
- **Data Connector** (`intents/data_connector`) — exposes a small, fixed team-directory
dataset as a Data Connector data source.
- **Design Editor** (`intents/design_editor`) — a single "Get data" button that calls
`requestDataTable({ withDataConnector: "self", dataSelectionDisplay: "show" })`, which
opens the Data Connector's selection UI and resolves with the `DataTable` the user
loads, then renders it as a simple table.
For API reference docs and instructions on running this example, see:
<https://www.canva.dev/docs/apps/examples/request-data-table/>.
Related examples: see `intents/data_connector_intent` for a fuller Data Connector
implementation (search, filtering, multiple data sources), and
`intents/implement_multiple_intents` for the general pattern of combining multiple
intents in one app.
NOTE: This example differs from what is expected for public apps to pass a Canva review:
- **Preview/beta API**: `requestDataTable` is a `@beta` API. Check the latest API
status in the docs before relying on it in a production app.
- **Static/hardcoded data**: The Data Connector always returns the same fixed dataset.
Production apps should use `request.dataSourceRef.source` to identify and fetch the
actual data requested from an external source.
- **Localization**: Text content is hardcoded in English. Production apps require
proper internationalization using the `@canva/app-i18n-kit` package for multi-language
support.
- **Error handling**: Error handling is simplified for demonstration. Production apps
must implement comprehensive error handling with clear user feedback and graceful
failure modes.
MARKDOWN

API reference

Need help?