0

Here is my custom Widget:

<?php

namespace Drupal\scripture\Plugin\Field\FieldWidget;

use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;

use Drupal\my_module\Plugin\Field\FieldType\MyFieldItem;

/**
 * Plugin implementation of my custom widget.
 *
 * @FieldWidget(
 *   id = "my_widget",
 *   label = @Translation("My Widget"),
 *   description = @Translation("A Widget belonging to me"),
 *   field_types = {
 *     "my_field_type"
 *   },
 *   multiple_values = FALSE,
 * )
 */
class MyWidget extends WidgetBase {

  /**
   * {@inheritdoc}
   */
  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {

    $element['#type'] = 'fieldset'; // just to show the title and put a nice frame around it

    $element['verse'] = array(
      '#type' => 'verse_picker', // This is a custom Element defined elsewhere.
    );

    /** @var MyFieldItem $item */
    $item = &$items[$delta];

    // If the item is not empty, populate the form element
    if (!$item->isEmpty()) {
      // ...
    }

    return $element;
  }

}

And here is my custom FieldType:

<?php

namespace Drupal\scripture\Plugin\Field\FieldType;

use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;

/**
 * Plugin implementation of my field type.
 *
 * @FieldType(
 *   id = "my_field_type",
 *   label = @Translation("My field Type"),
 *   description = @Translation("A FieldType belonging to me."),
 *   default_widget = "my_widget",
 *   default_formatter = "my_formatter"
 * )
 */
class MyFieldItem extends FieldItemBase {

  /**
   * {@inheritdoc}
   */
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
    // Prevent early t() calls by using the TranslatableMarkup.
    $properties['from'] = DataDefinition::create('integer')
      ->setRequired(TRUE);

    $properties['to'] = DataDefinition::create('integer')
      ->setRequired(TRUE);

    $properties['abbr'] = DataDefinition::create('string')
      ->addConstraint('Length', [
        'max' => 8,
      ])
      ->setRequired(FALSE);

    return $properties;
  }

  /**
   * {@inheritdoc}
   */
  public static function schema(FieldStorageDefinitionInterface $field_definition) {
    $schema = [
      // ...
    ];
    return $schema;
  }

  /**
   * {@inheritdoc}
   */
  public function isEmpty() {
    return empty($this->get('from')->getValue()) || empty($this->get('to')->getValue());
  }

}

Whenever I save a field, the properties stay NULL. Even when I use xdebug to inspect every call to isEmpty(), $this never has any populated properties. How does the user input get from the form element to the field item's properties? I could not find Drupal documentation on the matter.

1

submitForm() of the content entity form calls via several other methods (which you find in the call stack) this method of the field widget, which then sets the values of the field items.

WidgetBase::extractFormValues():

  public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
    $field_name = $this->fieldDefinition->getName();

    // Extract the values from $form_state->getValues().
    $path = array_merge($form['#parents'], [$field_name]);
    $key_exists = NULL;
    $values = NestedArray::getValue($form_state->getValues(), $path, $key_exists);

    if ($key_exists) {
      // Account for drag-and-drop reordering if needed.
      if (!$this->handlesMultipleValues()) {
        // Remove the 'value' of the 'add more' button.
        unset($values['add_more']);

        // The original delta, before drag-and-drop reordering, is needed to
        // route errors to the correct form element.
        foreach ($values as $delta => &$value) {
          $value['_original_delta'] = $delta;
        }

        usort($values, function ($a, $b) {
          return SortArray::sortByKeyInt($a, $b, '_weight');
        });
      }

      // Let the widget massage the submitted values.
      $values = $this->massageFormValues($values, $form, $form_state);

      // Assign the values and remove the empty ones.
      $items->setValue($values);
      $items->filterEmptyItems();

      // Put delta mapping in $form_state, so that flagErrors() can use it.
      $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
      foreach ($items as $delta => $item) {
        $field_state['original_deltas'][$delta] = isset($item->_original_delta) ? $item->_original_delta : $delta;
        unset($item->_original_delta, $item->_weight);
      }
      static::setWidgetState($form['#parents'], $field_name, $form_state, $field_state);
    }
  }
  • If I understand correctly, I'll have to override extractFormValues, because my form values and properties are structured differently? Or should that be done inside massageFormValues? – rudolfbyker Sep 6 '18 at 5:43
  • You can override extractFormValues if you have to, but since there is a lot of code generic to the list data type of multi value fields, which normally doesn't change, it is recommend to use massageFormValues. – 4k4 Sep 6 '18 at 6:21
1

To complement the answer given by 4k4: Further up the call stack from WidgetBase::extractFormValues, you will reach Map:setValue. This is where my form values got lost:

public function setValue($values, $notify = TRUE) {
  if (isset($values) && !is_array($values)) {
    throw new \InvalidArgumentException("Invalid values given. Values must be represented as an associative array.");
  }
  $this->values = $values;

  // Update any existing property objects.
  foreach ($this->properties as $name => $property) {
    $value = isset($values[$name]) ? $values[$name] : NULL;
    $property->setValue($value, FALSE);
    // Remove the value from $this->values to ensure it does not contain any
    // value for computed properties.
    unset($this->values[$name]);
  }
  // Notify the parent of any changes.
  if ($notify && isset($this->parent)) {
    $this->parent->onChange($this->name);
  }
}

The line $value = isset($values[$name]) ? $values[$name] : NULL; looks for values with the same name as the property. In my situation, those values are nested inside an array called verse like this:

Array
(
    [0] => Array
        (
            [verse] => Array
                (
                    [abbr] => AOV
                    // ... other stuff that doesn't get saved
                    [from] => 10172019
                    [to] => 10172381
                )

            [_original_delta] => 0
        )

)

Then I implement MyWidget:massageFormValues to make it look like this:

Array
(
    [0] => Array
        (
            [verse] => Array
                (
                    // ... doesn't matter
                )

            [_original_delta] => 0
            // the following three are used by Map:setValue()
            [from_vid] => 10172019
            [to_vid] => 10172381
            [translation] => AOV
        )

)

Here is the code:

public function massageFormValues(array $values, array $form, FormStateInterface $form_state)
  {
    foreach ($values as $delta => &$value) {
      // Place the values that should end up in the FieldItem's properties directly under the delta.
      $value['from'] = $value['verse']['from'];
      $value['to'] = $value['verse']['to'];
      $value['abbr'] = $value['verse']['abbr'];
    }
    // No call to parent needed.
    return $values;
  }

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.