Skip to main content
MUI Form & Table

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 MaeTable API documented,

  • but App Engine adds script-oriented conveniences:

    • TableInstance for 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:

GlobalPurpose
TableInstanceImperatively mounts a table into a DOM element.
QuickSearchActionToolbar placeholder that renders the built-in quick search control.
FilterActionToolbar placeholder that renders the built-in filter UI.
SettingsActionToolbar placeholder that renders the built-in settings UI.
ReactReact 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:

  1. The wrapper creates an internal <div> inside the parent element.
  2. It mounts MvcTable into that container.
  3. It returns a Proxy.
  4. 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 MaeTable API 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, and refetch(),
  • query.key is the React Query key (ReadonlyArray<unknown>) used as queryKey,
  • query.fn receives 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 AppEngineQueryKey share 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:

  • id
  • keyField
  • columns
  • toolbar
  • selectionMode
  • selectionLimit
  • sort
  • onSelectionChange
  • onRowClick
  • isSelectable
  • slots
  • hotKeys
  • ui
  • sx
  • storeSettingsInLocalStorage

Less commonly used but still part of the typed App Engine config:

  • error (optional) boolean flag that toggles a visual error state on the table container. When set to true, 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-level isError state, the React Query error status returned by type: 'fetching', or any of the toolbar/selection behavior). Set it back to false (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:

  • id should be stable. It is used by the runtime for persisted UI settings such as size, visible columns, and page size.
  • selectionMode defaults to 'multiple' if you do not provide it.
  • selectionLimit (optional) caps the maximum number of rows that can be selected simultaneously.
  • sx is the standard MUI system prop forwarded to the outer table wrapper. Use it for per-instance style overrides via nested selectors (for example sx={{ '& .MuiTableCell-root': { paddingBlock: 0.5 } }}). The unrelated ui prop still controls the container's paper elevation, padding, and background color and can be combined with sx.
  • storeSettingsInLocalStorage defaults to true.
  • 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.actionsLeft or toolbar.actionsRight.

Toolbar actions in scripts

Toolbar configuration in App Engine scripts follows the normal toolbar type, but scripts also get three predefined helper placeholders:

  • QuickSearchAction
  • FilterAction
  • SettingsAction

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,
  • transform for 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

    • selection
    • select(...)
    • deSelect(...)
    • setSelection(...)
    • selectAll()
    • deSelectAll()
  • filtering and search

    • quickSearch
    • setQuickSearch(...)
    • filter
    • setFilter(...)
    • resetFilter()
  • sorting

    • sort
    • setSort(...)
  • pagination and view state

    • pagination.page
    • pagination.setPage(...)
    • pagination.rowsPerPage
    • pagination.setRowsPerPage(...)
    • size.setSize(...)
    • visibility.setVisibleColumns(...)
  • data helpers

    • addItem(...)

    • removeItem(...)

    • editItem(...)

    • replaceAll(...)

      • getDataByKeyField(...)
      • getRowKeyValue(...)

Extra instance members by mode

ModeExtra 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

  1. 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.

  2. Use TableInstance.create(...), not new TableInstance(...).

  3. Standalone tables do not automatically add toolbar controls. Add QuickSearchAction, FilterAction, and SettingsAction yourself when needed.

  4. type: 'data' and type: 'fetching' behave differently for inline persistence.

    • fetching integrates with the query-backed mutation flow.
    • data is ideal for manual, local, or externally managed row synchronization.
  5. destroy() exists at runtime and should be called when the mounted table is no longer needed.

  6. id should stay stable if you want persisted UI settings such as visible columns, page size, and density to behave predictably.

Updated on Aug 11, 2026