DEV Community

Aaron K Saunders
Aaron K Saunders

Posted on

[Video] Payload CMS Custom Array Field Component

Learn how to implement a custom tagging system in Payload CMS using the array field and a custom React component! This video walks you through building a dynamic tag input where users can add, remove, and manage tags directly within the Payload admin panel.

The Video Covers:

  • Defining the Array Field: Setting up the tags field in your User collection with the type: 'array' configuration.
  • Custom Component Creation: Building a React component (CustomTagsArrayFieldClient) to handle tag input, display, and deletion. We leverage Payload's hooks (useField, useForm, useFormFields) to interact with the form data.
  • Adding and Removing Tags: Implementing logic to add new tags (with Enter key or button click) and remove existing tags using callbacks.
  • Data Structure: How the tag data is structured and retrieved via the API.
  • Rendering the Tags: Using React's map function to display the tags dynamically.

This tutorial video tutorial provides a practical example of extending Payload's functionality with custom field components.

The Video

Setting Up Project

Follow instructions for creating a blank application using create-payload-app

Add Custom Component to User Collection

Add new field to the user collection

 { name: 'tags', type: 'array', admin: { components: { Field: '@/collections/CustomFields/CustomTagsArrayFieldClient', }, }, fields: [ { name: 'tag', type: 'text', }, ], }, 
Enter fullscreen mode Exit fullscreen mode

Create the Custom Component

Create new file CustomTagsArrayFieldClient.tsx

'use client' import type { ArrayFieldClientComponent } from 'payload' import { TextField, useFormFields, useField, useForm } from '@payloadcms/ui' import React, { useCallback, useMemo, memo } from 'react' /** * Interface for Tag component props */ interface TagProps { /** Unique identifier for the tag */ id: string /** Display value of the tag */ value: string /** Callback function to remove the tag */ onRemove: (index: number) => void /** Index of the tag in the array */ index: number } /** * Memoized Tag component that renders an individual tag with delete functionality * @component */ const Tag = memo(({ id, value, onRemove, index }: TagProps) => ( <div style={{ backgroundColor: '#e0e0e0', padding: '8px 12px', borderRadius: '8px', display: 'flex', alignItems: 'center', gap: '4px', }} > <span style={{ fontSize: '14px', fontWeight: 'bold', cursor: 'default' }}>{value}</span>  <button onClick={() => onRemove(index)} type="button" style={{ border: 'none', background: 'none', padding: '0 4px', cursor: 'pointer', fontSize: '14px', fontWeight: 'bold', }} > X </button>  </div> )) Tag.displayName = 'Tag' /** * Custom array field component for managing tags in Payload CMS * @component * @param {Object} props - Component props from Payload CMS * @param {string} props.path - Path to the field in the form * @param {Object} props.field - Field configuration from Payload CMS */ const CustomTagsArrayFieldClient: ArrayFieldClientComponent = ({ path, field, ...props }) => { const { rows } = useField({ path, hasRows: true }) const { addFieldRow, removeFieldRow, setModified } = useForm() const { dispatch } = useFormFields(([_, dispatch]) => ({ dispatch })) const [newTagValue, setNewTagValue] = React.useState('') /** * Get tag values from form fields */ const tags = useFormFields(([fields]) => rows?.map((row, index) => ({ id: row.id, value: fields[`${path}.${index}.tag`]?.value || '', })), ) /** * Handles adding a new tag to the array */ const handleAddRow = useCallback(() => { if (!newTagValue.trim()) return addFieldRow({ path: 'tags', schemaPath: `${path}.0.tag`, }) setTimeout(() => { dispatch({ type: 'UPDATE', path: `${path}.${rows?.length || 0}.tag`, value: newTagValue.trim(), }) setNewTagValue('') setModified(true) }, 0) }, [addFieldRow, dispatch, path, rows?.length, newTagValue, setModified]) /** * Handles Enter key press in the input field */ const handleKeyPress = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault() handleAddRow() } }, [handleAddRow], ) /** * Handles removing a tag from the array */ const handleRemoveTag = useCallback( (index: number) => { removeFieldRow({ path, rowIndex: index }) setModified(true) }, [removeFieldRow, path, setModified], ) /** * Memoized tag list rendering */ const tagList = useMemo( () => ( <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}> {tags?.map((tag, index) => ( <Tag key={tag.id} id={tag.id} value={tag.value as string} onRemove={handleRemoveTag} index={index} />  ))} </div>  ), [tags, handleRemoveTag], ) return ( <div> <h4>Tags</h4>  <div style={{ marginTop: '18px' }}>{tagList}</div>  <div style={{ marginTop: '12px', display: 'flex', gap: '8px' }}> <input className="inputFieldClass" type="text" value={newTagValue} onChange={(e) => setNewTagValue(e.target.value)} onKeyDown={handleKeyPress} placeholder="Enter tag name" style={{ padding: '4px 8px', borderRadius: '4px', border: '1px solid #ccc', fontSize: '14px', width: '260px', }} />  <button onClick={handleAddRow} type="button" disabled={!newTagValue.trim()}> Add Tag </button>  </div>  </div>  ) } export default memo(CustomTagsArrayFieldClient) 
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
maicolsantos profile image
Maicol Santos

As always great content Aaron, I tried it the other way, just using ADD_ROW, so keep doing this amazing contents

`
const {dispatchFields} = useForm()

dispatchFields({
type: 'ADD_ROW',
path: 'tags',

subFieldState: {
tag: {value: 'Foo'}
},
})
`