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.
| Surface | What f.textarea() produces |
|---|---|
| Column | text, no length cap |
| Value | string, or string | null without .required() |
| Schema | z.string(), no cap |
| Operators | stringOps, fourteen of them |
| Form control | TextareaField, a plain textarea |
| List cell | TextCell, 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.
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.
| Method | Effect |
|---|---|
.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().
| Operator | Operand | Matches |
|---|---|---|
eq / ne | string | Equal, not equal |
not | string, or null | Not equal, or IS NOT NULL when given null |
in / notIn | string[] | In the list, not in the list |
like / notLike | string | LIKE, case-sensitive, you write the % |
ilike / notIlike | string | The same, case-insensitive |
contains | string | Substring, the % are added for you |
startsWith | string | Prefix |
endsWith | string | Suffix |
isNull / isNotNull | boolean | Pass 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'smaxLengthattribute, so typing stops there. Without it there is no attribute and no stop.- The admin rebuilds
z.string()fromminLengthandmaxLength, 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.
| Key | Effect |
|---|---|
placeholder | Placeholder text, resolved through the locale |
rows | The rows attribute, default 3 |
autoResize | Nothing reads it |
richText | Nothing reads it |
showCounter | Nothing 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.
Related
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.
Text field
`f.text()` is the single-line string. The call picks the varchar width, the width becomes the Zod cap, and the field filters with the full string operator set.
Email field
`f.email()` is a varchar with a format check on it. One argument sizes the column, and the type extends the string operator set with two domain matchers.