Кастомні поля Payload CMS
Payload надає 20+ вбудованих типів полів. Для нестандартних випадків — кастомні компоненти React для admin panel при повному збереженні логіки валідації та зберігання на стороні сервера.
Вбудовані поля з розширеною конфігурацією
Поле з умовною видимістю:
{
name: 'discountPrice',
type: 'number',
admin: {
condition: (data, siblingData) => siblingData.hasDiscount === true,
description: 'Ціна зі знижкою',
},
}
Поле з валідацією:
{
name: 'phone',
type: 'text',
validate: (value) => {
if (!value) return true
const phoneRegex = /^\+380\d{9}$/
if (!phoneRegex.test(value)) {
return 'Формат: +380XXXXXXXXX'
}
return true
},
}
Richtext з кастомними функціями:
import { lexicalEditor, LinkFeature, UploadFeature } from '@payloadcms/richtext-lexical'
{
name: 'content',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
LinkFeature({ enabledCollections: ['pages', 'posts'] }),
UploadFeature({ collections: { media: { fields: [{ name: 'caption', type: 'text' }] } } }),
],
}),
}
Кастомний компонент поля
Для відображення нестандартного UI в admin panel, при цьому дані зберігаються як звичайно:
// fields/ColorPicker/index.tsx (admin компонент)
'use client'
import { useField } from 'payload/components/forms'
const ColorPickerField = ({ path }: { path: string }) => {
const { value, setValue } = useField<string>({ path })
const colors = ['#FF5733', '#33FF57', '#3357FF', '#FF33A8', '#33A8FF']
return (
<div className="field-type">
<label className="field-label">Колір</label>
<div style={{ display: 'flex', gap: 8 }}>
{colors.map(color => (
<div
key={color}
onClick={() => setValue(color)}
style={{
width: 32,
height: 32,
borderRadius: '50%',
background: color,
cursor: 'pointer',
border: value === color ? '3px solid #000' : '2px solid transparent',
}}
/>
))}
</div>
<input
type="text"
value={value || ''}
onChange={e => setValue(e.target.value)}
placeholder="#000000"
style={{ marginTop: 8 }}
/>
</div>
)
}
export default ColorPickerField
// collections/Products.ts — підключення кастомного компонента
{
name: 'brandColor',
type: 'text',
admin: {
components: {
Field: '/fields/ColorPicker/index#ColorPickerField',
},
},
}
Поле Blocks (гнучкий конструктор сторінок)
// blocks/TextBlock.ts
import { Block } from 'payload/types'
const TextBlock: Block = {
slug: 'textBlock',
labels: { singular: 'Текстовий блок', plural: 'Текстові блоки' },
fields: [
{ name: 'content', type: 'richText' },
{
name: 'columns',
type: 'select',
options: [
{ label: '1 колонка', value: '1' },
{ label: '2 колонки', value: '2' },
],
defaultValue: '1',
},
],
}
const ImageBlock: Block = {
slug: 'imageBlock',
fields: [
{ name: 'image', type: 'upload', relationTo: 'media', required: true },
{ name: 'caption', type: 'text' },
{ name: 'fullWidth', type: 'checkbox', defaultValue: false },
],
}
// У колекції Pages:
{
name: 'sections',
type: 'blocks',
blocks: [TextBlock, ImageBlock, CTABlock, GalleryBlock],
minRows: 1,
}
Рендеринг Blocks у фронтенді
// components/Blocks.tsx
import type { Page } from '@/payload-types'
type BlockComponent = {
textBlock: React.FC<{ content: any; columns: string }>
imageBlock: React.FC<{ image: any; caption?: string; fullWidth: boolean }>
}
const blockComponents: BlockComponent = {
textBlock: ({ content, columns }) => (
<div className={`columns-${columns}`}>
<RichText content={content} />
</div>
),
imageBlock: ({ image, caption, fullWidth }) => (
<figure className={fullWidth ? 'full-width' : ''}>
<img src={image.url} alt={image.alt} />
{caption && <figcaption>{caption}</figcaption>}
</figure>
),
}
export const Blocks = ({ sections }: { sections: Page['sections'] }) => {
return (
<>
{sections?.map((block, i) => {
const Component = blockComponents[block.blockType as keyof BlockComponent]
if (!Component) return null
return <Component key={i} {...(block as any)} />
})}
</>
)
}
Віртуальні поля (тільки admin)
{
name: 'fullName',
type: 'text',
admin: {
readOnly: true,
description: 'Вичисляється автоматично',
},
hooks: {
afterRead: [
({ data }) => `${data?.firstName} ${data?.lastName}`.trim(),
],
},
}
Часові рамки
Розробка набору кастомних полів (3–5 нестандартних типів з компонентами) — 2–3 дні.







