Custom Fields
Create custom field types with custom storage, validation, and admin rendering.
Create field types that don't exist in the core -- color pickers, phone numbers, currencies, coordinates, or domain-specific types.
Overview
A custom field requires two parts:
- Server field factory -- defines storage, validation, and metadata
- Admin renderer -- React component for editing and displaying the field
Both are registered through a module and discovered by codegen.
Server: Field Factory
Define a field factory that specifies how the value is stored and validated.
import { varchar } from "drizzle-orm/pg-core";
import { z } from "zod";
/**
* Color field — stores a hex color string.
*/
export function colorField(columnName: string) {
return {
toColumn: (name: string) => varchar(columnName || name, { length: 7 }),
$types: {
input: "" as string,
output: "" as string,
column: varchar("", { length: 7 }),
},
getLocation: () => "main" as const,
getMetadata: () => ({
type: "color" as const,
validation: z.string().regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color"),
}),
};
}Field Factory Contract
A field factory must return an object with:
| Property | Type | Description |
|---|---|---|
toColumn() | (name) => PgColumn | null | Create a Drizzle column (or null for virtual) |
$types | { input, output, column } | Type inference helpers |
getLocation() | () => "main" | "i18n" | "virtual" | Where the field is stored |
getMetadata() | () => { type, ... } | Metadata for admin introspection |
Admin: Field Renderer
Register a React component to render the field in the admin panel.
import { useFieldContext } from "@questpie/admin/client";
export function ColorField() {
const { value, onChange, field } = useFieldContext();
return (
<div>
<label>{field.label}</label>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<input
type="color"
value={value || "#000000"}
onChange={(e) => onChange(e.target.value)}
/>
<input
type="text"
value={value || ""}
onChange={(e) => onChange(e.target.value)}
placeholder="#000000"
pattern="^#[0-9a-fA-F]{6}$"
/>
</div>
</div>
);
}Register via File Convention
Create a field definition file using the field() factory:
import { field } from "@questpie/admin/client";
export default field("color", {
component: () => import("./color-field.js"),
cell: () => import("./color-cell.js"),
});The optional cell component renders the field in list view table columns:
export function ColorCell({ value }: { value: string }) {
return (
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<div
style={{
width: 16,
height: 16,
borderRadius: 4,
backgroundColor: value || "#ccc",
}}
/>
<span>{value || "—"}</span>
</div>
);
}Module Registration
Package the server-side field factory into a module so it extends the f proxy:
import { module } from "questpie";
import { colorField } from "../fields/color";
import { currencyField } from "../fields/currency";
export const customFieldsModule = module({
name: "custom-fields",
fields: {
color: colorField,
currency: currencyField,
},
});Register the module in modules.ts:
import { adminModule } from "@questpie/admin/server";
import { customFieldsModule } from "../../modules/custom-fields";
export default [adminModule, customFieldsModule] as const;After running questpie generate, the fields become available on the f builder:
collection("products")
.fields(({ f }) => ({
name: f.text({ required: true }),
brandColor: f.color(), // Your custom field!
price: f.currency("USD"), // Your custom field!
}))Localized Custom Fields
To support localization, return "i18n" from getLocation():
getLocation: () => "i18n" as const,This stores the field value in the i18n table, one per locale.
Related Pages
- Fields -- Built-in field types
- Building a Module -- Package as a module
- Plugin API -- Codegen plugin reference