How does data propagate from the formElement of a Widget to the properties of a FieldItem? Here is my custom Widget:

&lt;?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 &amp;$form, FormStateInterface $form_state) {

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

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

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

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

    return $element;
  }

}


And here is my custom FieldType:

&lt;?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')
      -&gt;setRequired(TRUE);

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

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

    return $properties;
  }

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

  /**
   * {@inheritdoc}
   */
  public function isEmpty() {
    return empty($this-&gt;get('from')-&gt;getValue()) || empty($this-&gt;get('to')-&gt;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.
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-&gt;fieldDefinition-&gt;getName();

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

    if ($key_exists) {
      // Account for drag-and-drop reordering if needed.
      if (!$this-&gt;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 =&gt; &amp;$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-&gt;massageFormValues($values, $form, $form_state);

      // Assign the values and remove the empty ones.
      $items-&gt;setValue($values);
      $items-&gt;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 =&gt; $item) {
        $field_state['original_deltas'][$delta] = isset($item-&gt;_original_delta) ? $item-&gt;_original_delta : $delta;
        unset($item-&gt;_original_delta, $item-&gt;_weight);
      }
      static::setWidgetState($form['#parents'], $field_name, $form_state, $field_state);
    }
  }