Custom Paragraphs Module – Complete Usage Guide

This guide explains how to use the Custom Paragraphs module to create dynamic repeatable field groups in Drupal custom forms.


What is Custom Paragraphs?

Custom Paragraphs is a JavaScript-powered solution that allows developers to create repeatable form field groups with advanced features like file uploads, CKEditor support, and validation.

  • Add unlimited items dynamically
  • Supports multiple field types
  • Handles file uploads with preview
  • Stores data as JSON

Step 1: Attach Library

$form['#attached']['library'][] = 'your_module/repeatable_fields';

---

Step 2: JavaScript Initialization

(function (Drupal, once, window) {
  "use strict";

  Drupal.behaviors.repeatableFields = {
    attach(context) {
      once("repeatable-fields", "[data-repeatable-field-group]", context)
        .forEach((element, index) => {

          let config = {};

          try {
            config = JSON.parse(
              element.getAttribute("data-repeatable-field-group") || "{}"
            );
          } catch (error) {
            config = {};
          }

          element.setAttribute("data-rfg-instance", index + 1);

          if (typeof window.RepeatableFieldGroup === "function") {
            new window.RepeatableFieldGroup(element, config);
          }
        });
    },
  };
})(Drupal, once, window);

Explanation:

  • Reads configuration from HTML attribute
  • Initializes repeatable field group
  • Supports multiple instances

Step 3: Library Definition

repeatable_fields:
  version: 1.01
  js:
    js/repeatable_fields.js: {}
  dependencies:
    - core/drupal
    - core/once
    - custom_paragraphs/custom_paragraphs

Step 4: Drupal Form Implementation

$form['cards_wrapper'] = [
  '#type' => 'container',
  '#tree' => true,
  '#attributes' => ['id' => 'cards-wrapper'],
];

$form['cards_wrapper']['cards'] = [
  '#type' => 'container',
  '#attributes' => [
    'class' => ['cards-list'],
    'data-repeatable-field-group' => json_encode([...CONFIG HERE...]),
  ],
];

Hidden Field (Data Storage)

$form['cards_wrapper']['cards_json'] = [
  '#type' => 'hidden',
  '#attributes' => ['class' => ['cards-list-hidden']],
];

All repeatable data is stored in JSON format inside this hidden field.


Full Configuration Example

{
  "groupLabel": "Card",
  "maxItems": 8,
  "minItems": 1,
  "initialItems": 1,
  "addButtonText": "Add another",
  "removeButtonText": "Remove",
  "hiddenInputSelector": ".cards-list-hidden",
  "validationPrefix": "cards-list",
  "validateBeforeAdd": true,

  "fields": [
    {
      "type": "text",
      "name": "title",
      "label": "Title",
      "required": true
    },
    {
      "type": "textarea",
      "name": "description",
      "editor": "ckeditor5",
      "editorFormat": "basic_html"
    },
    {
      "type": "file",
      "name": "image",
      "upload_location": "public://images/"
    }
  ]
}

Configuration Options Explained

OptionDescription
groupLabelLabel for each item
maxItemsMaximum number of items
minItemsMinimum required items
initialItemsItems shown initially
addButtonTextAdd button label
removeButtonTextRemove button label
hiddenInputSelectorWhere JSON data is stored
validateBeforeAddValidate before adding new item

Field Options

  • type: text, textarea, select, file, checkbox
  • name: field key
  • label: field label
  • required: true/false
  • editor: ckeditor5
  • upload_location: Drupal file path

How It Works

  1. User clicks "Add"
  2. New field group appears
  3. User fills data
  4. Data stored in hidden JSON field
  5. On submit → backend processes JSON

Best Use Cases

  • Cards / Sections builder
  • Team members
  • Multiple uploads
  • Dynamic form inputs

Repeatable Field Group Configuration (Full Example)

The following example demonstrates a complete implementation of a repeatable field group using the Custom Paragraphs module inside a Drupal form. Each option is carefully configured to control behavior, validation, UI, and data handling.


$section['cards_wrapper'] = [
  '#type' => 'container', // Drupal container to group elements
  '#tree' => true, // Keeps form values structured in array format
  '#attributes' => [
    'id' => 'cards-wrapper', // Unique wrapper ID
  ],
];

