Standards Docs
Tutorial — Build a CRM

Chapter 3 — views

Declare a list view and a detail view, mount them in Next.js.

The schema describes how to render lists and detail pages; the UI components mount these declaratively from those definitions.

Declare the list view

A list view defines the columns and layout for a records table. Create schema/views/contact.view.ts:

// @noverify
import { listView } from "@stndrds/schema";

export const CONTACT_LIST_VIEW = listView("default", "All contacts")
  .for("contact")
  .default()
  .tab("all", "All")
    .columns("firstName", "lastName", "email", "stage")
    .default()
  .build();

Key points:

  • .for("contact") binds the view to the object named "contact".
  • .default() on the view marks it as the fallback when no view is specified.
  • .tab() opens a tab configuration. Each tab has its own columns, layout, and filters.
  • .columns() lists attribute names in display order.

Register it at your schema entry point:

// @noverify
import { viewRegistry } from "@stndrds/schema";
import { CONTACT_LIST_VIEW } from "./views/contact.view";

viewRegistry.register(CONTACT_LIST_VIEW);

Declare the detail view

A detail view organises attributes into tabs and groups. Add to contact.view.ts:

// @noverify
import { detailView, group, listView } from "@stndrds/schema";

export const CONTACT_DETAIL_VIEW = detailView("detail", "Contact")
  .for("contact")
  .default()
  .tab("general", "General")
    .form(
      group("basics", "Basics")
        .fields("firstName", "lastName", "email", "phone"),
      group("status", "Status")
        .fields("kind", "stage"),
    )
  .build();

Key points:

  • .tab().form(...groups) creates a form tab. Pass one or more group() builders.
  • group(id, label).fields(...names) is the shortest way to add multiple fields. Use .field(name, options) when you need span or read-only overrides.
  • Register this view the same way: viewRegistry.register(CONTACT_DETAIL_VIEW).

View names must be kebab-case ("my-view", not "myView"). The builders throw at build time if the format is invalid.

Mount the list page

Create app/contacts/page.tsx:

// @noverify
"use client";
import { ViewAwareRecordsView } from "@stndrds/ui";

export default function ContactsPage() {
  return <ViewAwareRecordsView objectId="contact" />;
}

objectId accepts the object's name ("contact") or its database id. The component fetches the default list view automatically and renders the full records table with view switcher, filters, and sort controls.

Use <ViewAwareRecordsView /> when the user can switch views via a picker — it persists the selection in URL state.

Mount the detail page

Create app/contacts/[id]/page.tsx:

// @noverify
"use client";
import { ViewAwareRecordEditView } from "@stndrds/ui";

interface Props {
  params: { id: string };
}

export default function ContactPage({ params }: Props) {
  return (
    <ViewAwareRecordEditView
      objectId="contact"
      recordId={params.id}
      layout="page"
    />
  );
}

ViewAwareRecordEditView loads the default detail view for the object and renders the full tab layout. Pass onDelete, onNavigate, and onTabChange to wire navigation.

Next: Chapter 4 — permissions.

On this page