Thanks for taking the time to read this ebook! This is something that I started writing towards the tail-end of 2020, when I was working with Drupal 8 and struggling with a lack of documentation around widgets and the widget API. Since then I’ve moved on to work with Ruby and other non-Drupal technologies, and it doesn’t look likely I’ll be spending time on this in the future, so instead of leaving it to rot in a private repo I’m publishing it here under an LGPL license and making the GitHub repository public.
My hope is that at the least someone will find it a useful reference, even in its partially-complete state. If you would like to contribute, I’m happy to accept PRs and merge in changes to round out the sections I haven’t totally fleshed out.
If you do find this useful, you can express your appreciation by buying me a coffee (or several).
Enjoy!
1. Preface
I’ve been developing for Drupal 8 for about three years now and, whilst it’s been a marked improvement from Drupal 7 in many ways, I’ve often found myself looking for documentation that doesn’t exist. After numerous expeditions into the Drupal source code I started documenting my findings in blog posts, partly for my own memory and partly to help others having the same issue.
At a certain point this felt like it needed a more thorough treatment—how many others were struggling through the core APIs and being held back by the lack of comprehensive documentation? Whilst there are lots of great ebooks already out there, most cover development at a high level rather than diving into a particular part of the API in detail.
This is my aim with this publication—to not only give you the information you need to create a widget, but also to explore the API in enough detail that you’re comfortable venturing outside the limits of the code herein to create your own, more advanced, widgets with confidence.
So come along with me, on a journey through the widget API and beyond.
I should note at this point that this was written (and all code was tested) against Drupal 8.9.x, which is the current Drupal version at the time of writing. However, I’m confident that this code will be equally applicable to Drupal 9 given the widget API has not changed—the WidgetInterface
is exactly the same, and WidgetBase
has lost a single trait.
2. What are Widgets?
I’m assuming that if you’re here you already have at least some idea of what a widget is and why you want to make one—if that’s the case, feel free to skip to the next chapter. If not, have a read.
Widgets are part of the triumvirate of core field Plugins, formed of FieldType
, FieldFormatter
, and FieldWidget
. The roles of these plugins are as follows:
2.1. FieldType
A FieldType
defines the structure and schema of a particular type of field—for example, a text or date field. It doesn’t know anything about how you enter the information for the field, or how it looks on the front-end, just how it stores and processes data.
2.2. FieldFormatter
A FieldFormatter
defines the front-end appearance for one or more
FieldTypes
. You can have more than one formatter for a type, and if they’re similar enough a formatter can span multiple types (although most only cater to a single type). The contract between the formatter and type is the data structure the type defines - the formatter then takes the data the field returns when being displayed and performs the necessary operations to show it to the user (escaping data, wrapping it in a theme template, etc.).
2.3. FieldWidget
A FieldWidget
defines the back-end interface for one or more
FieldTypes
. This is the form that an admin user will edit the field with, and as with FieldFormatters
a type often has multiple widgets available. These can be as simple as a single text input for a plain-text field, or as complex as a JavaScript-drivenz widget for a date repeat field.
Ultimately the contract that the widget has with the type is the same as the formatter—the structure of the data that the type expects to save in the database. This means that, whatever the front-end interface of the widget, it must return data in the same format that the type defines to be able to save it into the database (and subsequently return it for display with the formatter)
So the reason to make a widget might be that you’re defining a new field type and you want to give users a way to enter data or, more commonly, you want to add a new way for a user to interact with an existing field type. You also might not be defining an entirely new widget, and instead you just want to alter the behaviour of an existing widget (or widgets). Don’t worry - we’ll cover that too.
So that’s the use-case for this ebook. Now carry on to find out how to do it!
3. A basic Widget
So let’s start getting into the interesting bits first—how to make a widget. The code below is written against Drupal 8, but since the API hasn’t changed in Drupal 9 at the time of writing I’d expect it all to be forwards-compatible. If you run into any issues, see the errata section for more information.
Widgets are an example of a Drupal Plugin, which means we need to create and annotate a particular class. To get started we’re going to use Drupal console to generate a module, since the topic of this book isn’t creating modules. We could also create the FieldWidget
using Drupal Console, but at this stage I’d prefer to walk through the process so that you’re aware of all of the steps—later on I’ll demonstrate how to do this more quickly with Drupal Console.
To get started, let’s generate a module to add our widget to:
> ddev drupal generate:module
// Welcome to the Drupal module generator
Enter the new module name:
> Example widget
Enter the module machine name [example_widget]:
>
Enter the module Path [modules/custom]:
>
Enter module description [My Awesome Module]:
> A module for my example widget
Enter package name [Custom]:
>
Enter Drupal Core version [8.x]:
>
Do you want to generate a .module file? (yes/no) [yes]:
> yes
Define module as feature (yes/no) [no]:
> no
Do you want to add a composer.json file to your module? (yes/no) [yes]:
> no
Would you like to add module dependencies? (yes/no) [no]:
> yes
Module dependencies separated by commas (i.e. context, panels):
> text
Do you want to generate a unit test class? (yes/no) [yes]:
> no
Do you want to generate a themeable template? (yes/no) [yes]:
> no
Do you want proceed with the operation? (yes/no) [yes]:
> yes
This will generate a basic module structure like this:
web/modules/custom/
└── example_widget
├── example_widget.info.yml
└── example_widget.module
We also want to create the directories our Plugin code will live in. The Drupal autoloader will look for classes in the src
directory of modules, so that will be our first level. Next the FieldWidget is a Plugin
, and it’s a Field
plugin of type FieldWidget
, so we end up with this structure:
web/modules/custom/
└── example_widget/
├── example_widget.info.yml
├── example_widget.module
└── src
└── Plugin
└── Field
└── FieldWidget
Inside this directory we can then create our plugin file. Let’s call our widget class ExampleTextFieldWidget
, so for compatibility with PSR-4 we need the file to have the same name. Inside that file we can create our widget plugin class;
<?php
namespace Drupal\example_widget\Plugin\Field\FieldWidget;
use Drupal\Core\Field\WidgetBase;
class ExampleTextFieldWidget extends WidgetBase {
}
To let Drupal recognise this as a plugin we have to add the appropriate annotation:
<?php
namespace Drupal\example_widget\Plugin\Field\FieldWidget;
use Drupal\Core\Field\WidgetBase;
/**
* Plugin implementation of the 'example_text_field_widget' widget.
*
* @FieldWidget(
* id = "example_text_field_widget",
* module = "example_widget",
* label = @Translation("Example text field widget"),
* field_types = {
* "string"
* }
* )
*/
class ExampleTextFieldWidget extends WidgetBase {
}
We also have to implement the only WidgetInterface
method that isn’t covered by WidgetBase
--formElement()
:
<?php
namespace Drupal\example_widget\Plugin\Field\FieldWidget;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Field\FieldItemListInterface;
/**
* Plugin implementation of the 'example_text_field_widget' widget.
*
* @FieldWidget(
* id = "example_text_field_widget",
* module = "example_widget",
* label = @Translation("Example text field widget"),
* field_types = {
* "string"
* }
* )
*/
class ExampleTextFieldWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state): array {
return [];
}
}
This is now a valid enough Widget plugin that you can enable it for a field—enable the module with ddev drush moi example_widget
(or ddev drush en example_widget
) if you’re using an older version of Drush, and create a new Text (plain) field on a node bundle. On the Manage Form Display tab you will see your new Widget available as Example text field widget--you can enable it if you want, but right now you won’t see anything output if you edit the node. Let’s deal with that next.
Since this is a widget for a text field, we want to add some kind of element we can enter text in. Let’s create a simple textfield:
<?php
namespace Drupal\example_widget\Plugin\Field\FieldWidget;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Field\FieldItemListInterface;
/**
* Plugin implementation of the 'example_text_field_widget' widget.
*
* @FieldWidget(
* id = "example_text_field_widget",
* module = "example_widget",
* label = @Translation("Example text field widget"),
* field_types = {
* "string"
* }
* )
*/
class ExampleTextFieldWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state): array {
$element['value'] = [
'#type' => 'textfield',
'#title' => $this->t('Text'),
'#default_value' => $items[$delta]->value ?? '',
];
return $element;
}
}
If you now edit the field you created that uses this widget, you’ll be able to edit and save text, but it doesn’t show up in the field when you go to edit it (although it will appear on the front-end). We need to add a default value:
<?php
namespace Drupal\example_widget\Plugin\Field\FieldWidget;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Field\FieldItemListInterface;
/**
* Plugin implementation of the 'example_text_field_widget' widget.
*
* @FieldWidget(
* id = "example_text_field_widget",
* module = "example_widget",
* label = @Translation("Example text field widget"),
* field_types = {
* "string"
* }
* )
*/
class ExampleTextFieldWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state): array {
$element['value'] = [
'#type' => 'textfield',
'#title' => $this->t('Text'),
'#default_value' => $items[$delta]->value ?? '',
];
return $element;
}
}
Now you can edit the page, give the field a value, save it, and see it reflected in your widget to edit again. Congratulations—you’ve made your first FieldWidget
!
4. Adding an AJAX button
Because widget forms are standalone elements of a larger form, the general method of interacting with them using buttons is via AJAX actions. It is possible to add handlers to the parent form, but we then have to deal with the whole form array rather than the specific part we’re interested in for the widget, and isn’t a standard way of interacting with them in core.
Before doing any more on this, it’s worth a quick recap of the form AJAX API, since it’s something that not everyone uses a lot (myself included).
AJAX events can be added to any element that creates an event, with the general format:
'#ajax' => [
'callback' => '::myAjaxCallback', // don't forget :: when calling a class method.
//'callback' => [$this, 'myAjaxCallback'], //alternative notation
'disable-refocus' => FALSE, // Or TRUE to prevent re-focusing on the triggering element.
'event' => 'change',
'wrapper' => 'edit-output', // This element is updated with this AJAX callback.
'progress' => [
'type' => 'throbber',
'message' => $this->t('Verifying entry...'),
],
]
See the AJAX form API documentation for a full list of properties that can be applied to an #ajax
attribute.
Note that an #ajax
attribute can only have a single callback, unlike a property like #submit
where we can attach an array of callbacks. All other callback notation applies though, so we can either attach the callback to a static class method, a method on an existing object instance, or a regular function.
We pick the event that invokes the callback, along with some properties of the AJAX response - for example, whether the element is refocussed after the callback is executed, what the progress indicator is, and what ID the element to update has.
The callback function is expected to return an AjaxResponse
, which contains one or more commands - see the AJAX commands documentation for a full list of the available commands. These can replace HTML, add CSS, display an alert, and many other common AJAX actions. Alternatively, you can create your own command plugin that implements the CommandInterface
, a subject which is beyond the scope of this guide.
Essentially an AjaxResponse
is a way of packaging a set of pre-defined configurable JavaScript actions to be executed in the DOM.
Let’s add a basic button that tells us how long the text in the field is. First we need to add a button to the output of formElement()
:
$element['value'] = [
'#type' => 'textfield',
'#title' => $this->t('Text'),
'#default_value' => $items[$delta]->value ?? '',
];
$element['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Check length'),
]
return $element;
Now we have a button, but it doesn’t do much. Let’s give it an AJAX handler:
$element['value'] = [
'#type' => 'textfield',
'#title' => $this->t('Text'),
'#default_value' => $items[$delta]->value ?? '',
];
$element['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Check length'),
'#ajax' => [
'callback' => [$this, 'doAjax'],
'progress' => [
'type' => 'throbber',
'message' => $this->t('Counting characters...'),
],
],
];
return $element;
Rather than add a callback like [static::class, 'doAjax']
where I’d have to use a static class, I’ve used $this
so that I can call out to $this→t()
within my method. We can’t invoke the method using the double colon syntax like ['::doAjax']
because it would get invoked on the form object, and the method is attached to the Widget class.
Now we also need to add our AJAX callback method to the class:
public function doAjax(array $form, FormStateInterface $form_state) {
}
AJAX handlers must return an AjaxResponse
with one or more AJAX commands to execute actions on the front-end. Let’s add a simple OpenModalDialogCommand
to show us the field count:
public function doAjax(array $form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$parents = array_slice($triggering_element['#parents'], 0, -1);
$value = $form_state->getValue(array_merge($parents, ['value']));
$message = $this->t('The message is @count characters long', ['@count' => mb_strlen($value)]);
$response = new AjaxResponse();
$response->addCommand(new OpenModalDialogCommand($this->t('Message count'), $message));
return $response;
}
We also need to add the dialog library as an attachment to our form:
$element['#attached']['library'][] = 'core/drupal.dialog.ajax';
This now shows us an alert with the number of characters in the field.
Our complete widget code is now:
<?php
namespace Drupal\example_widget\Plugin\Field\FieldWidget;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'example_text_field_widget' widget.
*
* @FieldWidget(
* id = "example_widget_basic_widget",
* module = "example_widget",
* label = @Translation("Basic text field widget"),
* field_types = {
* "string"
* }
* )
*/
class BasicWidgetExample extends WidgetBase {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state): array {
$element['value'] = [
'#type' => 'textfield',
'#title' => $this->t('Text'),
'#default_value' => $items[$delta]->value ?? '',
];
$element['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Check length'),
'#ajax' => [
'callback' => [$this, 'doAjax'],
'progress' => [
'type' => 'throbber',
'message' => $this->t('Counting characters...'),
],
],
];
$element['#attached']['library'][] = 'core/drupal.dialog.ajax';
return $element;
}
/**
* AJAX handler.
*
* @param array $form
* The form array.
* @param FormStateInterface $form_state
* The form state.
*
* @return AjaxResponse
* A series of commands to be executed.
*/
public function doAjax(array $form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$parents = array_slice($triggering_element['#parents'], 0, -1);
$value = $form_state->getValue(array_merge($parents, ['value']));
$message = $this->t('The message is @count characters long', ['@count' => mb_strlen($value)]);
$response = new AjaxResponse();
$response->addCommand(new OpenModalDialogCommand($this->t('Message count'), $message));
return $response;
}
}
That’s a basic example of a widget AJAX button with callback. All we had to add was:
-
A
submit
element with an#ajax
callback -
A corresponding method on the Widget class
-
An
AjaxResponse
containing one or more AJAX commands
5. Settings
Now we have a basic widget that offers character counts for our field. But what if we want to make the widget configurable? Maybe sometimes we want to have character counts for a field, and other times we want word counts.
This is where some of the methods that are implemented on WidgetBase
will need to be overridden. To add basic settings we need four things:
-
A settings schema to be used by the form
-
A default value for the setting
-
A form to change the setting for the field
-
A summary of the settings to show on the field admin page
Let’s implement these in order.
To create a schema for our widget settings, we first need to create a configuration schema file for our module. In example_widget/config/schema
, create a file called example_widget.schema.yml
. As per the widget documentation we then need to create a schema for our widget of the format field.widget.settings.[WIDGET ID]
:
field.widget.settings.example_widget_basic_widget:
type: mapping
label: 'Example basic field widget settings'
mapping:
count_type:
type: text
label: 'Count type'
This creates a basic schema with a text field corresponding to our settingm which we’ve called count_type
. If you’re interested in more of the specifics on the naming of this schema, or how it gets used, see the advanced settings section.
Next the default settings. This uses, unsurprisingly, the defaultSettings()
method of WidgetInterface
. Let’s assume we have two types of count available, 'letter' and 'word':
/**
* {@inheritdoc}
*/
public static function defaultSettings(): array {
return [
'count_type' => 'letter',
] + parent::defaultSettings();
}
Here we override the defaultSettings
method, adding a new default settings value called 'count_type'
in addition to the settings values from the parent field—in our case not entirely necessary since the implementation of defaultSettings
on WidgetBase
is blank, but important if we were extending another text field widget which offers some additional settings.
Now we need to add a form to allow the administrator to manipulate our settings. Since we have a setting that has a set of fixed values, of which we need the user to pick one, let’s go with a select
element:
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state): array {
$elements = [];
$elements['count_type'] = [
'#type' => 'select',
'#title' => t('Count type'),
'#default_value' => $this->getSetting('count_type'),
'#options' => [
'letter' => $this->t('Letter'),
'word' => $this->t('Word'),
],
];
return $elements;
}
Now we have something that’s visible in the admin interface—you can edit the node bundle that your field is attached to and toggle this setting using the new select box:
However, you still can’t see the value of this setting in the settings summary, so finally let’s add a settingsSummary
method to our widget class:
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = parent::settingsSummary();
$summary[] = $this->t('Shows a @count_type count', ['@count_type' => $this->getSetting('count_type')]);
return $summary;
}
Now we can edit our setting on the Manage form display tab and update the stored value, have it saved in the form config, and see the settings summary updated with our selected value:
6. Generating widgets with Drupal console
Now we can get started by creating a module and a widget with console generation commands. If you haven’t used Drupal Console before, it gives you a bunch of scaffolding commands to generate basic plugins and components, which saves a lot of time when you’re getting up and running
(although sometimes what it generates isn’t ideal). You can see all of these commands by running drupal list generate
:
$ drupal list generate Drupal Console Launcher version 1.9.4 Drupal Console version 1.9.4 Available commands for the "generate" namespace: generate:ajax:command (gac) Generate & Register a custom ajax command generate:authentication:provider (gap) Generate an Authentication Provider generate:block:type (gbt) Generate a block content type generate:breakpoint (gb) Generate breakpoint generate:cache:context (gcc) Generate a cache context ...
This will give you a long list of all of the available generate commands. The two we’re interested in right now are generate:module
and generate:plugin:fieldwidget
.
To get started, let’s generate a module to add our Widget to:
> ddev drupal generate:module
// Welcome to the Drupal module generator
Enter the new module name:
> Example widget
Enter the module machine name [my_module]:
>
Enter the module Path [modules/custom]:
>
Enter module description [My Awesome Module]:
> A module for my example widget
Enter package name [Custom]:
>
Enter Drupal Core version [8.x]:
>
Do you want to generate a .module file? (yes/no) [yes]:
> yes
Define module as feature (yes/no) [no]:
> no
Do you want to add a composer.json file to your module? (yes/no) [yes]:
> no
Would you like to add module dependencies? (yes/no) [no]:
> yes
Module dependencies separated by commas (i.e. context, panels):
> text
Do you want to generate a unit test class? (yes/no) [yes]:
> no
Do you want to generate a themeable template? (yes/no) [yes]:
> no
Do you want proceed with the operation? (yes/no) [yes]:
> yes
This will generate a basic module structure like this:
web/modules/custom/
└── example_widget
├── example_widget.info.yml
└── example_widget.module
We can then generate our base widget:
web/modules/custom/
└── example_widget
├── example_widget.info.yml
└── example_widget.module
├── example_widget.schema.yml
└── src
└── Plugin
└── Field
└── FieldWidget
└── ExampleWidgetFieldWidget.php (1)
-
The structure of these directories and the associated namespace is important for this to work.
This creates both a widget plugin class, and a schema.yml
file. Let’s take a look at what’s in these.
<?php
namespace Drupal\example_widget\Plugin\Field\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'example_widget_field_widget' widget.
*
* @FieldWidget(
* id = "example_widget_field_widget",
* module = "my_module",
* label = @Translation("Example widget field widget"),
* field_types = {
* "string"
* }
* )
*/
class ExampleWidgetFieldWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'size' => 60,
'placeholder' => '',
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$elements = [];
$elements['size'] = [
'#type' => 'number',
'#title' => t('Size of textfield'),
'#default_value' => $this->getSetting('size'),
'#required' => TRUE,
'#min' => 1,
];
$elements['placeholder'] = [
'#type' => 'textfield',
'#title' => t('Placeholder'),
'#default_value' => $this->getSetting('placeholder'),
'#description' => t('Text that will be shown inside the field until a value is entered. This hint is usually a sample value or a brief description of the expected format.'),
];
return $elements;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = [];
$summary[] = t('Textfield size: @size', ['@size' => $this->getSetting('size')]);
if (!empty($this->getSetting('placeholder'))) {
$summary[] = t('Placeholder: @placeholder', ['@placeholder' => $this->getSetting('placeholder')]);
}
return $summary;
}
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element['value'] = $element + [
'#type' => 'textfield',
'#default_value' => isset($items[$delta]->value) ? $items[$delta]->value : NULL,
'#size' => $this->getSetting('size'),
'#placeholder' => $this->getSetting('placeholder'),
'#maxlength' => $this->getFieldSetting('max_length'),
];
return $element;
}
}
7. How do widgets work?
Our widget is an implementation of the FieldWidget
plugin which, if you’ve used plugins before, should look pretty familiar. First we have the annotation that defines the plugin:
/**
* Plugin implementation of the 'example_widget_field_widget' widget.
*
* @FieldWidget( (1)
* id = "example_widget_field_widget",
* module = "example_widget",
* label = @Translation("Example widget field widget"),
* field_types = {
* "string"
* }
* )
*/
-
For the full
@FieldWidget
annotation source, see \Drupal\Core\Field\Annotation\FieldWidget.
Here it’s given a unique ID (prefixed with the module name), the implementing module, a translatable (human-readable) label, and a list of field types that it supports using IDs from corresponding FieldType
plugins. Not included in this annotation are multiple_values
, which defines whether the widget handles multiple values at once (default FALSE
) and weight
which alters the sorting of the widget relative to other widgets during discovery (default NULL
).
It’s important to note that the plugin is also in a specific directory -
src/Plugin/Field/FieldWidget
. This is both a discovery mechanism, and a useful convention that makes it easy to see what, if any, widget plugins a module implements.
After the annotation, we get into some code:
class ExampleWidgetFieldWidget extends WidgetBase {
//...
}
The widget class extents WidgetBase
, which in turn implements
WidgetInterface
. The class hierarchy looks like this:
This doesn’t really tell us much about what we need to do to make a new widget. Let’s take a look at what we need to implement from WidgetBase.
If we create a new class that extends WidgetBase, the only method from the interface that we need to implement is formElement()
. As you can probably guess from the name, this lets us define the input form element for this field input. From WidgetInterface
:
/**
* Returns the form for a single field widget.
*
* Field widget form elements should be based on the passed-in $element, which
* contains the base form element properties derived from the field
* configuration.
*
* The BaseWidget methods will set the weight, field name and delta values for
* each form element. If there are multiple values for this field, the
* formElement() method will be called as many times as needed.
*
* Other modules may alter the form element provided by this function using
* hook_field_widget_form_alter() or
* hook_field_widget_WIDGET_TYPE_form_alter().
*
* The FAPI element callbacks (such as #process, #element_validate,
* #value_callback, etc.) used by the widget do not have access to the
* original $field_definition passed to the widget's constructor. Therefore,
* if any information is needed from that definition by those callbacks, the
* widget implementing this method, or a
* hook_field_widget[_WIDGET_TYPE]_form_alter() implementation, must extract
* the needed properties from the field definition and set them as ad-hoc
* $element['#custom'] properties, for later use by its element callbacks.
*
* @param \Drupal\Core\Field\FieldItemListInterface $items
* Array of default values for this field.
* @param int $delta
* The order of this item in the array of sub-elements (0, 1, 2, etc.).
* @param array $element
* A form element array containing basic properties for the widget:
* - #field_parents: The 'parents' space for the field in the form. Most
* widgets can simply overlook this property. This identifies the
* location where the field values are placed within
* $form_state->getValues(), and is used to access processing
* information for the field through the getWidgetState() and
* setWidgetState() methods.
* - #title: The sanitized element label for the field, ready for output.
* - #description: The sanitized element description for the field, ready
* for output.
* - #required: A Boolean indicating whether the element value is required;
* for required multiple value fields, only the first widget's values are
* required.
* - #delta: The order of this item in the array of sub-elements; see $delta
* above.
* @param array $form
* The form structure where widgets are being attached to. This might be a
* full form structure, or a sub-element of a larger form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return array
* The form elements for a single widget for this field.
*
* @see hook_field_widget_form_alter()
* @see hook_field_widget_WIDGET_TYPE_form_alter()
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state);
We can take a few interesting facts from this method. The first is that we’re not dealing with a complete form here—we have an $element
which is what’s passed around to alter hooks and other methods. Although we have access to $form
and $form_state
, this isn’t like defining our own standalone form class where we have complete control over the parent form.
We can also see that we have access to a couple of variables which are more reminiscent of the field API than the form API--$items
and
$delta
. The comments tell us that $items
represents the default values for the field (i.e. the values that are currently saved), and
$delta
is the current value offset if we have multiple values in the field.
The reference to #field_parents
in the $element
is useful, and is something we’ll be coming back to in future interactions with widgets.
Since the widget form is part of an overall form, we often have to use the parent form elements to locate it in the $form_state
array for the purposes of retrieving information about the widget. The parents also let us know what field the widget is attached to at the point that we interact with the widget in an entity context, since the widget itself is entirely agnostic of any concrete field instance.
If we jump back to our scaffolded widget we can see a basic form is defined in formElement()
:
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element['value'] = $element + [
'#type' => 'textfield',
'#default_value' => isset($items[$delta]->value) ? $items[$delta]->value : NULL,
'#size' => $this->getSetting('size'),
'#placeholder' => $this->getSetting('placeholder'),
'#maxlength' => $this->getFieldSetting('max_length'),
];
return $element;
}
Most of this is standard form render element code—if the form side isn’t familiar to you I’d recommend taking a break here and giving the Form API documentation a read, particularly the section on Form Render Elements. Don’t worry, I’ll wait.
We can see the use of $items
here in conjunction with $delta
in the way we’d expect from the interface method comments—the relevant item is retrieved using the $delta
offset in $items
. The magic getter $items[$delta]->value
is used to retrieve the actual default value—it’s important to note that the use of value
here is not arbitrary and corresponds directly to the key name of the data in the schema (and since this is a simple widget, to the name of the form field used to set it). If we take a look at the core StringItem
type we see this schema()
method:
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return [
'columns' => [
'value' => [
'type' => $field_definition->getSetting('is_ascii') === TRUE ? 'varchar_ascii' : 'varchar',
'length' => (int) $field_definition->getSetting('max_length'),
'binary' => $field_definition->getSetting('case_sensitive'),
],
],
];
}
Remember earlier when I said the contract the FieldWidget
has with the
FieldType
is the schema? This is that contract in action.
For the rest of the form element attributes we’re getting our values from the getSetting()
method:
'#size' => $this->getSetting('size'),
'#placeholder' => $this->getSetting('placeholder'),
'#maxlength' => $this->getFieldSetting('max_length'),
This is what the schema and the rest of the boilerplate code is for—it manages a configuration object for this widget which allows setting and getting of configuration specific to the widget on this field instance.
The new settings defined for our widget are size
and placeholder
— max_length
is defined on the StringItem
class:
public static function defaultStorageSettings() {
return [
'max_length' => 255,
'is_ascii' => FALSE,
] + parent::defaultStorageSettings();
}
(N.B. you can see the is_ascii
setting is also defined here but doesn’t have an admin interface. This is generally defined as part of the field defaults, rather than being configurable).
So going back to our formElement
, we would expect the output of this to be a text input, with these attributes defined:
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element['value'] = $element + [
'#type' => 'textfield',
'#default_value' => isset($items[$delta]->value) ? $items[$delta]->value : NULL,
'#size' => $this->getSetting('size'),
'#placeholder' => $this->getSetting('placeholder'),
'#maxlength' => $this->getFieldSetting('max_length'),
];
return $element;
}
We can see the documentation for the textfield render element on Drupal.org, which outlines these properties. If we make a basic field in Drupal, we can see it outputs something like this:
<input
data-drupal-selector="edit-field-plain-text-test-0-value"
type="text"
id="edit-field-plain-text-test-0-value"
name="field_plain_text_test[0][value]"
value=""
size="60"
maxlength="255"
placeholder="This is placeholder text"
class="form-text" />
We have the size
, max_length
, and placeholder
attributes that are set on our widget, plus some other attributes generated from the name of this particular field instance.
So we’ve covered the formElement()
method, which was the only method we had to implement on our widget class extending WidgetBase
to satisfy WidgetInterface
, but what about the rest of the scaffolding that was generated by drupal console? The other methods that are set on our widget class are:
public static function defaultSettings()
public function settingsForm(array $form, FormStateInterface $form_state)
public function settingsSummary()
We also have an example_widget.schema.yml
file. Let’s take a look at that first, since that informs what these methods do with the settings:
field.widget.settings.example_widget_field_widget:
type: mapping
label: 'Example widget field widget widget settings'
mapping:
size:
type: integer
label: 'Size'
placeholder:
type: textfield
label: 'Placeholder'
We see here that a mapping
schema is defined with two new settings -
size
and placeholder
--which match what we see in our widget class.
If you’re not familiar with the Schema API now is a good time to have a quick browse of the documentation, but essentially what we have here is a dictionary data element which defines two named sub-elements: an integer called size
and a textfield called placeholder
.
These are defined in a specially-named schema
field.widget.settings.example_widget_field_widget
, following the pattern field.widget.settings.<widget_id>
. This isn’t ever invoked directly by the field widget code, and instead is part of the schema defined for the entity form display in core.entity.schema.yml
:
# Overview configuration information for form mode displays.
core.entity_form_display.*.*.*:
type: config_entity
label: 'Entity form display'
mapping:
...
content:
type: sequence
label: 'Field widgets'
sequence:
type: mapping
label: 'Field widget'
mapping:
...
settings:
type: field.widget.settings.[%parent.type]
label: 'Settings'
...
This means that you’ll see your widget settings exported in the appropriate core.entity_form_display.<entity>.<bundle>.<view_mode>
object for your entity form display, and you can debug the settings by querying the same object with drush cget
.
Once we know that these are the available settings, the rest of the methods in our widget class make more sense:
public static function defaultSettings() {
return [
'size' => 60,
'placeholder' => '',
] + parent::defaultSettings();
}
The defaultSettings()
method allows us to set default values for each of the settings when the widget is created—these are merged together with any parent values (which we can override at this stage if we’re extending an existing widget, such as with is_ascii
as mentioned above). This is also the value which will be returned by getSetting()
if no value is saved.
public function settingsForm(array $form, FormStateInterface $form_state) {
$elements = [];
$elements['size'] = [
'#type' => 'number',
'#title' => t('Size of textfield'),
'#default_value' => $this->getSetting('size'),
'#required' => TRUE,
'#min' => 1,
];
$elements['placeholder'] = [
'#type' => 'textfield',
'#title' => t('Placeholder'),
'#default_value' => $this->getSetting('placeholder'),
'#description' => t('Text that will be shown inside the field until a value is entered. This hint is usually a sample value or a brief description of the expected format.'),
];
return $elements;
}
The settingsForm()
then provides the form for administrators to manage these settings. We don’t need to set our own submission function for these, just make sure the elements have the same keys as those defined in the schema.
Finally we have the settingsSummary()
method:
public function settingsSummary() {
$summary = [];
$summary[] = t('Textfield size: @size', ['@size' => $this->getSetting('size')]);
if (!empty($this->getSetting('placeholder'))) {
$summary[] = t('Placeholder: @placeholder', ['@placeholder' => $this->getSetting('placeholder')]);
}
return $summary;
}
This defines the summary string that you see on the "Manage form display" page for an entity:
The one thing we don’t see for this form is any kind of validation and submission handling, but on the definition of settingsForm()
on
WidgetInterface
we see this comment:
* Invoked from \Drupal\field_ui\Form\EntityDisplayFormBase to allow
* administrators to configure the widget. The field_ui module takes care of
* handling submitted form values.
If we drop a breakpoint into our settingsForm()
method and take a look at the $form
, we can see the underlying form is the EntityFormDisplayEditForm
in the field_ui
module. This inherits from EntityDisplayFormBase
, which defines a submitForm()
and multistepSubmit()
method. The default submission handler from $form_state->getSubmitHandlers()
is ::multistepSubmit()
--the submitForm()
method is invoked when you save the entire form display using the "Save" button.
The class does not have a validation method, so no validation is carried out on the form by default. If you want to add your own submission or validation handlers, you can alter the $form_state
in the settingsForm()
method to add your own for the whole form, or add validation handlers on a per-element basis with the #element_validate
attribute.
So how do we add new handlers to the form submission? If we drop a breakpoint into the multistepSubmit
function we can see what the triggering element is, and by using #array_parents
figure out where it lives in the form array. If we do this for the title form element we get:
array (
0 => 'fields',
1 => 'title',
2 => 'plugin',
3 => 'settings_edit_form',
4 => 'actions',
5 => 'save_settings',
)
And if we look to the form we can see this is the "Update" button for the plugin.
To edit the rows we need to check what the available field definitions are on the form display. See EntityDisplayFormBase->form()
:
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
$field_definitions = $this->getFieldDefinitions();
$extra_fields = $this->getExtraFields();
// ... some code omitted ...
// Field rows.
foreach ($field_definitions as $field_name => $field_definition) {
$table[$field_name] = $this->buildFieldRow($field_definition, $form, $form_state);
}
// Non-field elements.
foreach ($extra_fields as $field_id => $extra_field) {
$table[$field_id] = $this->buildExtraFieldRow($field_id, $extra_field);
}
Can’t use this because getFieldDefinitions()
is protected…
Can we do something with the entity to cross-reference the fields with the form? Is this available on the form object to us?
8. How do settings work?
Our settings get loaded via HtmlEntityFormController
:
protected function getFormObject(RouteMatchInterface $route_match, $form_arg) {
// If no operation is provided, use 'default'.
$form_arg .= '.default';
list ($entity_type_id, $operation) = explode('.', $form_arg);
$form_object = $this->entityTypeManager->getFormObject($entity_type_id, $operation);
// Allow the entity form to determine the entity object from a given route
// match.
$entity = $form_object->getEntityFromRouteMatch($route_match, $entity_type_id);
$form_object->setEntity($entity);
return $form_object;
}
The entity is an instance of EntityFormDisplay
. The $entity_type_id
is entity_form_display
, and the route name is entity.entity_form_display.node.default
.
In EntityDisplayFormBase
we have:
public function getEntityFromRouteMatch(RouteMatchInterface $route_match, $entity_type_id) {
$route_parameters = $route_match->getParameters()->all();
return $this->getEntityDisplay($route_parameters['entity_type_id'], $route_parameters['bundle'], $route_parameters[$this->displayContext . '_mode_name']);
}
Where the values correspond to:
-
$route_parameters['entity_type_id']
is'node'
-
$route_parameters['bundle']
is'page'
-
$this→displayContext
is'form'
-
$route_parameters['form_mode_name']
is'default'
So the call literally resolves to:
$this->getEntityDisplay('node', 'page', 'default');
Let’s take a look at what EntityFormDisplayEditForm→getEntityDisplay()
does:
protected function getEntityDisplay($entity_type_id, $bundle, $mode) {
return $this->entityDisplayRepository->getFormDisplay($entity_type_id, $bundle, $mode);
}
Okay, that didn’t get us much further. How about EntityDisplayRepository→getFormDisplay()
public function getFormDisplay($entity_type, $bundle, $form_mode = self::DEFAULT_DISPLAY_MODE) {
$storage = $this->entityTypeManager->getStorage('entity_form_display');
// Try loading the entity from configuration; if not found, create a fresh
// entity object. We do not preemptively create new entity form display
// configuration entries for each existing entity type and bundle whenever a
// new form mode becomes available. Instead, configuration entries are only
// created when an entity form display is explicitly configured and saved.
$entity_form_display = $storage->load($entity_type . '.' . $bundle . '.' . $form_mode);
if (!$entity_form_display) {
$entity_form_display = $storage->create([
'targetEntityType' => $entity_type,
'bundle' => $bundle,
'mode' => $form_mode,
'status' => TRUE,
]);
}
return $entity_form_display;
}
Now we’re getting somewhere!
The first thing this does is load the 'entity_form_display'
configuration from the entityTypeManager storage. Then it loads a key based on the passed values for our type, bundle, and storage—in our case effectively loading entity_form_display.node.page.default
. And as we saw earlier in core.entity.schema.yml
that we have the pattern core.entity_form_display.*.*.*
defined for the base form schema.
What we end up with is a configuration entity with a schema corresponding to that build from core.entity_form_display.*.*.*
, where this wildcard name is resolved as;
[module].[config_entity_id].[entity_type_id].[bundle].[form_mode_name]
(Where form_mode_name
is basically the display mode).
The last part of this is in $storage->create()
, where we see a call which literally resolves to:
$route_match->getParameters()->all()
array (
'form_mode_name' => 'default',
'entity_type_id' => 'node',
'bundle' => 'page',
'node_type' =>
Drupal\node\Entity\NodeType::__set_state(array(
'type' => 'page',
'name' => 'Basic page',
'description' => 'Use <em>basic pages</em> for your static content, such as an \'About us\' page.',
'help' => '',
'new_revision' => true,
'preview_mode' => 1,
'display_submitted' => false,
'originalId' => 'page',
'status' => true,
'uuid' => '207d641a-0dc0-4f00-a7cd-f3bb867fe81c',
'isUninstalling' => false,
'langcode' => 'en',
'third_party_settings' =>
array (
),
'_core' =>
array (
'default_config_hash' => 'KuyA4NHPXcmKAjRtwa0vQc2ZcyrUJy6IlS2TAyMNRbc',
),
'trustedData' => false,
'entityTypeId' => 'node_type',
'enforceIsNew' => NULL,
'typedData' => NULL,
'cacheContexts' =>
array (
),
'cacheTags' =>
array (
),
'cacheMaxAge' => -1,
'_serviceIds' =>
array (
),
'_entityStorages' =>
array (
),
'dependencies' =>
array (
),
'isSyncing' => false,
)),
)
So the settings are resolved via the form, which has an attached ConfigEntity
. The schema for this is built dynamically based on the widgets included in the form using the special [%key]
and [%parent]
placeholders. These allow the schema to reference values from the include widgets.
If we look at some real exported config, we see:
...
content:
...
field_example_field:
weight: '1'
settings:
count_type: letter
third_party_settings: { }
type: example_widget_basic_widget
region: content
...
This from the template schema in core.entity.schema.yml
:
content:
type: sequence
label: 'Field widgets'
sequence:
type: mapping
label: 'Field widget'
mapping:
type:
type: string
label: 'Widget type machine name'
weight:
type: integer
label: 'Weight'
region:
type: string
label: 'Region'
settings:
type: field.widget.settings.[%parent.type]
label: 'Settings'
third_party_settings:
type: sequence
label: 'Third party settings'
sequence:
type: field.widget.third_party.[%key]
For our example widget, [%key]
resolves to field_example_widget
, So our third-party settings would be set via a schema value of field.widget.third_party.field_example_widget
. In practice I haven’t seen a module that uses third-party settings, but there’s a test example of this in core in the field_third_party_test
module.
The more interesting value is [%parent.type]
. The parent is the widget, and the type is the id of the widget plugin, so for our example widget it resolves to example_widget_base_widget
. This will be used by all widgets, and then provides the reference back to the widget type when the settings are loaded.
9. How does form AJAX work?
Because widget forms are standalone elements of a larger form, the general method of interacting with them using buttons is via AJAX actions. It is possible to add handlers to the parent form, but we then have to deal with the whole form array rather than the specific part we’re interested in for the widget, and isn’t a standard way of interacting with them in core.
Before doing any more on this, it’s worth a quick recap of the form AJAX API, since it’s something that not everyone uses a lot (myself included).
AJAX events can be added to any element that creates an event, with the general format:
'#ajax' => [
'callback' => '::myAjaxCallback', // don't forget :: when calling a class method.
//'callback' => [$this, 'myAjaxCallback'], //alternative notation
'disable-refocus' => FALSE, // Or TRUE to prevent re-focusing on the triggering element.
'event' => 'change',
'wrapper' => 'edit-output', // This element is updated with this AJAX callback.
'progress' => [
'type' => 'throbber',
'message' => $this->t('Verifying entry...'),
],
]
See the AJAX form API documentation for a full list of properties that can be applied to an #ajax
attribute.
Note that an #ajax
attribute can only have a single callback, unlike a property like #submit
where we can attach an array of callbacks. All other callback notation applies though, so we can either attach the callback to a static class method, a method on an existing object instance, or a regular function.
We pick the event that invokes the callback, along with some properties of the AJAX response - for example, whether the element is refocussed after the callback is executed, what the progress indicator is, and what ID the element to update has.
The callback function is expected to return an AjaxResponse
, which contains one or more commands - see the AJAX commands documentation for a full list of the available commands. These can replace HTML, add CSS, display an alert, and many other common AJAX actions. Alternatively, you can create your own command plugin that implements the CommandInterface
, a subject which is beyond the scope of this guide.
Essentially an AjaxResponse
is a way of packaging a set of pre-defined configurable JavaScript actions to be executed in the DOM.
As usual, we can take a look at what core does to see some examples of
#ajax
in widgets. In the MediaLibraryWidget
we see a callback to open the media library modal:
$element['open_button'] = [
'#type' => 'button',
'#value' => $this->t('Add media'),
'#name' => $field_name . '-media-library-open-button' . $id_suffix,
'#attributes' => [
'class' => [
'js-media-library-open-button',
],
// The jQuery UI dialog automatically moves focus to the first :tabbable
// element of the modal, so we need to disable refocus on the button.
'data-disable-refocus' => 'true',
],
'#media_library_state' => $state,
'#ajax' => [
'callback' => [static::class, 'openMediaLibrary'],
'progress' => [
'type' => 'throbber',
'message' => $this->t('Opening media library.'),
],
],
// Allow the media library to be opened even if there are form errors.
'#limit_validation_errors' => [],
];
public static function openMediaLibrary(array $form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$library_ui = \Drupal::service('media_library.ui_builder')->buildUi($triggering_element['#media_library_state']);
$dialog_options = MediaLibraryUiBuilder::dialogOptions();
return (new AjaxResponse())
->addCommand(new OpenModalDialogCommand($dialog_options['title'], $library_ui, $dialog_options));
}
We can do some validation of our form using AJAX, and return a message depending on the validity of the field content:
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state): array {
$element['value'] = array_merge($element, [
'#type' => 'textfield',
'#default_value' => isset($items[$delta]->value) ?? NULL,
'#prefix' => '<span id="foobar">',
'#suffix' => '</span>',
]);
$element['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Validate'),
'#ajax' => [
'callback' => [static::class, 'doAjax'],
'wrapper' => 'foobar',
'progress' => [
'type' => 'throbber',
]
]
];
$element['#element_validate'][] = [$this, 'validateElement'];
return $element;
}
public static function doAjax($form, FormStateInterface $form_state) {
/** @var FormValidatorInterface $form_validator */
$form_validator = \Drupal::service('form_validator');
$form_id = $form_state->getFormObject()->getFormId();
$form_validator->validateForm($form_id, $form, $form_state);
if (FormState::hasAnyErrors()) {
$message = t('Invalid');
$type = MessengerInterface::TYPE_ERROR;
}
else {
$message = t('Valid');
$type = MessengerInterface::TYPE_STATUS;
}
$response = new AjaxResponse();
$response->addCommand(new MessageCommand($message, NULL, ['type' => $type]));
return $response;
}
We don’t actually need to do the validation bit since the AJAX callback does that already, so the AJAX callback can be simplified to:
public static function doAjax($form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$wrapper_id = $triggering_element['#ajax']['wrapper'];
$parents = array_slice($triggering_element['#array_parents'], 0, count($triggering_element['#array_parents']) - 1);
$element = NestedArray::getValue($form, $parents);
if ($form_state->getError($element)) {
$message = t('Invalid');
$type = MessengerInterface::TYPE_ERROR;
}
else {
$message = t('Valid');
$type = MessengerInterface::TYPE_STATUS;
}
$response = new AjaxResponse();
$response->addCommand(new MessageCommand($message, NULL, ['type' => $type]));
$response->addCommand(new ReplaceCommand("#$wrapper_id", $element['value']));
return $response;
}
Take a look at AjaxFormHelperTrait
- it has some interesting AJAX stuff for replacing the form.
https://codelekhk.com/2018/07/15/drupal-8-ajax-validations-for-custom-form https://drupal.stackexchange.com/questions/215699/how-can-i-get-the-form-validated-with-ajax
This isn’t great - the errors are divorced from the element, and we’re having to do a lot of manipulation. We could re-render the whole form if we have an error (see the ajaxSubmit()
method on the
AjaxFormHelperTrait
) but it would be nicer to just re-render this element with all of the error information.
In core there’s the Inline Form Errors module which will let us do just that. Enable this and try re-validating our field. If you’re using ddev you can enable the module with:
ddev drush en inline_form_errors
We can now simplify our AJAX callback even more:
public static function doAjax($form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$wrapper_id = $triggering_element['#ajax']['wrapper'];
$parents = array_slice($triggering_element['#array_parents'], 0, count($triggering_element['#array_parents']) - 1);
$element = NestedArray::getValue($form, $parents);
$response = new AjaxResponse();
$response->addCommand(new ReplaceCommand("#$wrapper_id", $element['value']));
return $response;
}
Since the AJAX processing already validates the form for us, and we want to re-render the form every time regardless of whether it’s successful (since if there’s an error and the content is fixed, clicking validate should remove the error), this works fine for us.
There are still some things that could be made nicer here, for example adding a positive message for valid field content, but I’ll leave that as an exercise for the reader.
10. Third-party settings
Third-party settings are a part of the form API which allows you to add your own settings options to one or more widgets using hooks. The use-case for this might be to add a global setting (like a checkbox to manage a front-end display option) to a group of widgets in your site, or to enhance a particular kind of widget without extending the class.
If you’re in a situation where the most appropriate option is to create a new widget by extending the class, this probably isn’t for you.
10.1. The API
We can find information about the interface portion of the API in field_ui.api.php
:
/**
* Allow modules to add settings to field widgets provided by other modules.
*
* @param \Drupal\Core\Field\WidgetInterface $plugin
* The instantiated field widget plugin.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
* @param string $form_mode
* The entity form mode.
* @param array $form
* The (entire) configuration form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* Returns the form array to be built.
*
* @see \Drupal\field_ui\Form\EntityFormDisplayEditForm::thirdPartySettingsForm()
*/
function hook_field_widget_third_party_settings_form(\Drupal\Core\Field\WidgetInterface $plugin, \Drupal\Core\Field\FieldDefinitionInterface $field_definition, $form_mode, array $form, \Drupal\Core\Form\FormStateInterface $form_state) {
$element = [];
// Add a 'my_setting' checkbox to the settings form for 'foo_widget' field
// widgets.
if ($plugin->getPluginId() == 'foo_widget') {
$element['my_setting'] = [
'#type' => 'checkbox',
'#title' => t('My setting'),
'#default_value' => $plugin->getThirdPartySetting('my_module', 'my_setting'),
];
}
return $element;
}
/**
* Alters the field widget settings summary.
*
* @param array $summary
* An array of summary messages.
* @param array $context
* An associative array with the following elements:
* - widget: The widget object.
* - field_definition: The field definition.
* - form_mode: The form mode being configured.
*
* @see \Drupal\field_ui\Form\EntityFormDisplayEditForm::alterSettingsSummary()
*/
function hook_field_widget_settings_summary_alter(array &$summary, array $context) {
// Append a message to the summary when an instance of foo_widget has
// mysetting set to TRUE for the current view mode.
if ($context['widget']->getPluginId() == 'foo_widget') {
if ($context['widget']->getThirdPartySetting('my_module', 'my_setting')) {
$summary[] = t('My setting enabled.');
}
}
}
Note that these hooks are part of the field_ui
module, because they only affect the interface portions of managing third-party settings. Anything that implements these settings will have to use other mechanisms.
The way these interact with the widget is via the ThirdPartySettingsInterface
--since it’s a fairly small interface, I’ll include it here for reference:
/**
* Sets the value of a third-party setting.
*
* @param string $module
* The module providing the third-party setting.
* @param string $key
* The setting name.
* @param mixed $value
* The setting value.
*
* @return $this
*/
public function setThirdPartySetting($module, $key, $value);
/**
* Gets the value of a third-party setting.
*
* @param string $module
* The module providing the third-party setting.
* @param string $key
* The setting name.
* @param mixed $default
* The default value
*
* @return mixed
* The value.
*/
public function getThirdPartySetting($module, $key, $default = NULL);
/**
* Gets all third-party settings of a given module.
*
* @param string $module
* The module providing the third-party settings.
*
* @return array
* An array of key-value pairs.
*/
public function getThirdPartySettings($module);
/**
* Unsets a third-party setting.
*
* @param string $module
* The module providing the third-party setting.
* @param string $key
* The setting name.
*
* @return mixed
* The value.
*/
public function unsetThirdPartySetting($module, $key);
/**
* Gets the list of third parties that store information.
*
* @return array
* The list of third parties.
*/
public function getThirdPartyProviders();
}
This interface is very simple, and creates a contract for how a class should get and set third-party settings from other code outside of the implementing module. For our purposes, this is implemented by PluginBase
(which is then extended by WidgetBase
).
The form itself is surfaced by EntityDisplayFormBase
as part of the buildFieldRow()
method—this retrieves both the settings form and and third-party settings forms for the widget, then renders them out.
The settings are loaded with the configuration entity in
We can create our own example of a module that adds some third-party settings to a widget. Let’s take the StringTextfieldWidget
and add a setting that appends a variable number of exclamation marks to the text.
As with our field widget settings, we need a schema that these settings will adhere to. We can look back at our exploration of the form configuration entity schema to figure out what name this should have:
content:
type: sequence
label: 'Field widgets'
sequence:
type: mapping
label: 'Field widget'
mapping:
type:
type: string
label: 'Widget type machine name'
weight:
type: integer
label: 'Weight'
region:
type: string
label: 'Region'
settings:
type: field.widget.settings.[%parent.type]
label: 'Settings'
third_party_settings:
type: sequence
label: 'Third party settings'
sequence:
type: field.widget.third_party.[%key]
So a third-party setting schema has the name field.widget.third_party.[%key]
, where [%key]
is the widget id as that’s the settings key for the widget.
So, for our third-party settings, let’s add a field for "colour":
field.widget.third_party.string_textfield:
type: 'Mapping'
label: 'Third party settings for string widget'
mapping:
colour:
type: string
label: 'Field widget colour'
Now we need to add the matching settings form. This should all be familiar from adding a regular settings form for a widget, except this time we’re doing it with hooks:
/**
* Implements hook_field_widget_third_party_settings_form().
*/
function foobar_field_widget_third_party_settings_form(\Drupal\Core\Field\WidgetInterface $plugin, \Drupal\Core\Field\FieldDefinitionInterface $field_definition, $form_mode, array $form, \Drupal\Core\Form\FormStateInterface $form_state) {
if ($plugin->getPluginId() === 'string_textfield' && $plugin->getThirdPartySetting('foobar', 'colour')) {
$element['colour'] = [
'#type' => 'textfield',
'#title' => t('Colour'),
'#default_value' => $plugin->getThirdPartySetting('foobar', 'colour', 'none'),
];
return $element;
}
}
This hook gets called for all plugins, so we check the plugin ID ($plugin
here is the current instance of the widget class) then set the form element if it’s the one we’re after. You’ll see we’re using one of the ThirdPartySettingsInterface
methods to retrieve the value of the setting from the widget class.
The final part of the interface implementation is to add the field value to the settings summary:
/**
* Implements hook_field_widget_settings_summary_alter().
*/
function foobar_field_widget_settings_summary_alter(array &$summary, array $context) {
/** @var \Drupal\Core\Field\WidgetInterface $widget */
$widget = $context['widget'];
if ($widget->getPluginId() === 'string_textfield') {
$summary[] = t('Colour: @colour', ['@colour' => $widget->getThirdPartySetting('foobar', 'colour')]);
}
}
You’ll see that we haven’t added a default setting anywhere in the same way we would with a defaultSettings()
method in the widget class. Unfortunately it looks like at this time this isn’t possible without altering the parent form, so the default will be that the widget has no value until the settings form is first saved. You may therefore have to take this into account in any code that uses the settings later on.
10.2. The implementation
We’ve now added our settings, and we can retrieve them from the widget object with getThirdPartySetting
. So how do we make them affect our widget display?
For this we turn to hook_field_widget_form_alter()
. From field.api.php
:
/**
* Alter forms for field widgets provided by other modules.
*
* This hook can only modify individual elements within a field widget and
* cannot alter the top level (parent element) for multi-value fields. In most
* cases, you should use hook_field_widget_multivalue_form_alter() instead and
* loop over the elements.
*
* @param $element
* The field widget form element as constructed by
* \Drupal\Core\Field\WidgetBaseInterface::form().
* @param $form_state
* The current state of the form.
* @param $context
* An associative array containing the following key-value pairs:
* - form: The form structure to which widgets are being attached. This may be
* a full form structure, or a sub-element of a larger form.
* - widget: The widget plugin instance.
* - items: The field values, as a
* \Drupal\Core\Field\FieldItemListInterface object.
* - delta: The order of this item in the array of subelements (0, 1, 2, etc).
* - default: A boolean indicating whether the form is being shown as a dummy
* form to set default values.
*
* @see \Drupal\Core\Field\WidgetBaseInterface::form()
* @see \Drupal\Core\Field\WidgetBase::formSingleElement()
* @see hook_field_widget_WIDGET_TYPE_form_alter()
* @see hook_field_widget_multivalue_form_alter()
*/
function hook_field_widget_form_alter(&$element, \Drupal\Core\Form\FormStateInterface $form_state, $context) {
// Add a css class to widget form elements for all fields of type mytype.
$field_definition = $context['items']->getFieldDefinition();
if ($field_definition->getType() == 'mytype') {
// Be sure not to overwrite existing attributes.
$element['#attributes']['class'][] = 'myclass';
}
}
If you’ve been paying attention, you’ll see that we’ve switched module APIs—since we’re no longer dealing with the UI portion of the widget, and instead are changing the widget itself, we’re now in the Field API. This means that, if your custom setting is set, it will apply even when the Field UI module is disabled (as you’d expect).
Let’s add a hook to alter our widget form:
/**
* Implements hook_field_widget_form_alter().
*/
function foobar_field_widget_form_alter(&$element, \Drupal\Core\Form\FormStateInterface $form_state, $context) {
/** @var \Drupal\Core\Field\WidgetBaseInterface $widget */
$widget = $context['widget'];
if ($widget->getPluginId() === 'string_textfield') {
// @todo do this with proper attached styles
$element['value']['#attributes']['style'] = 'color: ' .$widget->getThirdPartySetting('foobar', 'colour', 'inherit') . ';';
}
}
11. Appendix
11.1. Form API primer
11.2. Render element primer
11.3. ddev environment
I’m assuming you already have a basic Drupal 8 environment set up—if not, the easiest way to get started is with the ddev installer--go ahead, I’ll wait.
Once you have your Drupal environment set up, go ahead and install Drupal console with ddev composer install --dev drupal/console
omit the ddev
if you’re using something else). If you are using ddev, we can make life a bit easier by adding a ddev command so that you can invoke the console from your host machine. In the .ddev/commands/web
directory, create a file called drupal
with the following content:
#!/bin/bash
/var/www/html/vendor/bin/drupal "$@"
Make it executable with chmod 775 .ddev/commands/web/drupal
, and you should be able to run ddev drupal
and get the output from Drupal console on the ddev container.
12. Errata
If you spot any issues with the content of this ebook, please let me know so I can correct it accordingly. Send an email to ben@benkyriakou.com with the details of the issue, your edition version, and the page it appears on. Thanks!