$section['cards_wrapper']['cards'] = [
  '#type' => 'container', // Container for repeatable items
  '#attributes' => [
    'class' => ['cards-list'], // CSS class for styling & JS targeting

    // Main configuration passed to JavaScript
    'data-repeatable-field-group' => json_encode([

      // Label used for each item (e.g., Card 1, Card 2)
      'groupLabel' => 'Card',

      // Maximum number of items allowed
      'maxItems' => 8,

      // Minimum number of items required
      'minItems' => 1,

      // Number of items shown initially
      'initialItems' => 1,

      // Text for "Add" button
      'addButtonText' => 'Add another',

      // Text for "Remove" button
      'removeButtonText' => 'Remove',

      // CSS selector for hidden field where JSON is stored
      'hiddenInputSelector' => '.cards-list-hidden',

      // Prefix used for validation message classes
      'validationPrefix' => 'cards-list',

      // Prevent adding new item until current fields are valid
      'validateBeforeAdd' => true,

      // Custom ID for add button
      'addButtonId' => 'cards-list-add-button',

      // CSS classes for add button
      'addButtonClass' => ['btn', 'btn-primary', 'cards-list-add'],

      // Custom ID for remove button
      'removeButtonId' => 'cards-list-remove-button',

      // CSS classes for remove button
      'removeButtonClass' => ['btn', 'btn-danger', 'cards-list-remove'],

      // Store values as JSON (default true)
      'storeAsJson' => true,

      // Default values (useful for edit forms)
      'fieldGroupDefaultValue' => [],

      // Fields definition
      'fields' => [

        [
          'type' => 'text', // Input type
          'name' => 'title', // Field key in JSON
          'label' => 'Title', // Label shown in UI
          'placeholder' => 'Enter title', // Placeholder text
          'required' => true, // Mandatory field
          'requiredMessage' => 'Title is required.', // Error message
        ],

        [
          'type' => 'textarea', // Multi-line input
          'name' => 'description',
          'label' => 'Description',
          'placeholder' => 'Enter description',
          'required' => true,
          'requiredMessage' => 'Description is required.',

          // Enable CKEditor 5
          'editor' => 'ckeditor5',

          // Text format to use
          'editorFormat' => 'basic_html',
        ],

        [
          'type' => 'file', // File upload field
          'name' => 'icon',
          'label' => 'Icon',

          // Allow only single file
          'multiple' => false,

          // Allowed file types
          'accept' => 'png,jpg,jpeg,webp,svg,gif',

          'required' => true,
          'requiredMessage' => 'Icon is required.',

          // Drupal file system path
          'upload_location' => 'public://background-images/',
        ],

        [
          'type' => 'text',
          'name' => 'icon_alt_text',
          'label' => 'Icon alt text',
          'placeholder' => 'Enter icon alt text',
          'required' => true,
          'requiredMessage' => 'Icon alt text is required.',
        ],

        [
          'type' => 'file',
          'name' => 'image',
          'label' => 'Image',
          'multiple' => false,
          'accept' => 'png,jpg,jpeg,webp,svg,gif',
          'required' => true,
          'requiredMessage' => 'Image is required.',
          'upload_location' => 'public://background-images/',
        ],

        [
          'type' => 'text',
          'name' => 'image_alt_text',
          'label' => 'Image alt text',
          'placeholder' => 'Enter image alt text',
          'required' => true,
          'requiredMessage' => 'Image alt text is required.',
        ],

        [
          'type' => 'textfield', // Alias of text (optional usage)
          'name' => 'badge_text',
          'label' => 'Badge text',
          'placeholder' => 'Enter badge text',
          'required' => false,
        ],
      ],
    ]),
  ],
];


// Hidden field to store JSON data
$section['cards_wrapper']['cards_json'] = [
  '#type' => 'hidden', // Hidden input field
  '#default_value' => '', // Default empty value
  '#attributes' => [
    'class' => ['cards-list-hidden'], // Must match hiddenInputSelector
  ],
];

💡 Tip: Ensure that the .cards-list-hidden selector matches the hiddenInputSelector value, otherwise data will not be stored correctly.


Conclusion

Custom Paragraphs provides a powerful and flexible way to build dynamic forms without complex backend logic. It is lightweight, developer-friendly, and highly customizable.

Related Posts

Comments

Chatbot Avatar
Hapus Infotech
We typically reply in a few minutes.
×