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 (0)