QUESTPIE
SchemaFields

Textarea field

`f.textarea()` is the multi-line string. There is no argument to pass, the `text` column is unbounded whatever you write, and the field filters with the full string operator set.

View markdown
SurfaceWhat f.textarea() produces
Columntext, no length cap
Valuestring, or string | null without .required()
Schemaz.string(), no cap
OperatorsstringOps, fourteen of them
Form controlTextareaField, a plain textarea
List cellTextCell, truncated, off the default columns

Every call produces that same column. No method on the type reopens it, and only .array() and the .drizzle() escape hatch replace it.

src/questpie/server/collections/posts.ts
import { collection } from "#questpie/factories";

export const posts = collection("posts").fields(({ f }) => ({
	title: f.text(255).required(),
	excerpt: f.textarea(), // text, selects as string | null
	body: f.textarea().required().max(20_000), // text NOT NULL, capped in the schema
}));

Methods

.required(), .default(), .label(), .localized() and the rest of the base chain work here as on any field. On top of those the type adds two.

MethodEffect
.min(n)Adds z.string().min(n), a character count
.max(n)Adds z.string().max(n), a character count

Both write a validation bound and nothing else. The column stays unbounded text either way, so a .max(n) you later raise needs no migration.

`.pattern()`, `.trim()`, `.lowercase()` and `.uppercase()` are not here

Only min and max come with the type. The other four belong to f.text(), and calling one here fails to compile. Normalize with .zod() or a beforeValidate hook instead.

Filtering

Textarea carries stringOps, the same fourteen operators as f.text().

OperatorOperandMatches
eq / nestringEqual, not equal
notstring, or nullNot equal, or IS NOT NULL when given null
in / notInstring[]In the list, not in the list
like / notLikestringLIKE, case-sensitive, you write the %
ilike / notIlikestringThe same, case-insensitive
containsstringSubstring, the % are added for you
startsWithstringPrefix
endsWithstringSuffix
isNull / isNotNullbooleanPass false and each inverts
const { docs } = await app.collections.posts.find({
	where: { excerpt: { contains: "release" } },
});

`contains` is a `LIKE` scan

It builds %value% around a bound parameter and scans. Fine for filtering a list, wrong for ranked search over prose. Use .searchable() and the search adapter for that.

In the admin

The form control is TextareaField, a textarea at three rows. It sets resize-none, so a reader cannot drag it taller, and it carries a minimum height that a smaller rows will not go under. Raising rows is the one lever.

What crosses to the browser:

  • .max(n) becomes the textarea's maxLength attribute, so typing stops there. Without it there is no attribute and no stop.
  • The admin rebuilds z.string() from minLength and maxLength, so the form checks before it posts.
  • Nothing else. The type has no pattern or format to send.

.admin() comes from @questpie/admin and takes five keys on this type, on top of the shared ones every field has.

KeyEffect
placeholderPlaceholder text, resolved through the locale
rowsThe rows attribute, default 3
autoResizeNothing reads it
richTextNothing reads it
showCounterNothing reads it

The last three are declared on TextareaFieldAdminMeta and nothing under packages/ reads them. autoResize even reaches the control, which drops it. Treat all three as absent.

The list column

Textarea sits on the admin's heavy-field list, beside json, object, blocks and richText, so it is never one of the default visible columns. The column picker still offers it, and naming it in .list() pins it. When it does render, TextCell truncates at 300 pixels and puts the whole value in the cell's title.

.list(({ v, f }) => v.collectionTable({ columns: [f.excerpt] }))

`showInList` will not bring it back

The key type-checks on every field's .admin(), and nothing under packages/ reads it. Name the field in the .list() callback's columns instead.

Lists

.array() replaces the column with jsonb and swaps the string operators for the multi-value set. The admin renders one textarea per item.

notes: f.textarea().array().maxItems(20),

Per-item .min() and .max() still apply. .minItems(n) and .maxItems(n) bound the list.

Collaborative editing

f.textarea() is one of the two shapes .crdt({ format: "text" }) accepts.

body: f.textarea().default("").required().crdt({ format: "text" }),

.required() and .default("") are both mandatory. .min(), .max() and .localized() each disqualify the field, and QUESTPIE refuses to build the app rather than merging into a value it cannot check.

Text is the single-line sibling. f.text({ mode: "text" }) produces the identical text column, schema and operators. It differs in the type string, which routes the admin to a one-line input instead of this control, and in carrying four more methods.

Rich text is the step up when the prose needs structure rather than a bigger box.

Arrays covers what .array() does to reads, writes and filters.

Eligible fields lists every reason a .crdt() field is rejected.

Validation covers the derived schema and .zod(), the place to normalize a value.

On this page