Skip to Content

Tutorial 3: Building UI with Components

Buildpad’s UI component library provides two high-level components that eliminate most of the boilerplate for CRUD interfaces:

  • CollectionList — A paginated, filterable, sortable table that fetches data directly from your DaaS collection.
  • CollectionForm — A full CRUD form that auto-renders the correct input for every field type in your collection.

In this tutorial you’ll walk through the /content/tasks list page and the /content/tasks/[id] detail form that were already generated by Tutorial 2, understand how they work, and learn how to customise them.

By the end you’ll have:

  • A working /content/tasks route showing all tasks in a filterable, sortable table
  • A /content/tasks/[id] route with a full create/edit/delete form
  • Session-aware UI using the useAuth hook

Prerequisites

Verify Your Components Are Installed

CollectionList and CollectionForm are already in your project — the /create-project bootstrap installs all 50+ Buildpad components up front. You’ll find them in components/ui/.

The copy & own model means components are real source files in your project — not a black-box npm package. You can read, modify, and extend them freely.

Confirm by checking buildpad.json at your project root, which lists every installed component under installedComponents. You can also run:

npx @buildpad/cli@latest status

Explore the Tasks List Page

When Tutorial 2 ran /create-collection for the tasks collection, it already generated app/(authenticated)/content/tasks/page.tsx. Open it now:

app/(authenticated)/content/tasks/page.tsx
"use client"; import { useRouter } from "next/navigation"; import { CollectionList } from "@/components/ui"; export default function TasksListPage() { const router = useRouter(); return ( <CollectionList collection="tasks" enableSearch enableFilter enableSelection enableCreate enableDelete enableSort enableResize enableReorder enableHeaderMenu archiveField="status" archiveValue="cancelled" unarchiveValue="todo" limit={25} onCreate={() => router.push("/content/tasks/+")} onItemClick={(item) => router.push(`/content/tasks/${item.id}`)} /> ); }

Open http://localhost:3000/content/tasks — you should see your seed tasks rendered in the table with sorting, search, and filter already working.

CollectionList showing tasks with status, priority, and due date columns

Explore the Tasks Detail Page

The /create-collection skill also generated the detail form at app/(authenticated)/content/tasks/[id]/page.tsx:

app/(authenticated)/content/tasks/[id]/page.tsx
"use client"; import { use } from "react"; import { useRouter } from "next/navigation"; import { Group, Button } from "@mantine/core"; import { IconArrowLeft } from "@tabler/icons-react"; import { CollectionForm } from "@/components/ui"; import { isNewItem } from "@/lib/buildpad/utils"; export default function TaskDetailPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = use(params); const router = useRouter(); const handleSuccess = (data?: Record<string, unknown>) => { if (isNewItem(id) && data?.id) { router.push(`/content/tasks/${data.id}`); } }; return ( <div> <Group mb="md"> <Button variant="subtle" leftSection={<IconArrowLeft size={16} />} onClick={() => router.push("/content/tasks")} > Tasks </Button> </Group> <CollectionForm collection="tasks" id={isNewItem(id) ? undefined : id} mode={isNewItem(id) ? "create" : "edit"} onSuccess={handleSuccess} /> </div> ); }
  • Navigate to /content/tasks/+ to create a new task — the + sentinel is handled by isNewItem().
  • Navigate to /content/tasks/<id> to edit an existing task.
  • CollectionForm auto-renders the correct input for each field based on the interface type you defined in Tutorial 2.

CollectionForm showing the task edit form with all field types

Add Session Awareness with useAuth

The generated page.tsx gives every authenticated user the same experience. The useAuth hook is already installed at lib/buildpad/hooks/useAuth.ts — you just need to wire it in. A common pattern is hiding the create button from non-admin users.

Open app/(authenticated)/content/tasks/page.tsx and update it:

app/(authenticated)/content/tasks/page.tsx
"use client"; import { useRouter } from "next/navigation"; import { CollectionList } from "@/components/ui"; import { useAuth } from "@/lib/buildpad/hooks/useAuth"; export default function TasksListPage() { const { isAdmin } = useAuth(); const router = useRouter(); return ( <CollectionList collection="tasks" enableSearch enableFilter enableSelection enableCreate enableDelete enableSort enableResize enableReorder enableHeaderMenu archiveField="status" archiveValue="cancelled" unarchiveValue="todo" limit={25} onCreate={isAdmin ? () => router.push("/content/tasks/+") : undefined} onItemClick={(item) => router.push(`/content/tasks/${item.id}`)} /> ); }

Passing onCreate={undefined} hides the create button entirely — CollectionList also checks collection permissions server-side, so non-admin users won’t see it regardless.

Verify the Sidebar Navigation

The Tasks link was already added to the sidebar when Tutorial 2 ran /create-collection. Open components/AppNavShell.tsx and confirm the NAV_ITEMS array includes a Tasks entry:

components/AppNavShell.tsx
const NAV_ITEMS = [ // ...other items { label: "Tasks", href: "/content/tasks", icon: IconChecklist }, ];

Navigate to http://localhost:3000/content/tasks — the Tasks link in the sidebar should highlight automatically because Next.js Link matches the active path.

If the link isn’t highlighted, check that AppNavShell.tsx uses usePathname() to compare the current path against each item’s href.

Troubleshooting

Warning: Function components cannot be given refs (forwardRef)

If you see this warning in the browser console after running pnpm dev, it’s caused by a Mantine package version mismatch. The Buildpad stack requires all Mantine packages to be on v8, but @mantine/dates and @mantine/tiptap may have been installed at v9:

Show warning output

Warning: Function components cannot be given refs. Attempts to access this ref will fail. at DatePickerInput

Fix it by pinning both packages to v8:

Show fix command

pnpm add @mantine/dates@8 @mantine/tiptap@8

Then restart the dev server.

Other common issues

SymptomCauseFix
Table shows “No records” despite seed dataCollection name mismatchCheck the collection prop matches the exact name in your DaaS backend
403 error on data loadAuth token not forwardedEnsure you’re inside the (authenticated) route group
Create button missingenableCreate not setAdd enableCreate to CollectionList
useAuth returns null userNot inside AuthProviderConfirm (authenticated)/layout.tsx wraps children in the provider

What’s Next

You have working list and form pages. In the next tutorial you’ll lock down who can do what using role-based access control.

Continue to Tutorial 4: Role-Based Access →

Last updated on