Create a custom field formatter

Last updated on
15 August 2023

The field formatter formats the field data to be viewed by the end user. Field formatters are defined as plugins, so it's a good idea to familiarize yourself with the Plugin API before diving into writing a new field formatter.

Field formatter class

File: /modules/random/src/Plugin/Field/FieldFormatter/RandomDefaultFormatter.php

<?php

namespace Drupal\random\Plugin\Field\FieldFormatter;

use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;

/**
 * Plugin implementation of the 'Random_default' formatter.
 *
 * @FieldFormatter(
 *   id = "random_default",
 *   label = @Translation("Random text"),
 *   field_types = {
 *     "random"
 *   }
 * )
 */
class RandomDefaultFormatter extends FormatterBase {

  /**
   * {@inheritdoc}
   */
  public function settingsSummary() {
    $summary = [];
    $summary[] = $this->t('Displays the random string.');
    return $summary;
  }

  /**
   * {@inheritdoc}
   */
  public function viewElements(FieldItemListInterface $items, $langcode) {
    $element = [];

    foreach ($items as $delta => $item) {
      // Render each element as markup.
      $element[$delta] = ['#markup' => $item->value];
    }

    return $element;
  }

}

Paragraphs module

If you are using Paragraphs module, you can access all the fields in the paragraph. For example, create a summary:

  /**
   * {@inheritdoc}
   */
  public function viewElements(FieldItemListInterface $items, $langcode) {
    // Make an array of column names.
    $column_names = [];
    foreach ($items as $item) {
      $column_names[] = $item->entity->field_column_name->value;
    }
    // Theme a list of the column names.
    $list = [
      '#theme' => 'item_list',
      '#list_type' => 'ul',
      '#items' => $column_names,
      '#empty' => $this->t('None'),
    ];
    // Create the element.
    $element = [];
    $element[] = $list;
    return $element;
  }

Formatter Settings

If your formatter needs custom display settings, there are three steps required to achieve this:

  1. Override PluginSettingsBase::defaultSettings() in order to set the defaults
  2. Create the configuration schema for the settings you've created
  3. Create a form to allow users to change the settings

Step 1: Override PluginSettingsBase::defaultSettings() in order to set the defaults

/**
 * {@inheritdoc}
 */
public static function defaultSettings() {
  return [
    // Declare a setting named 'text_length', with
    // a default value of 'short'
    'text_length' => 'short',
  ] + parent::defaultSettings();
}

Step 2: Create the configuration schema for the settings you've created

Configuration schema goes in the following file:

[MODULE ROOT]/config/schema/[MODULE_NAME].schema.yml

In this file, you will describe the data type(s) of the setting(s) you've created in defaultSettings():

Step 1 created a setting named 'text_length', that stores a string value. The schema for this would look like the following:

field.formatter.settings.[FORMATTER ID]:
  type: mapping
  label: 'FORMATTER NAME text length'
  mapping:
    text_length:
      type: string
      label: 'Text Length'

Step 3: Create a form to allow users to change the settings

The form to allow users to change the values for the settings is created by overriding FormatterBase::settingsForm()

Remember to add the form state namespace to the top of the PHP file.

use Drupal\Core\Form\FormStateInterface;
/**
 * {@inheritdoc}
 */
public function settingsForm(array $form, FormStateInterface $form_state) {
  $form = parent::settingsForm($form, $form_state);

  $form['text_length'] = [
    '#title' => $this->t('Text length'),
    '#type' => 'select',
    '#options' => [
      'short' => $this->t('Short'),
      'long' => $this->t('Long'),
    ],
    '#default_value' => $this->getSetting('text_length'),
  ];

  return $form;
}

Using #ajax in settings forms

Using #ajax in settings forms is not straightforward, as the form snippet created in settingsForm() is not at the root of the form, but rather nested deeply. In the example below, the form has two settings: display_type, which can be either 'label' or 'entity', and entity_display_mode, which can be either 'full' or 'teaser'. The entity display mode is only shown when the display_type is set to 'entity'.

