Create a custom field formatter

Last updated on
9 April 2017

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 in to writing a new field type.

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 = array();
    $settings = $this->getSettings();

    $summary[] = t('Displays the random string.');

    return $summary;
  }

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

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

    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 for users to change the values for the settings is created by overriding FormatterBase::settingsForm()

/**
 * {@inheritdoc}
 */
public function settingsForm(array $form, FormStateInterface $form_state) {
  $element['text_length'] = [
    '#title' => t('Text length'),
    '#type' => 'select',
    '#options' => [
      'short' => $this->t('Short'),
      'long' => $this->t('Long'),
    ],
    '#default_value' => $this->getSetting('text_length'),
  ];

  return $element;
}

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) {
  $element['display_type'] = [
    '#title' => 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'],
    ],
  ];

  $element['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') {
    $element['entity_display_mode']['#type'] = 'select';
    $element['entity_display_mode']['#title'] = $this->t('View mode');
    $element['entity_display_mode']['#options'] = [
      'full' => $this->t('Full'),
      'teaser' => $this->t('Teaser'),
    ];
    $element['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
    $element['entity_display_mode']['#markup'] = '';
  }

  return $element;
}

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 ContainerFactoryPluginInterface::create()
  3. Override FormatterBase::__construct()

1) Implement the ContainerFactoryPluginInterface interface

use Symfony\Component\DependencyInjection\ContainerInterface;

class MyFormatter extends FormatterBase implements ContainerFactoryPluginInterface {

2) Implement ContainerFactoryPluginInterface::create()

This example injects the entity.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.manager')
  );
}

3) Override FormatterBase::__construct()

Override __construct() on FormatterBase, making sure to call parent::__construct(), then store the service in a property of the class

/**
 * The entity manager service
 *
 * @var \Drupal\Core\Entity\EntityManagerInterface
 */
protected $entityManager;

/**
 * Construct a MyFormatter object
 *
 * @param \Drupal\Core\Entity\EntityManagerInterface $entityManager
 *   The entity manager service
 */
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, EntityManagerInterface $entityManager) {
  parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);

  $this->entityManager = $entityManager;
}

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