Drupal 8 Field API series part 2: field widgets

In the first article of the Drupal 8 Field API series, we saw how field formatters are written in Drupal 8. Now it's time for widgets. You might get a déjà vu when reading as a lot resemble to formatters.

Plugins

Creating field widgets in Drupal 7 was done by implementing four hooks. In Drupal 8, widgets are now plugins using the new Plugin API. Hooks are replaced by methods in classes, which means that your module file will be empty if you only provide a widget, unless you also implement one of the (new) widget alter hooks. Being classes, this means that field widgets can now extend on each other. A good example in core is the image field widget extending the file field widget class. Discovery and class instantiation is managed by the new widget plugin manager.

Create a file like '{your_module}/lib/Drupal/{your_module}/Plugin/field/FieldWidget/{NameOfYourWidget}.php. That's a lot of directories right ? Welcome to the world of PSR-0, namespaces and plugins in D8. This is most likely going to change, feel free to read, or even help along in https://drupal.org/node/1971198. Also, plugin managers can control where plugins reside, see https://drupal.org/node/2043379, so we'll probably change this at some point.

In most cases, you will want to extend the WidgetBase class which does most of the heavy lifting for you (holds the code that was in the field_default_*() functions in Drupal 7). Following classes will usually be imported at the top of your file depending on which methods you override:

<?php
// WidgetBase class.
use Drupal\Core\Field\WidgetBase;
// FieldItemListInterface
use Drupal\Core\Field\FieldItemListInterface;
// Symfone violation interface
use Symfony\Component\Validator\ConstraintViolationInterface;
?>

1. hook_field_widget_info() are now annotations

hook_field_widget_info() is replaced by annotation-based plugin discovery, using the \Drupal\field\Annotation\FieldWidget annotation class. As for other plugin types, the accepted properties are documented in the annotation class. Other modules can extend this by implementing hook_field_widget_info_alter(). Note that some property names have changed since Drupal 7 (spaces replaces by underscores). This is how an annotation looks like, which is placed right above the class keyword.

<?php
/**
* Plugin implementation of the 'foo_widget' widget
*
* @FieldWidget(
*   id = "foo_widget",
*   label = @Translation("Foo widget"),
*   field_types = {
*     "text",
*     "text_long"
*   },
*   settings = {
*     "size" = "600",
*   }
* )
*/
class FooWidget extends WidgetBase { }
?>

2. hook_field_widget_settings_form() becomes WidgetInterface::settingsForm()

Next up is to create a settingsForm() method. If you have an old settings form, you can simply move the code to this method. The calling code (typically Field UI) takes care of saving the settings on form submit. Remember to always start with an empty $elements array and not with the $form argument from the function arguments.

Side note: in all methods in the widget class:

  • the settings values currently configured for the widget can be accessed with $this->getSetting('settings_key'), or $this->getSettings()
  • the settings values currently configured for the field on which the widget is being used can be accessed with $this->getSetting('settings_key'), or $this->getSettings(). Those methods return both field level settings and instance level settings, merged.
  • If access to field properties other than field settings is needed, the field definition can be accessed with $this->getFieldDefinition(). This returns an object implementing \Drupal\Core\Entity\Field\FieldDefinitionInterface, which unifies the separate $field and $instance structures D7 coders are familiar with. More details on this will come in a following post.

<?php
 
/**
   * {@inheritdoc}
   */
 
public function settingsForm(array $form, array &$form_state) {
   
$element = array();

   
$element['size'] = array(
     
'#type' => 'number',
     
'#title' => t('Size of textfield'),
     
'#default_value' => $this->getSetting('size'),
     
'#required' => TRUE,
     
'#min' => 1,
    );

    return
$element;
  }
?>

3. hook_field_widget_form becomes WidgetInterface::formElement()

This is where you return the widget form. Also, the methods now receive the field values as a \Drupal\Core\Field\FieldItemListInterface object, rather than an $items array in Drupal 7. More information can be found about Drupal 8 Entity API and the syntax around field values in the handbook. Simply put, FieldInterface objects can be accessed and iterated on like an array of items keyed by delta, and properties in each item can be accessed by simple object syntax.

<?php
 
/**
   * {@inheritdoc}
   */
 
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, array &$form_state) {
   
$main_widget = $element + array(
     
'#type' => 'textfield',
     
'#default_value' => isset($items[$delta]->value) ? $items[$delta]->value : NULL,
     
'#size' => $this->getSetting('size'),
     
'#placeholder' => $this->getSetting('placeholder'),
     
'#maxlength' => $this->getSetting('max_length'),
     
'#attributes' => array('class' => array('text-full')),
    );

    if (
$this->getSetting('text_processing')) {
     
$element = $main_widget;
     
$element['#type'] = 'text_format';
     
$element['#format'] = isset($items[$delta]->format) ? $items[$delta]->format : NULL;
     
$element['#base_type'] = $main_widget['#type'];
    }
    else {
     
$element['value'] = $main_widget;
    }

    return
$element;
  }
?>

4. hook_field_widget_error becomes WidgetInterface::errorElement()

The second parameter, $violation, contains the list of constraint violations reported during the validation phase. In Drupal 8, the Sympony contraints class is used to validate objects, whether doing this programmatically or through a form. We won't go deeper into this for now. In the next article, when we'll talk about the field types, this will become much clearer. Just remember for now that validation should not happen in your widgets, but in constraints.

<?php
 
/**
   * {@inheritdoc}
   */
 
public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, array &$form_state) {
    return
$element[$violation->arrayPropertyPath[0]];
  }
?>

5. WidgetInterface::settingsSummary()

This is a new method which resembles FormatterInterface::settingsSummary(). With the introduction of form modes in D8, a given field can be displayed with different widgets in the different form modes used by the entity type. This method is used to output the settings currently configured for the widget in Field UI screens, just like you do with field formatters - try to keep it short :-).

<?php
 
/**
   * {@inheritdoc}
   */
 
public function settingsSummary() {
   
$summary = array();

   
$summary[] = t('Textfield size: !size', array('!size' => $this->getSetting('size')));
   
$placeholder = $this->getSetting('placeholder');
    if (!empty(
$placeholder)) {
     
$summary[] = t('Placeholder: @placeholder', array('@placeholder' => $placeholder));
    }

    return
$summary;
  }
?>

6. WidgetInterface::massageFormValues()

This is a new method which you should only implement if you need todo complex FAPI tricks when a form is submitted. This lets you turn the raw submitted form values produced by your widget into the "regular" format expected for field values. Examples in core are the taxonomy autocomplete widget and the file widget which need todo additional processing when a form is submitted.

Alter hooks

Besides the existing hook_field_widget_info_alter, two new hooks are introduced allowing you to alter widget settings form or summary on Field UI, which behave almost the same like field formatter alter hooks: hook_field_widget_settings_form and hook_field_widget_settings_summary_alter.

Resources

Conclusion

Like formatters, writing and maintaining field widgets for Drupal 8 is not hard. In most cases, when porting, it's simply moving the contents of your old hooks to the methods in a class. In the next part, we will see how you write field type plugins in Drupal 8.

Comments

Submitted by Agnar Ødegård on August 5, 2013 - 12:48

I really appreciate your effort. If I may suggest a topic for a future post it would be best practice. Say you want to make an admin form. In D7 yoiu'd go hook_form and then system_settings_form(). From the D8 modules I've studied so far I've seen several variants using different controllers; specifying the form directly inside *.route, specifying the class and the reference a method inside that class... I can replicate and I can understand, but it's not always obvious when to use what method. Anyway, great work!

Add new comment

You are here