773 lines
19 KiB
Markdown
773 lines
19 KiB
Markdown
# Payload Field Types Reference
|
|
|
|
Complete reference for all Payload field types with examples.
|
|
|
|
## Text Field
|
|
|
|
```ts
|
|
import type { TextField } from 'payload'
|
|
|
|
const textField: TextField = {
|
|
name: 'title',
|
|
type: 'text',
|
|
required: true,
|
|
unique: true,
|
|
minLength: 5,
|
|
maxLength: 100,
|
|
index: true,
|
|
localized: true,
|
|
defaultValue: 'Default Title',
|
|
validate: (value) => Boolean(value) || 'Required',
|
|
admin: {
|
|
placeholder: 'Enter title...',
|
|
position: 'sidebar',
|
|
condition: (data) => data.showTitle === true,
|
|
},
|
|
}
|
|
```
|
|
|
|
> **When to use `position: 'sidebar'`:** Reserve the sidebar for short fields that
|
|
> give quick insight into the content — status, category, author, publish date,
|
|
> slug. Avoid it for fields that need horizontal space to be useful, like a
|
|
> description, rich text content, or long text — those belong in the main document
|
|
> area. (`title` above is shown in the sidebar only to demonstrate the option.)
|
|
|
|
### Slug Field Helper
|
|
|
|
Built-in helper for auto-generating slugs. **Use this for all slugs** instead of
|
|
hand-rolling a `{ name: 'slug', type: 'text', unique: true }` field — it
|
|
auto-generates the slug from the title, adds a regenerate toggle, and handles
|
|
uniqueness and indexing. Call it with no args when the collection has a `title`
|
|
field — the slug is generated from `title` by default:
|
|
|
|
```ts
|
|
import { slugField } from 'payload'
|
|
import type { CollectionConfig } from 'payload'
|
|
|
|
export const Pages: CollectionConfig = {
|
|
slug: 'pages',
|
|
fields: [
|
|
{ name: 'title', type: 'text', required: true },
|
|
slugField(), // name: 'slug', useAsSlug: 'title', required, unique, position: 'sidebar'
|
|
],
|
|
}
|
|
```
|
|
|
|
`useAsSlug` defaults to `'title'`, so if the collection has **no `title` field**,
|
|
you must pass the source field explicitly — otherwise the slug generates from a
|
|
field that doesn't exist:
|
|
|
|
```ts
|
|
// Collection keyed on `name` instead of `title`
|
|
fields: [{ name: 'name', type: 'text', required: true }, slugField({ useAsSlug: 'name' })]
|
|
```
|
|
|
|
Override defaults when needed (`overrides` receives the generated `RowField`):
|
|
|
|
```ts
|
|
slugField({
|
|
name: 'slug', // defaults to 'slug'
|
|
useAsSlug: 'title', // defaults to 'title'
|
|
checkboxName: 'generateSlug', // defaults to 'generateSlug'
|
|
localized: true,
|
|
required: true, // defaults to true
|
|
position: 'sidebar', // default; the slug is a short field well-suited to the sidebar
|
|
overrides: (field) => {
|
|
field.fields[1].label = 'Custom Slug Label'
|
|
return field
|
|
},
|
|
})
|
|
```
|
|
|
|
## Rich Text (Lexical)
|
|
|
|
```ts
|
|
import type { RichTextField } from 'payload'
|
|
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
|
import { HeadingFeature, LinkFeature } from '@payloadcms/richtext-lexical'
|
|
|
|
const richTextField: RichTextField = {
|
|
name: 'content',
|
|
type: 'richText',
|
|
required: true,
|
|
localized: true,
|
|
editor: lexicalEditor({
|
|
features: ({ defaultFeatures }) => [
|
|
...defaultFeatures,
|
|
HeadingFeature({
|
|
enabledHeadingSizes: ['h1', 'h2', 'h3'],
|
|
}),
|
|
LinkFeature({
|
|
enabledCollections: ['posts', 'pages'],
|
|
}),
|
|
],
|
|
}),
|
|
}
|
|
```
|
|
|
|
### Advanced Lexical Configuration
|
|
|
|
```ts
|
|
import {
|
|
BoldFeature,
|
|
EXPERIMENTAL_TableFeature,
|
|
FixedToolbarFeature,
|
|
HeadingFeature,
|
|
IndentFeature,
|
|
InlineToolbarFeature,
|
|
ItalicFeature,
|
|
LinkFeature,
|
|
OrderedListFeature,
|
|
UnderlineFeature,
|
|
UnorderedListFeature,
|
|
lexicalEditor,
|
|
} from '@payloadcms/richtext-lexical'
|
|
|
|
// Global editor config with full features
|
|
export default buildConfig({
|
|
editor: lexicalEditor({
|
|
features: () => {
|
|
return [
|
|
UnderlineFeature(),
|
|
BoldFeature(),
|
|
ItalicFeature(),
|
|
OrderedListFeature(),
|
|
UnorderedListFeature(),
|
|
LinkFeature({
|
|
enabledCollections: ['pages'],
|
|
fields: ({ defaultFields }) => {
|
|
const defaultFieldsWithoutUrl = defaultFields.filter((field) => {
|
|
if ('name' in field && field.name === 'url') return false
|
|
return true
|
|
})
|
|
|
|
return [
|
|
...defaultFieldsWithoutUrl,
|
|
{
|
|
name: 'url',
|
|
type: 'text',
|
|
admin: {
|
|
condition: ({ linkType }) => linkType !== 'internal',
|
|
},
|
|
label: ({ t }) => t('fields:enterURL'),
|
|
required: true,
|
|
},
|
|
]
|
|
},
|
|
}),
|
|
IndentFeature(),
|
|
EXPERIMENTAL_TableFeature(),
|
|
]
|
|
},
|
|
}),
|
|
})
|
|
|
|
// Field-specific editor with custom toolbar
|
|
const richTextWithToolbars: RichTextField = {
|
|
name: 'richText',
|
|
type: 'richText',
|
|
editor: lexicalEditor({
|
|
features: ({ rootFeatures }) => {
|
|
return [
|
|
...rootFeatures,
|
|
HeadingFeature({ enabledHeadingSizes: ['h2', 'h3', 'h4'] }),
|
|
FixedToolbarFeature(),
|
|
InlineToolbarFeature(),
|
|
]
|
|
},
|
|
}),
|
|
label: false,
|
|
}
|
|
```
|
|
|
|
## Relationship
|
|
|
|
```ts
|
|
import type { RelationshipField } from 'payload'
|
|
|
|
// Single relationship
|
|
const singleRelationship: RelationshipField = {
|
|
name: 'author',
|
|
type: 'relationship',
|
|
relationTo: 'users',
|
|
required: true,
|
|
maxDepth: 2,
|
|
}
|
|
|
|
// Multiple relationships (hasMany)
|
|
const multipleRelationship: RelationshipField = {
|
|
name: 'categories',
|
|
type: 'relationship',
|
|
relationTo: 'categories',
|
|
hasMany: true,
|
|
filterOptions: {
|
|
active: { equals: true },
|
|
},
|
|
}
|
|
|
|
// Polymorphic relationship
|
|
const polymorphicRelationship: PolymorphicRelationshipField = {
|
|
name: 'relatedContent',
|
|
type: 'relationship',
|
|
relationTo: ['posts', 'pages'],
|
|
hasMany: true,
|
|
}
|
|
```
|
|
|
|
## Array
|
|
|
|
```ts
|
|
import type { ArrayField } from 'payload'
|
|
|
|
const arrayField: ArrayField = {
|
|
name: 'slides',
|
|
type: 'array',
|
|
minRows: 2,
|
|
maxRows: 10,
|
|
labels: {
|
|
singular: 'Slide',
|
|
plural: 'Slides',
|
|
},
|
|
fields: [
|
|
{
|
|
name: 'title',
|
|
type: 'text',
|
|
required: true,
|
|
},
|
|
{
|
|
name: 'image',
|
|
type: 'upload',
|
|
relationTo: 'media',
|
|
},
|
|
],
|
|
admin: {
|
|
initCollapsed: true,
|
|
},
|
|
}
|
|
```
|
|
|
|
## Blocks
|
|
|
|
```ts
|
|
import type { BlocksField, Block } from 'payload'
|
|
|
|
const HeroBlock: Block = {
|
|
slug: 'hero',
|
|
interfaceName: 'HeroBlock',
|
|
fields: [
|
|
{
|
|
name: 'heading',
|
|
type: 'text',
|
|
required: true,
|
|
},
|
|
{
|
|
name: 'background',
|
|
type: 'upload',
|
|
relationTo: 'media',
|
|
},
|
|
],
|
|
}
|
|
|
|
const ContentBlock: Block = {
|
|
slug: 'content',
|
|
fields: [
|
|
{
|
|
name: 'text',
|
|
type: 'richText',
|
|
},
|
|
],
|
|
}
|
|
|
|
const blocksField: BlocksField = {
|
|
name: 'layout',
|
|
type: 'blocks',
|
|
blocks: [HeroBlock, ContentBlock],
|
|
}
|
|
```
|
|
|
|
## Select
|
|
|
|
```ts
|
|
import type { SelectField } from 'payload'
|
|
|
|
// Use select for genuine taxonomy. For publish state, enable versions.drafts
|
|
// and rely on the auto-injected _status field instead of a custom select.
|
|
const selectField: SelectField = {
|
|
name: 'priority',
|
|
type: 'select',
|
|
options: [
|
|
{ label: 'Low', value: 'low' },
|
|
{ label: 'Medium', value: 'medium' },
|
|
{ label: 'High', value: 'high' },
|
|
],
|
|
defaultValue: 'medium',
|
|
required: true,
|
|
}
|
|
|
|
// Multiple select
|
|
const multiSelectField: SelectField = {
|
|
name: 'tags',
|
|
type: 'select',
|
|
hasMany: true,
|
|
options: ['tech', 'news', 'sports'],
|
|
}
|
|
```
|
|
|
|
## Upload
|
|
|
|
```ts
|
|
import type { UploadField } from 'payload'
|
|
|
|
const uploadField: UploadField = {
|
|
name: 'featuredImage',
|
|
type: 'upload',
|
|
relationTo: 'media',
|
|
required: true,
|
|
filterOptions: {
|
|
mimeType: { contains: 'image' },
|
|
},
|
|
}
|
|
```
|
|
|
|
## Point (Geolocation)
|
|
|
|
Point fields store geographic coordinates with automatic 2dsphere indexing for geospatial queries.
|
|
|
|
```ts
|
|
import type { PointField } from 'payload'
|
|
|
|
const locationField: PointField = {
|
|
name: 'location',
|
|
type: 'point',
|
|
label: 'Location',
|
|
required: true,
|
|
}
|
|
|
|
// Returns [longitude, latitude]
|
|
// Example: [-122.4194, 37.7749] for San Francisco
|
|
```
|
|
|
|
### Geospatial Queries
|
|
|
|
```ts
|
|
// Query by distance (sorted by nearest first)
|
|
const nearbyLocations = await payload.find({
|
|
collection: 'stores',
|
|
where: {
|
|
location: {
|
|
near: [10, 20], // [longitude, latitude]
|
|
maxDistance: 5000, // in meters
|
|
minDistance: 1000,
|
|
},
|
|
},
|
|
})
|
|
|
|
// Query within polygon area
|
|
const polygon: Point[] = [
|
|
[9.0, 19.0], // bottom-left
|
|
[9.0, 21.0], // top-left
|
|
[11.0, 21.0], // top-right
|
|
[11.0, 19.0], // bottom-right
|
|
[9.0, 19.0], // closing point
|
|
]
|
|
|
|
const withinArea = await payload.find({
|
|
collection: 'stores',
|
|
where: {
|
|
location: {
|
|
within: {
|
|
type: 'Polygon',
|
|
coordinates: [polygon],
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
// Query intersecting area
|
|
const intersecting = await payload.find({
|
|
collection: 'stores',
|
|
where: {
|
|
location: {
|
|
intersects: {
|
|
type: 'Polygon',
|
|
coordinates: [polygon],
|
|
},
|
|
},
|
|
},
|
|
})
|
|
```
|
|
|
|
**Note**: Point fields are not supported in SQLite.
|
|
|
|
## Join Fields
|
|
|
|
Join fields create reverse relationships, allowing you to access related documents from the "other side" of a relationship.
|
|
|
|
```ts
|
|
import type { JoinField } from 'payload'
|
|
|
|
// From Users collection - show user's orders
|
|
const ordersJoinField: JoinField = {
|
|
name: 'orders',
|
|
type: 'join',
|
|
collection: 'orders',
|
|
on: 'customer', // The field in 'orders' that references this user
|
|
admin: {
|
|
allowCreate: false,
|
|
defaultColumns: ['id', 'createdAt', 'total', 'currency', 'items'],
|
|
},
|
|
}
|
|
|
|
// From Users collection - show user's cart
|
|
const cartJoinField: JoinField = {
|
|
name: 'cart',
|
|
type: 'join',
|
|
collection: 'carts',
|
|
on: 'customer',
|
|
admin: {
|
|
allowCreate: false,
|
|
defaultColumns: ['id', 'createdAt', 'total', 'currency'],
|
|
},
|
|
}
|
|
```
|
|
|
|
## Virtual Fields
|
|
|
|
```ts
|
|
import type { TextField } from 'payload'
|
|
|
|
// Computed from siblings
|
|
const computedVirtualField: TextField = {
|
|
name: 'fullName',
|
|
type: 'text',
|
|
virtual: true,
|
|
hooks: {
|
|
afterRead: [({ siblingData }) => `${siblingData.firstName} ${siblingData.lastName}`],
|
|
},
|
|
}
|
|
|
|
// From relationship path
|
|
const pathVirtualField: TextField = {
|
|
name: 'authorName',
|
|
type: 'text',
|
|
virtual: 'author.name',
|
|
}
|
|
```
|
|
|
|
## Conditional Fields
|
|
|
|
```ts
|
|
import type { UploadField, CheckboxField } from 'payload'
|
|
|
|
// Simple boolean condition
|
|
const enableFeatureField: CheckboxField = {
|
|
name: 'enableFeature',
|
|
type: 'checkbox',
|
|
}
|
|
|
|
const conditionalField: TextField = {
|
|
name: 'featureText',
|
|
type: 'text',
|
|
admin: {
|
|
condition: (data) => data.enableFeature === true,
|
|
},
|
|
}
|
|
|
|
// Sibling data condition (from hero field pattern)
|
|
const typeField: SelectField = {
|
|
name: 'type',
|
|
type: 'select',
|
|
options: ['none', 'highImpact', 'mediumImpact', 'lowImpact'],
|
|
defaultValue: 'lowImpact',
|
|
}
|
|
|
|
const mediaField: UploadField = {
|
|
name: 'media',
|
|
type: 'upload',
|
|
relationTo: 'media',
|
|
admin: {
|
|
condition: (_, { type } = {}) => ['highImpact', 'mediumImpact'].includes(type),
|
|
},
|
|
required: true,
|
|
}
|
|
```
|
|
|
|
## Radio
|
|
|
|
Radio fields present options as radio buttons for single selection.
|
|
|
|
```ts
|
|
import type { RadioField } from 'payload'
|
|
|
|
const radioField: RadioField = {
|
|
name: 'priority',
|
|
type: 'radio',
|
|
options: [
|
|
{ label: 'Low', value: 'low' },
|
|
{ label: 'Medium', value: 'medium' },
|
|
{ label: 'High', value: 'high' },
|
|
],
|
|
defaultValue: 'medium',
|
|
admin: {
|
|
layout: 'horizontal', // or 'vertical'
|
|
},
|
|
}
|
|
```
|
|
|
|
## Row (Layout)
|
|
|
|
Row fields arrange fields horizontally in the admin panel (presentational only).
|
|
|
|
```ts
|
|
import type { RowField } from 'payload'
|
|
|
|
const rowField: RowField = {
|
|
type: 'row',
|
|
fields: [
|
|
{
|
|
name: 'firstName',
|
|
type: 'text',
|
|
admin: { width: '50%' },
|
|
},
|
|
{
|
|
name: 'lastName',
|
|
type: 'text',
|
|
admin: { width: '50%' },
|
|
},
|
|
],
|
|
}
|
|
```
|
|
|
|
## Collapsible (Layout)
|
|
|
|
Collapsible fields group fields in an expandable/collapsible section.
|
|
|
|
```ts
|
|
import type { CollapsibleField } from 'payload'
|
|
|
|
const collapsibleField: CollapsibleField = {
|
|
label: ({ data }) => data?.title || 'Advanced Options',
|
|
type: 'collapsible',
|
|
admin: {
|
|
initCollapsed: true,
|
|
},
|
|
fields: [
|
|
{ name: 'customCSS', type: 'textarea' },
|
|
{ name: 'customJS', type: 'code' },
|
|
],
|
|
}
|
|
```
|
|
|
|
## UI (Custom Components)
|
|
|
|
UI fields allow fully custom React components in the admin (no data stored).
|
|
|
|
```ts
|
|
import type { UIField } from 'payload'
|
|
|
|
const uiField: UIField = {
|
|
name: 'customMessage',
|
|
type: 'ui',
|
|
admin: {
|
|
components: {
|
|
Field: '/path/to/CustomFieldComponent',
|
|
Cell: '/path/to/CustomCellComponent', // For list view
|
|
},
|
|
},
|
|
}
|
|
```
|
|
|
|
## Tabs & Groups
|
|
|
|
```ts
|
|
import type { TabsField, GroupField } from 'payload'
|
|
|
|
// Tabs
|
|
const tabsField: TabsField = {
|
|
type: 'tabs',
|
|
tabs: [
|
|
{
|
|
label: 'Content',
|
|
fields: [
|
|
{ name: 'title', type: 'text' },
|
|
{ name: 'body', type: 'richText' },
|
|
],
|
|
},
|
|
{
|
|
label: 'SEO',
|
|
fields: [
|
|
{ name: 'metaTitle', type: 'text' },
|
|
{ name: 'metaDescription', type: 'textarea' },
|
|
],
|
|
},
|
|
],
|
|
}
|
|
|
|
// Group (named)
|
|
const groupField: GroupField = {
|
|
name: 'meta',
|
|
type: 'group',
|
|
fields: [
|
|
{ name: 'title', type: 'text' },
|
|
{ name: 'description', type: 'textarea' },
|
|
],
|
|
}
|
|
```
|
|
|
|
## Reusable Field Factories
|
|
|
|
Create composable field patterns that can be customized with overrides.
|
|
|
|
```ts
|
|
import type { Field, GroupField } from 'payload'
|
|
|
|
// Utility for deep merging
|
|
const deepMerge = <T>(target: T, source: Partial<T>): T => {
|
|
// Implementation would deeply merge objects
|
|
return { ...target, ...source }
|
|
}
|
|
|
|
// Reusable link field factory
|
|
type LinkType = (options?: {
|
|
appearances?: ('default' | 'outline')[] | false
|
|
disableLabel?: boolean
|
|
overrides?: Record<string, unknown>
|
|
}) => GroupField
|
|
|
|
export const link: LinkType = ({ appearances, disableLabel = false, overrides = {} } = {}) => {
|
|
const linkField: GroupField = {
|
|
name: 'link',
|
|
type: 'group',
|
|
admin: {
|
|
hideGutter: true,
|
|
},
|
|
fields: [
|
|
{
|
|
type: 'row',
|
|
fields: [
|
|
{
|
|
name: 'type',
|
|
type: 'radio',
|
|
options: [
|
|
{ label: 'Internal link', value: 'reference' },
|
|
{ label: 'Custom URL', value: 'custom' },
|
|
],
|
|
defaultValue: 'reference',
|
|
admin: {
|
|
layout: 'horizontal',
|
|
width: '50%',
|
|
},
|
|
},
|
|
{
|
|
name: 'newTab',
|
|
type: 'checkbox',
|
|
label: 'Open in new tab',
|
|
admin: {
|
|
width: '50%',
|
|
style: {
|
|
alignSelf: 'flex-end',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'reference',
|
|
type: 'relationship',
|
|
relationTo: ['pages'],
|
|
required: true,
|
|
maxDepth: 1,
|
|
admin: {
|
|
condition: (_, siblingData) => siblingData?.type === 'reference',
|
|
},
|
|
},
|
|
{
|
|
name: 'url',
|
|
type: 'text',
|
|
label: 'Custom URL',
|
|
required: true,
|
|
admin: {
|
|
condition: (_, siblingData) => siblingData?.type === 'custom',
|
|
},
|
|
},
|
|
],
|
|
}
|
|
|
|
if (!disableLabel) {
|
|
linkField.fields.push({
|
|
name: 'label',
|
|
type: 'text',
|
|
required: true,
|
|
})
|
|
}
|
|
|
|
if (appearances !== false) {
|
|
linkField.fields.push({
|
|
name: 'appearance',
|
|
type: 'select',
|
|
defaultValue: 'default',
|
|
options: [
|
|
{ label: 'Default', value: 'default' },
|
|
{ label: 'Outline', value: 'outline' },
|
|
],
|
|
})
|
|
}
|
|
|
|
return deepMerge(linkField, overrides) as GroupField
|
|
}
|
|
|
|
// Usage
|
|
const navItem = link({ appearances: false })
|
|
const ctaButton = link({
|
|
overrides: {
|
|
name: 'cta',
|
|
admin: {
|
|
description: 'Call to action button',
|
|
},
|
|
},
|
|
})
|
|
```
|
|
|
|
## Field Type Guards
|
|
|
|
Type guards for runtime field type checking and safe type narrowing.
|
|
|
|
| Type Guard | Checks For | Use When |
|
|
| --------------------------- | ----------------------------------------------------------- | ---------------------------------------- |
|
|
| `fieldAffectsData` | Field stores data (has name, not UI-only) | Need to access field data or name |
|
|
| `fieldHasSubFields` | Field contains nested fields (group/array/row/collapsible) | Need to recursively traverse fields |
|
|
| `fieldIsArrayType` | Field is array type | Distinguish arrays from other containers |
|
|
| `fieldIsBlockType` | Field is blocks type | Handle blocks-specific logic |
|
|
| `fieldIsGroupType` | Field is group type | Handle group-specific logic |
|
|
| `fieldSupportsMany` | Field can have multiple values (select/relationship/upload) | Check for `hasMany` support |
|
|
| `fieldHasMaxDepth` | Field supports population depth control | Control relationship/upload/join depth |
|
|
| `fieldIsPresentationalOnly` | Field is UI-only (no data storage) | Exclude from data operations |
|
|
| `fieldIsSidebar` | Field positioned in sidebar | Separate sidebar rendering |
|
|
| `fieldIsID` | Field name is 'id' | Special ID field handling |
|
|
| `fieldIsHiddenOrDisabled` | Field is hidden or disabled | Filter from UI operations |
|
|
| `fieldShouldBeLocalized` | Field needs localization handling | Proper locale table checks |
|
|
| `fieldIsVirtual` | Field is virtual (computed/no DB column) | Skip in database transforms |
|
|
| `tabHasName` | Tab is named (stores data) | Distinguish named vs unnamed tabs |
|
|
| `groupHasName` | Group is named (stores data) | Distinguish named vs unnamed groups |
|
|
| `optionIsObject` | Option is `{label, value}` format | Access option properties safely |
|
|
| `optionsAreObjects` | All options are objects | Batch option processing |
|
|
| `optionIsValue` | Option is string value | Handle string options |
|
|
| `valueIsValueWithRelation` | Value is polymorphic relationship | Handle polymorphic relationships |
|
|
|
|
```ts
|
|
import { fieldAffectsData, fieldHasSubFields, fieldIsArrayType } from 'payload'
|
|
|
|
function processField(field: Field) {
|
|
if (fieldAffectsData(field)) {
|
|
// Safe to access field.name
|
|
console.log(field.name)
|
|
}
|
|
|
|
if (fieldHasSubFields(field)) {
|
|
// Safe to access field.fields
|
|
field.fields.forEach(processField)
|
|
}
|
|
}
|
|
```
|
|
|
|
See [FIELD-TYPE-GUARDS.md](FIELD-TYPE-GUARDS.md) for detailed usage patterns.
|