Table configuration in AppEngine
Mount tables with TableInstance.create(), pick between data and fetching mode, and wire up the built-in toolbar actions.
POSTED: | UPDATED: | by Stefan Schüttenkopf
Overview
The App Engine runtime exposes a script-friendly wrapper around the MVC table stack.
At runtime:
-
you mount tables with
TableInstance.create(parentElement, config), -
the wrapper renders
MvcTable, -
the table still uses the same column model and
MaeTableAPI documented, -
but App Engine adds script-oriented conveniences:
TableInstancefor imperative mounting,- globally available toolbar placeholders for quick search, filter, and settings,
- automatic
QueryClientProvider, CLDR loading, Studio theme setup, and internal table settings state.
This document is the App Engine wrapper guide. For the detailed low-level table API, column internals, and ref surface, keep Table configuration in code as the core reference.
Runtime globals available to scripts
The App Engine transpiler binds the following globals that matter for table authoring:
| Global | Purpose |
|---|---|
TableInstance | Imperatively mounts a table into a DOM element. |
QuickSearchAction | Toolbar placeholder that renders the built-in quick search control. |
FilterAction | Toolbar placeholder that renders the built-in filter UI. |
SettingsAction | Toolbar placeholder that renders the built-in settings UI. |
React | React is not available. |
Notes:
- In script authoring, the toolbar helper is called
SettingsAction(singular). - MUI does not work in App Engine scripts.
Lifecycle of TableInstance
The supported entry point is the static create() method.
type UserRow = {
id: string;
name: string;
role: 'admin' | 'editor';
};
const host = document.getElementById('table-root') as HTMLElement;
const table = TableInstance.create<UserRow, 'id'>(host, {
type: 'data',
id: 'users',
keyField: 'id',
columns: [
{ field: 'name', headerName: 'Name' },
{ field: 'role', headerName: 'Role' }
],
data: {
data: [
{ id: 'u-1', name: 'Alice', role: 'admin' },
{ id: 'u-2', name: 'Bob', role: 'editor' }
]
}
});
What happens internally:
- The wrapper creates an internal
<div>inside the parent element. - It mounts
MvcTableinto that container. - It returns a
Proxy. - Access to methods and properties is forwarded to the mounted table instance after the ref is ready.
Like the form runtime, the table proxy target also exposes:
table.destroy();
destroy() unmounts the React root and removes the generated container. It is part of the public surface returned by create().
When a script creates multiple forms and tables before registering a sidebar panel, create them first and then register the panel:
const filtersForm = FormInstance.create<FilterValues>(filtersRoot, filtersConfig);
const summaryForm = FormInstance.create<SummaryValues>(summaryRoot, summaryConfig);
const table = TableInstance.create<UserRow, 'id'>(tableRoot, tableConfig);
window.map.stores.sidebarMenu.addMenuItem(
{
id: 'users-panel',
icon: { tooltip: 'Users' },
panel: {
domContent: panelRoot,
title: 'Users'
}
},
true
);
Choosing the table mode
TableInstance.create() accepts the App Engine table config union, which supports two table modes.
type: 'data'
Use this when you already have the row array in memory and want a table backed by that supplied data.
{
type: 'data',
data: {
data: rows,
initialSelection: ['u-2'],
isLoading: false,
isError: false,
isFetched: true,
isFetching: false,
isRefetching: false
}
}
Characteristics:
- easiest mode for local arrays,
- good for script-managed data,
- returned instance includes the normal
MaeTableAPI plus extra loading-state setters, - best choice when your script wants to push rows in with
addItem()/replaceAll()or keep the data lifecycle manual.
type: 'fetching'
Use this when the table should fetch its own rows through the wrapper's React Query integration.
{
type: 'fetching',
query: {
key: ['users'],
fn: async ({ signal }) => {
return await loadUsers(signal);
},
initialSelection: ['u-1'],
enabled: true
}
}
Characteristics:
- wrapper manages React Query state,
- useful for server-backed data grids,
- returned instance exposes the React Query result surface such as
data,error,isLoading,isFetching,status,fetchStatus, andrefetch(), query.keyis the React Query key (ReadonlyArray<unknown>) used asqueryKey,query.fnreceives React Query context (queryKey,signal) so requests can be cancelled cleanly,- loading/error/fetching state comes from the query result rather than manual setter calls.
query.key and AppEngineQueryKey
The query.key field is typed as AppEngineQueryKey, which is defined as:
type AppEngineQueryKey = ReadonlyArray<unknown>;
This mirrors the React Query QueryKey contract and identifies the cached query for this table:
- It must be an array. Even single-scope keys are written as
['users'], not'users'. - Entries can be any serializable value (strings, numbers, booleans, plain objects, nested arrays, etc.). React Query hashes the key deterministically to look up cached data.
- Two tables that share the same
AppEngineQueryKeyshare the same cache entry. Use distinct keys when you want independent fetch state, and reuse the same key when you intentionally want tables to stay in sync. - Because the type is
ReadonlyArray<unknown>, the runtime does not enforce a particular shape, but keeping keys stable and structured (for example['domain', 'entity', params]) makes cache invalidation from elsewhere in the script predictable.
Common configuration shape
Both App Engine table modes share a core set of configuration fields.
Most commonly used properties are:
idkeyFieldcolumnstoolbarselectionModeselectionLimitsortonSelectionChangeonRowClickisSelectableslotshotKeysuisxstoreSettingsInLocalStorage
Less commonly used but still part of the typed App Engine config:
error(optional)booleanflag that toggles a visual error state on the table container. When set totrue, the wrapper draws a 1px border in the theme's error color (theme.palette.error.main) around the table and adds a small amount of padding, making it easy to highlight the table when an external condition is invalid. It is purely presentational (it does not affect the data, the row-levelisErrorstate, the React Query error status returned bytype: 'fetching', or any of the toolbar/selection behavior). Set it back tofalse(or leave it unset) to clear the highlight.
The meaning of the other properties is the same as in the normal table README.
Practical reminders for App Engine scripts:
idshould be stable. It is used by the runtime for persisted UI settings such as size, visible columns, and page size.selectionModedefaults to'multiple'if you do not provide it.selectionLimit(optional) caps the maximum number of rows that can be selected simultaneously.sxis the standard MUI system prop forwarded to the outer table wrapper. Use it for per-instance style overrides via nested selectors (for examplesx={{ '& .MuiTableCell-root': { paddingBlock: 0.5 } }}). The unrelateduiprop still controls the container's paper elevation, padding, and background color and can be combined withsx.storeSettingsInLocalStoragedefaults totrue.- Standalone App Engine tables do not automatically add toolbar controls; if you want quick search, filters, or settings, you must place the toolbar placeholders in
toolbar.actionsLeftortoolbar.actionsRight.
Toolbar actions in scripts
Toolbar configuration in App Engine scripts follows the normal toolbar type, but scripts also get three predefined helper placeholders:
QuickSearchActionFilterActionSettingsAction
Example:
toolbar: {
title: 'Users',
actionsLeft: [QuickSearchAction],
actionsRight: [FilterAction, SettingsAction]
}
You can also mix built-in actions with your own JSX actions: (not available in 17.2)
import { Button } from '@mui/material';
toolbar: {
title: 'Users',
actionsLeft: [QuickSearchAction],
actionsRight: [
FilterAction,
SettingsAction,
{
key: 'refresh',
element: <Button onClick={() => table.replaceAll([])}>Clear</Button>
}
]
}
Columns and rendering
The App Engine wrapper preserves the display-oriented table column model from the normal table library.
That means App Engine scripts can use:
- simple text/number/date/boolean/single-select columns,
transformfor sorting/filtering/rendering source values,valueFormatter,renderCell(not available in 17.2),- action columns.
App Engine intentionally narrows the authoring surface for editing. The config type omits:
- column
editable - column
validation - column
getEditStartValue - table
modifications
If you want TypeScript to catch those fields, prefer passing the object literal directly to TableInstance.create(...) or using satisfies AppEngineTableConfig<...> instead of casting through any.
Instance API available after create()
App Engine tables expose the public App Engine table ref surface, which is based on MaeTable<T, K> but intentionally omits newRow, updateRow, and focus.
That includes:
-
selection state and commands
selectionselect(...)deSelect(...)setSelection(...)selectAll()deSelectAll()
-
filtering and search
quickSearchsetQuickSearch(...)filtersetFilter(...)resetFilter()
-
sorting
sortsetSort(...)
-
pagination and view state
pagination.pagepagination.setPage(...)pagination.rowsPerPagepagination.setRowsPerPage(...)size.setSize(...)visibility.setVisibleColumns(...)
-
data helpers
-
addItem(...) -
removeItem(...) -
editItem(...) -
replaceAll(...)getDataByKeyField(...)getRowKeyValue(...)
-
Extra instance members by mode
| Mode | Extra runtime members |
|---|---|
type: 'data' | setIsLoading, setIsError, setIsFetching, setIsRefetching |
type: 'fetching' | React Query result members such as data, error, isLoading, isFetching, status, fetchStatus, refetch() |
Example:
table.setQuickSearch('Alice');
table.resetFilter();
table.selectAll();
table.pagination.setPage(0);
table.size.setSize('medium');
const row = table.getDataByKeyField('u-1');
End-to-end examples
1. Basic in-memory table
type UserRow = {
id: string;
name: string;
role: 'admin' | 'editor';
active: boolean;
};
const rows: UserRow[] = [
{ id: 'u-1', name: 'Alice', role: 'admin', active: true },
{ id: 'u-2', name: 'Bob', role: 'editor', active: false }
];
const table = TableInstance.create<UserRow, 'id'>(host, {
type: 'data',
id: 'users',
keyField: 'id',
selectionMode: 'multiple',
columns: [
{ field: 'name', headerName: 'Name' },
{ field: 'role', headerName: 'Role', type: 'singleSelect', valueOptions: ['admin', 'editor'] },
{ field: 'active', headerName: 'Active', type: 'boolean' }
],
data: {
data: rows
},
toolbar: {
title: 'Users',
actionsLeft: [QuickSearchAction],
actionsRight: [FilterAction, SettingsAction]
}
});
2. Query-backed table
type UserRow = {
id: string;
name: string;
role: string;
};
const table = TableInstance.create<UserRow, 'id'>(host, {
type: 'fetching',
id: 'users',
keyField: 'id',
columns: [
{ field: 'name', headerName: 'Name' },
{ field: 'role', headerName: 'Role' }
],
query: {
key: ['users'],
fn: async ({ signal }) => {
return await loadUsers(signal);
},
enabled: true
},
toolbar: {
title: 'Users',
actionsLeft: [QuickSearchAction],
actionsRight: [FilterAction, SettingsAction]
}
});
3. Programmatic table control
table.setQuickSearch('Alice');
table.select('u-1');
table.pagination.setRowsPerPage(25);
table.visibility.setVisibleColumns({ name: true, role: false, active: true });
await table.addItem({
id: 'u-3',
name: 'Carol',
role: 'editor'
});
Important constraints and gotchas
-
create()guarantees synchronous mount. For tables, the imperative API usually becomes available immediately even if the UI is currently showing loading, paused, or error content. -
Use
TableInstance.create(...), notnew TableInstance(...). -
Standalone tables do not automatically add toolbar controls. Add
QuickSearchAction,FilterAction, andSettingsActionyourself when needed. -
type: 'data'andtype: 'fetching'behave differently for inline persistence.fetchingintegrates with the query-backed mutation flow.datais ideal for manual, local, or externally managed row synchronization.
-
destroy()exists at runtime and should be called when the mounted table is no longer needed. -
idshould stay stable if you want persisted UI settings such as visible columns, page size, and density to behave predictably.