Alright, so our field now also has a widget that users can input data with. We can already use this field if we want, but when viewing the nodes, we have no way of displaying the field data, unless we do some custom preprocessing and retrieve it manually as we’ve seen earlier in the book. So, let's instead create the default field formatter because even if we don't need one, it's still a good practice to have one in place to make the field whole.
Before actually coding it, let's establish what we want our formatter to look and behave like. By default, we want the license plate data to be rendered like this:
<span class="license-plate--code">{{ code }}</span> <span class="license-plate--number">{{ number }}</span>
So, each component is wrapped inside its own span tag, and some handy classes are applied to them. Alternatively, we may want to concatenate the two values together into one single span tag:
<span class="license-plate">{{ code }} {{ number }}</span>
This could be a setting on the formatter, allowing the user to choose the preferred output. So, let's do it then.
Field formatters go inside the Plugin/Field/FieldFormatter namespace of our module, so let's go ahead and create our own:
namespace Drupal\license_plate\Plugin\Field\FieldFormatter;
use Drupal\Component\Utility\Html;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'default_license_plate_formatter' formatter.
*
* @FieldFormatter(
* id = "default_license_plate_formatter",
* label = @Translation("Default license plate formatter"),
* field_types = {
* "license_plate"
* }
* )
*/
class DefaultLicensePlateFormatter extends FormatterBase {}
Again, we start by inspecting the annotation, which looks very unsurprising. It looks almost like the one for our widget earlier, as formatters can also be used on multiple field types.
The class extends FormatterBase, which itself implements the obligatory FormatterInterface. By now, you recognize the pattern used with plugins--they all have to implement an interface and typically extend a base class, which provides some helpful functionalities common to all plugins of those types. Fields are no different.
The first thing we do inside this formatter class is, again, deal with its own settings (if we need any)- and as it happens, we have a configurable setting for our formatter, so let's define it and provide a default value:
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'concatenated' => 1,
] + parent::defaultSettings();
}
This is just like with the previous plugins. The concatenated setting will be used to determine the output of this field according to the two options we talked about earlier.
Next, predictably, we will need the form to manage this setting:
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
return [
'concatenated' => [
'#type' => 'checkbox',
'#title' => t('Concatenated'),
'#description' => t('Whether to concatenate the code and number into a single string separated by a space. Otherwise the two are broken up into separate span tags.'),
'#default_value' => $this->getSetting('concatenated'),
]
] + parent::settingsForm($form, $form_state);
}
Again, nothing special; we have a checkbox, which we use to manage a Boolean value (represented by 1 or 0). Lastly, just like with the widget, we have a summary display for formatters as well that we can define:
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = [];
$summary[] = t('Concatenated: @value', ['@value' => (bool) $this->getSetting('concatenated') ? 'Yes' : 'No']);
return $summary;
}
Here, we just print in a human-readable name of whatever has been configured, and this will be displayed when managing the field display in the UI and will look just like it did with the widget. Consistency is nice.
Now, we've reached the most critical aspect of any field formatter--the actual display:
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
foreach ($items as $delta => $item) {
$elements[$delta] = $this->viewValue($item);
}
return $elements;
}
/**
* Generate the output appropriate for one field item.
*
* @param \Drupal\Core\Field\FieldItemInterface $item
* One field item.
*
* @return array
*/
protected function viewValue(FieldItemInterface $item) {
$code = $item->get('code')->getValue();
$number = $item->get('number')->getValue();
return [
'#theme' => 'license_plate',
'#code' => $code,
'#number' => $number,
'#concatenated' => $this->getSetting('concatenated')
];
}
The method used for this is viewElements(), but for each element in the list, we simply delegate the processing to a helper method, because as you remember, the field is itself a list of value items (depending on the field cardinality), even if there is only one value in the field. These are keyed by a delta, which we also use to key the array of $elements that we return from the method.
For each individual item in the list, we then retrieve the value of the license plate code and number using the TypedData accessors we've seen earlier. Remember that at this point we are working with a FieldItemInterface whose get() method returns the DataType plugin that represents the actual value, which, in our case, is StringData, because that is what our field property definitions were:
$properties['number'] = DataDefinition::create('string')
->setLabel(t('Plate number'));
Also, the actual value inside these plugins are the string representations the user actually provided. We use these values together with the setting on whether to concatenate and pass them to a custom theme function (we have yet to define this). The important thing to keep in mind is that what we need to return, for each item, is a render array. This can be anything; consider the following example:
return [ '#markup' => $code . ' ' . $number, ];
However, that doesn't look nice, nor is configurable or overridable. So, we opt for a clean new theme function that takes those three arguments:
/**
* Implements hook_theme().
*/
function license_plate_theme($existing, $type, $theme, $path) {
return [
'license_plate' => [
'variables' => ['code' => NULL, 'number' => NULL, 'concatenated' => TRUE],
],
];
}
We default the value for concatenated to TRUE because that is what we set the default value to be inside defaultSettings(). So, we have to be consistent. The template file that goes with this, license-plate.html.twig, is also very simple:
{% if concatenated %}
<span class="license-plate">{{ code }} {{ number }}</span>
{% else %}
<span class="license-plate--code">{{ code }}</span> <span class="license-plate--number">{{ number }}</span>
{% endif %}
Depending on our setting, we output the markup differently. Other modules and themes now have a host of options to alter this output:
- They can create a new formatter plugin altogether
- They can override the template inside a theme
- They can alter the template to be used by this theme hook
That's it for the formatter plugin itself, but this time we're not forgetting about the configuration schema. Although we have a measly little Boolean value to define, it still needs to be done:
field.formatter.settings.default_license_plate_formatter:
type: mapping
label: 'Default license plate formatter settings'
mapping:
concatenated:
type: boolean
label: 'Whether to concatenate the two fields into one single span tag'
This works the same way as the other ones but with a different prefix--field.formatter.settings.
With that, we have our field formatter in the bag. The only thing left is for site builders to create fields of this type and start using it.
However, I still think we can do one better. Since we are working with license plates that deal with certain known formats, what if we make our field configurable to provide a list of license plate codes that can be used when inputting the data? This will have the added benefit of us learning something new about fields--field settings.