# The form (/docs/admin/collections/form)

---
title: The form
description: One .form() call lays out the create and edit screens. Sections, tabs and a sidebar arrange the fields, and any field can hide, lock or compute itself from a handler that runs on the server.
kind: reference
package: "@questpie/admin"
---

## What goes in it

`.form()` takes three keys. `fields` is required.

| Key       | Holds                                                         |
| --------- | ------------------------------------------------------------- |
| `fields`  | The main column, as a list of layout items                    |
| `sidebar` | A fixed column beside it, with its own list                   |
| `view`    | The renderer. `collection-form`, or `global-form` on a global |

```ts
.form(({ v, f }) =>
	v.collectionForm({
		sidebar: { position: "right", fields: [f.status, f.slug] },
		fields: [
			{ type: "section", label: "Content", fields: [f.title, f.body] },
		],
	}),
)
```

<Callout type="warn" title="A field you leave out is gone">
	`.form()` is the whole form. Nothing is appended for the fields you did not
	name, and no picker puts them back. Add a field to the collection later and it
	will not appear until you add it here too.
</Callout>

With no `.form()` at all, every field renders in the order you declared it.

## The four layout items

A `fields` array holds any mix of these.

| Item                          | Renders                              |
| ----------------------------- | ------------------------------------ |
| `f.title`                     | That field, plainly                  |
| `{ field: f.title, … }`       | That field, with per-instance config |
| `{ type: "section", … }`      | A titled group of items              |
| `{ type: "tabs", tabs: […] }` | Tabbed groups of items               |

Sections and tabs nest inside each other, to any depth.

## Sections

```ts
{
	type: "section",
	label: { en: "Details", sk: "Podrobnosti" },
	description: "Everything a reader sees first",
	wrapper: "collapsible",
	defaultCollapsed: true,
	layout: "grid",
	columns: 2,
	fields: [f.name, f.email],
}
```

| Key                | Default | Effect                                       |
| ------------------ | ------- | -------------------------------------------- |
| `label`            | none    | Heading above the group                      |
| `description`      | none    | One muted line under the heading             |
| `wrapper`          | `flat`  | `collapsible` puts it in an accordion        |
| `defaultCollapsed` | `false` | Starts a collapsible section closed          |
| `layout`           | `stack` | `inline` wraps in a row, `grid` uses columns |
| `columns`          | `2`     | Grid columns. 1 to 6                         |
| `gap`              | none    | Custom gap, in quarter rems                  |
| `hidden`           | `false` | Drops the whole section                      |
| `className`        | none    | Classes on the field container               |

A grid is responsive. `columns: 3` means one column on a narrow form and three
on a wide one. Nobody gets three squeezed inputs on a laptop.

## Tabs

```ts
{
	type: "tabs",
	tabs: [
		{ id: "basic", label: "Basic", fields: [f.name, f.email] },
		{
			id: "security",
			label: "Security",
			icon: { type: "icon", props: { name: "ph:lock" } },
			fields: [{ type: "section", label: "Access", fields: [f.banned] }],
		},
	],
}
```

Each tab needs `id` and `label`. `icon` and `hidden` are optional, and `fields`
takes the same four layout items as anywhere else.

`.form()` hands you `v` and `f`, not `c`. So an icon on a tab is written out by
hand, the way the example does it.

## The sidebar

```ts
sidebar: { position: "right", fields: [f.status, f.publishedAt] }
```

`position` is `right` by default, and `left` mirrors it. The sidebar sticks
while the main column scrolls, and it drops above the form on a narrow screen.

Sidebar fields are removed from the main column automatically. So listing a
field in both places is safe. You can also leave `fields: []`, and the main
column generates itself from whatever the sidebar did not take.

## Fields that react

Use the object form of a layout item, and four keys become handlers.

| Key        | Returns   | Does                            |
| ---------- | --------- | ------------------------------- |
| `hidden`   | `boolean` | Removes the field from the form |
| `readOnly` | `boolean` | Shows the value, refuses edits  |
| `disabled` | `boolean` | Greys the control out           |
| `compute`  | any value | Writes the value into the field |

Each takes a plain boolean, a function, or `{ handler, deps?, debounce? }`.
`compute` takes the last two only.

```ts
{
	type: "section",
	label: "Publishing",
	fields: [
		{ field: f.banReason, hidden: ({ data }) => !data.banned },
		{
			field: f.slug,
			compute: {
				handler: ({ data, prev }) =>
					data.slug && prev?.data?.title === data.title
						? undefined
						: slugify(String(data.title ?? "")),
				deps: ({ data }) => [data.title, data.slug],
				debounce: 300,
			},
		},
	],
}
```

Returning `undefined` from `compute` leaves the field alone. That is how the
example above stops overwriting a slug a person edited by hand.

A field with a `compute` stays editable. The handler writes the value when a
dependency changes, so anything typed in between survives until the next change.

### They run on the server

A handler never ships to the browser. The panel batches the tracked dependencies
to `/admin/reactive`. That route finds your handler on the builder and calls it.
So a handler may read the database, and it costs a round trip.

The context is `{ data, sibling, prev, ctx }`.

| Key       | Holds                                                    |
| --------- | -------------------------------------------------------- |
| `data`    | The whole form, as it stands now                         |
| `sibling` | The neighbouring values, inside an array or object field |
| `prev`    | The same two, as they stood before this change           |
| `ctx`     | `{ db, user, locale }`, plus `req` when there is one     |

The route requires `session.user.role === "admin"`, same as the rest of the
panel.

### Dependencies are inferred

You rarely write `deps`. The server runs the handler once against a recording
proxy while it builds the schema. Every value the handler reads becomes a
dependency, and that list is what the panel watches.

Inference only sees what the handler actually touched on that one call. A read
behind an `if` can be missed. Write `deps` yourself when that happens, either as
a string array or as a function returning the values. `debounce` is
milliseconds.

## Passing props to a field component

`props` forwards anything to the field's own component. Values can be static
JSON, or the same handler shapes as above.

```ts
{ field: f.author, props: { filter: ({ data }) => ({ team: data.team }) } }
{ field: f.counselorId, props: { filter: { role: "admin" } } }
```

A static value ships with the schema. A function stays on the server and
resolves through the same `/admin/reactive` route. That is how a relation
picker narrows itself against a value elsewhere on the form.

## Globals

A global takes the same `.form()`, with `v.globalForm()` in place of
`v.collectionForm()`. Sections, tabs, sidebars and reactive handlers all behave
identically. It has no `.list()`, because there is only ever one row.

## Where each topic lives

| Topic                                 | Page                                               |
| ------------------------------------- | -------------------------------------------------- |
| The other four methods                | [Collections and globals](/docs/admin/collections) |
| A live page beside this form          | [Preview](/docs/admin/collections/preview)         |
| Stacked content a person arranges     | [Blocks](/docs/schema/blocks)                      |
| Every `f.*` type and its control      | [Fields](/docs/schema/fields)                      |
| Writing a field component of your own | [Custom views](/docs/admin/custom-views)           |

## Next

**[Actions](/docs/admin/collections/actions)** covers the buttons around this
form, and how to add one that runs code of yours.