public function settingsForm(array $form, FormStateInterface $form_state) {
  $form['display_type'] = [
    '#title' => $this->t('Display Type'),
    '#type' => 'select',
    '#options' => [
      'label' => $this->t('Label'),
      'entity' => $this->t('Entity'),
    ],
    '#default_value' => $this->getSetting('display_type'),
    '#ajax' => [
      'wrapper' => 'private_message_thread_member_formatter_settings_wrapper',
      'callback' => [$this, 'ajaxCallback'],
    ],
  ];

  $form['entity_display_mode'] = [
    '#prefix' => '<div id="private_message_thread_member_formatter_settings_wrapper">',
    '#suffix' => '</div>',
  ];

  // First, retrieve the field name for the current field]
  $field_name = $this->fieldDefinition->getItemDefinition()->getFieldDefinition()->getName();
  // Next, set the key for the setting for which a value is to be retrieved
  $setting_key = 'display_type';

  // Try to retrieve a value from the form state. This will not exist on initial page load
  if($value = $form_state->getValue(['fields', $field_name, 'settings_edit_form', 'settings', $setting_key])) {
    $display_type = $value;
  }
  // On initial page load, retrieve the default setting
  else {
    $display_type = $this->getSetting('display_type');
  }

  if($display_type == 'entity') {
    $form['entity_display_mode']['#type'] = 'select';
    $form['entity_display_mode']['#title'] = $this->t('View mode');
    $form['entity_display_mode']['#options'] = [
      'full' => $this->t('Full'),
      'teaser' => $this->t('Teaser'),
    ];
    $form['entity_display_mode']['#default_value'] = $this->getSetting('entity_display_mode');
  }
  else {
    // Force the element to render (so that the AJAX wrapper is rendered) even
    // When no value is selected
    $form['entity_display_mode']['#markup'] = '';
  }

  return $form;
}

Next, create the ajax callback, and return the relevant form element:

public function ajaxCallback(array $form, FormStateInterface $form_state) {
  $field_name = $this->fieldDefinition->getItemDefinition()->getFieldDefinition()->getName();
  $element_to_return = 'entity_display_mode';

  return $form['fields'][$field_name]['plugin']['settings_edit_form']['settings'][$element_to_return];
}

Dependency Injection in Field Formatters

Using dependency injection in field formatters requires three steps:

  1. Implement the ContainerFactoryPluginInterface interface
  2. Implement or override ContainerFactoryPluginInterface::create()
  3. Implement or override __construct()

1) Implement the ContainerFactoryPluginInterface interface

Most field formatters will extend the \Drupal\Core\Field\FormatterBase class provided by Drupal Core. If you're doing this, you can skip this step and move right on to step 2), because FormatterBase already implements ContainerFactoryPluginInterface for you.

use Drupal\Core\Plugin\ContainerFactoryPluginInterface;

class MyFormatter implements ContainerFactoryPluginInterface {

2) Implement or override ContainerFactoryPluginInterface::create()

This example injects the entity_type.manager service into the formatter.

use Symfony\Component\DependencyInjection\ContainerInterface;

public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
  return new static(
    $plugin_id,
    $plugin_definition,
    $configuration['field_definition'],
    $configuration['settings'],
    $configuration['label'],
    $configuration['view_mode'],
    $configuration['third_party_settings'],
    // Add any services you want to inject here
    $container->get('entity_type.manager')
  );
}

3) Implement or override __construct()

Implement __construct() and store the service in a property of the class. If your field formatter extends \Drupal\Core\Field\FormatterBase class provided by Drupal Core, make sure to call parent::__construct() as in the example below.

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;

/**
 * The entity type manager service
 *
 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
 */
protected EntityTypeManagerInterface $entityTypeManager;

/**
 * Construct a MyFormatter object.
 *
 * @param string $plugin_id
 *   The plugin_id for the plugin instance.
 * @param mixed $plugin_definition
 *   The plugin implementation definition.
 * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
 *   Defines an interface for entity field definitions.
 * @param array $settings
 *   The formatter settings.
 * @param string $label
 *   The formatter label display setting.
 * @param string $view_mode
 *   The view mode.
 * @param array $third_party_settings
 *   Any third party settings.
 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
 *   Entity type manager service.
 */
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager) {
  parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);

  $this->entityTypeManager = $entity_type_manager;
}

You can now use the entity type manager anywhere in your formatter class as $this->entityTypeManager.

Help improve this page

Page status: No known problems

You can: