Bounded GridCore projections
View as MarkdownUse GridCore when the application owns temporary, composite, calculated, or browser-owned rows. The caller must bound every source and refuse to present a complete-looking result when a page is truncated.
Build the calculated rows
Section titled “Build the calculated rows”Suppose a screen owns two generated-table reads capped at 100 rows:
import { useMemo } from "react";import { CELL_GRID_WITH_ACTIVE_ROW, GridLevel, GridRuntimeProvider, createGridRuntime, inMemoryGridDataSource, useGridRuntimeEffect, type GridSchema, type TreeNode,} from "@sapporta/grid";import { number, percentage, text } from "@sapporta/grid/column-preset";
type Project = { id: string; name: string };type Task = { id: string; projectId: string; completed: boolean };type BoundedPage<T> = { data: readonly T[]; meta: { total: number; limit: number };};
const ROW_CAP = 100;declare const projectsPage: BoundedPage<Project>;declare const tasksPage: BoundedPage<Task>;
const schema = { rootLevel: "projects", levels: { projects: { name: "projects", rowHeaderColumn: "none", childLevels: [], options: {}, columns: [ text({ id: "name", name: "Project", edit: "none" }), number({ id: "tasks", name: "Tasks", edit: "none" }), number({ id: "completed", name: "Completed", edit: "none" }), percentage({ id: "completion", name: "Completion", edit: "none" }), ], }, },} satisfies GridSchema;A date or timestamp column takes no zone option. The zone is published
once, before the first grid renders, through setDisplayTimeZone from
@sapporta/grid/column-preset. A screen inside a generated application already
has it: the boot sequence publishes the active workspace’s zone before any route
renders, and appTimeZone() from @sapporta/frontend/platform reads it back. An
application driving @sapporta/grid on its own publishes it itself.
Check completeness before calculating:
export function ProjectProgressGrid() { const incomplete = projectsPage.meta.total > projectsPage.data.length || tasksPage.meta.total > tasksPage.data.length;
if (incomplete) { return ( <p role="status"> This summary is incomplete because a generated read exceeded the{" "} {ROW_CAP}-row cap. Use the scoped report for complete totals. </p> ); }
return <CompleteProjectProgressGrid />;}Pre-index the task rows and use stable domain identity for rowKey:
function CompleteProjectProgressGrid() { const tree = useMemo(() => { const countsByProject = new Map< string, { total: number; completed: number } >();
for (const task of tasksPage.data) { const counts = countsByProject.get(task.projectId) ?? { total: 0, completed: 0, }; counts.total += 1; if (task.completed) counts.completed += 1; countsByProject.set(task.projectId, counts); }
return projectsPage.data.map((project) => { const counts = countsByProject.get(project.id) ?? { total: 0, completed: 0, }; return { rowKey: project.id, levelName: "projects", columns: { name: project.name, tasks: counts.total, completed: counts.completed, completion: counts.total === 0 ? 0 : counts.completed / counts.total, }, }; }) satisfies TreeNode[]; }, []);
const runtime = useGridRuntimeEffect( () => createGridRuntime({ schema, interaction: CELL_GRID_WITH_ACTIVE_ROW, dataSource: inMemoryGridDataSource({ schema, tree, levels: { projects: { readonly: true, sortMode: "client", filterMode: "none", paginationMode: "none", }, }, }), }), [tree], );
if (!runtime) return null; return ( <GridRuntimeProvider runtime={runtime}> <GridLevel path={runtime.root.path} /> </GridRuntimeProvider> );}The application owns the rows and calculation; Grid owns rendering and
interaction. A ratio such as 0.4 renders as 40%. A bounded browser
projection is suitable only for small screen-local data. Reusable or complete
aggregates belong behind a scoped report route; moving the same loop to the
server changes placement, not complexity.