Errors from custom FieldItemList::getConstraints() displayed next to every single property Summary

Errors set by constraints on custom field types appear next to every single property by default.

We can use the ::errorElement() method on the widget to place errors that come from the ::getConstraints() method of the field item.

However, errors that come from ::getConstraints() in a custom field item list will not go through ::errorElement(). How can we reasonably place them?

Details

My original problem ist very complicated. I've drastically simplified it to show only what's essential. We start with a very simple custom field type with 2 number properties.

class YearRangeType extends FieldItemBase {
  public static function propertyDefinitions($field_definition) {
    $properties['start'] = DataDefinition::create('integer');
    $properties['end'] = DataDefinition::create('integer');
    return $properties;
  }

  public static function schema($field_definition) {
    $schema = ['columns' =&gt; [
      'start' =&gt; ['type' =&gt; 'int',],
      'end' =&gt; ['type' =&gt; 'int',],
    ],];
    return $schema;
  }

  public function isEmpty() {
    $start = $this-&gt;get('start')-&gt;getValue();
    $end = $this-&gt;get('end')-&gt;getValue();
    return !($start || $end);
  }
}


We define a custom FieldItemList for it that adds a simple validation: checking if at least 2 items are present.

class YearRangeItemList extends FieldItemList {
  public function getConstraints() {
    $constraints = parent::getConstraints();
    $constraints[] = $this-&gt;getTypedDataManager()
      -&gt;getValidationConstraintManager()
      -&gt;create('Count', [
        'min' =&gt; 2
      ]);
    return $constraints;
  }
}


When I try to save only one item, the field will look like this:



If the errors would result from YearRangeType::getConstraints(), we could use theerrorElement()` method of the widget to place them.

However, the Drupal core class WidgetBase does deliberately not pass errors set by the item list to errorElement(), see these lines in the Drupal core code (version 8.8.1).

How else should we place them?
We can place the errors with three modifications in the widget class.

First, we add the empty form element non_field_errors we can attach the errors to.

class YearRangeWidget extends WidgetBase {
    ...
    public function form($items, &amp;$form, $form_state, $get_delta = NULL) {
        $formElement = parent::form($items, $form, $form_state, $get_delta);
        $formElement['non_field_errors'] = ['#type' =&gt; 'item'];
        return $formElement;
    }
    ...
}


Second, in the flagErrors method, we collect all errors that do not belong to any property and call a separate method to treat them.

class YearRangeWidget extends WidgetBase {
    ...
    public function flagErrors($items, $violations, $form, $form_state) {
        $listViolations = [];
        foreach ($violations as $key =&gt; $violation) {
            if ($violation-&gt;getPropertyPath() === '') {
                $listViolations[] = $violation;
                $violations-&gt;remove($key);
            }
        }
        parent::flagErrors($items, $violations, $form, $form_state);
        $this-&gt;flagNonFieldErrors($items, $listViolations, $form, $form_state);
    }
    ...
}


Third, we define the method treating the separated errors.

class YearRangeWidget extends WidgetBase {
    ...
    public function flagNonFieldErrors($items, $violations, $form, $form_state) {
        $field = $items-&gt;getName();
        foreach ($violations as $violation) {
            $form_state-&gt;setError(
                $form[$field]['non_field_errors'],
                $violation-&gt;getMessage()
            );
        }
    }
    ...
}