datafield-1.x-dev/src/Plugin/Field/FieldType/DataFieldItem.php
src/Plugin/Field/FieldType/DataFieldItem.php
<?php
namespace Drupal\datafield\Plugin\Field\FieldType;
use Drupal\Core\Field\Attribute\FieldType;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Plugin implementation of the 'data_field' field type.
*/
#[FieldType(
id: "data_field",
label: new TranslatableMarkup("Data Field"),
description: new TranslatableMarkup("Data Field."),
default_widget: "data_field_table_widget",
default_formatter: "data_field_table_formatter",
)]
class DataFieldItem extends FieldItemBase {
public const DATETIME_STORAGE_TIMEZONE = 'UTC';
public const DATETIME_DATETIME_STORAGE_FORMAT = 'Y-m-d\TH:i:s';
public const DATETIME_DATE_STORAGE_FORMAT = 'Y-m-d';
/**
* {@inheritdoc}
*/
public static function defaultStorageSettings(): array {
// Need to have at least one item by default because the table is created
// before the user gets a chance to customize and will throw an Exception
// if there isn't at least one column defined.
return [
'columns' => [
'value' => [
'name' => 'value',
'max_length' => 255,
'size' => 'normal',
'type' => 'string',
'unsigned' => FALSE,
'precision' => 10,
'scale' => 2,
'datetime_type' => 'date',
],
],
] + parent::defaultStorageSettings();
}
/**
* {@inheritdoc}
*/
public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data): array {
$settings = $this->getSettings();
$fieldDefinition = $this->getFieldDefinition();
$isNew = $fieldDefinition->isNew();
$request_stack = \Drupal::service('request_stack');
$post = $request_stack->getCurrentRequest()->request->all();
if (!empty($post["field_storage"]["subform"]["settings"])) {
$settings["columns"] = $post["field_storage"]["subform"]["settings"]["storage"];
}
if ($isNew) {
$form["cardinality_container"]["cardinality"]["#default_value"] = -1;
$settings["columns"] = array_values($settings["columns"]);
}
// Add a new item if there aren't any, or we're rebuilding.
if ($form_state->get('add') || count($settings['columns']) == 0) {
$firstCol = current($settings['columns']);
if (!empty($firstCol['name'])) {
$firstCol['name'] .= count($settings['columns']);
}
$settings['columns'][] = $firstCol;
$form_state->set('add', NULL);
}
$element = [
'#tree' => TRUE,
'columns' => [
'#type' => 'value',
'#value' => $settings['columns'],
],
'storage' => [
'#type' => 'table',
'#header' => [
$this->t('Machine-readable name'),
$this->t('Type'),
$this->t('Maximum length'),
$this->t('Unsigned'),
$this->t('Size'),
$this->t('Precision'),
$this->t('Scale'),
$this->t('Date type'),
'',
],
],
'actions' => [
'#type' => 'actions',
],
];
$this->buildStorageTable($element, $settings, $form_state, $has_data);
if (!$has_data) {
$element['actions']['add'] = [
'#type' => 'submit',
'#value' => $this->t('Add another'),
'#submit' => [get_class($this) . '::addSubmit'],
// @todo copy setting.
'#states' => [
'visible' => [
'select[data-id="datafield-settings-clone"]' => ['value' => ''],
],
],
];
}
$form_state->setCached(FALSE);
return $element;
}
/**
* {@inheritDoc}
*/
protected function buildStorageTable(array &$element, array $settings, FormStateInterface $form_state, bool $has_data) {
$default_settings = self::defaultStorageSettings()['columns']['value'];
$isNew = $this->getFieldDefinition()->isNew();
$i = 0;
foreach ($settings['columns'] as $item) {
$name = $item['name'] ?? 'value';
$storageKeys = array_keys($settings["storage"] ?? $settings["columns"]);
if (!empty($storageKeys[$i]) && !empty($settings["storage"][$storageKeys[$i]])) {
$name = $settings["storage"][$storageKeys[$i]]["name"];
}
if ($i === $form_state->get('remove')) {
$form_state->set('remove', NULL);
// @todo it must remove columns in database.
if (!empty($settings["columns"])) {
if (isset($settings["columns"][$name])) {
unset($settings["columns"][$name]);
$this->getFieldDefinition()->setSetting('columns', $settings["columns"]);
}
else {
foreach ($settings["columns"] as $index => $col) {
if ($col['name'] == $name) {
unset($settings["columns"][$index]);
break;
}
}
}
}
if (!empty($settings["field_settings"][$name])) {
unset($settings["field_settings"][$name]);
$this->getFieldDefinition()->setSetting('field_settings', $settings["field_settings"]);
}
if (!$isNew) {
$this->getFieldDefinition()->save();
}
if (!empty($element["columns"]["#value"][$name])) {
unset($element["columns"]["#value"][$name]);
}
if (!empty($element["columns"]["#value"][$name])) {
unset($element["columns"]["#value"][$name]);
}
if (!empty($element["columns"]["#value"][$i])) {
unset($element["columns"]["#value"][$i]);
}
if (!empty($element["storage"][$i])) {
unset($element["storage"][$i]);
}
continue;
}
$parents = ['field_storage', 'subform', 'settings', 'storage', $name];
$this->buildStorageRow($element['storage'][$i], $parents, $item, $default_settings, $has_data);
$element['storage'][$i]['remove'] = [
'#type' => 'submit',
'#parents' => [...$parents, 'remove'],
'#value' => $this->t('Remove'),
'#submit' => [get_class($this) . '::removeSubmit'],
'#name' => 'remove:' . $i,
'#delta' => $i,
'#disabled' => $has_data,
];
$i++;
}
if ($i == 1) {
$element['storage'][0]['remove']['#access'] = FALSE;
}
}
/**
* {@inheritDoc}
*/
protected function buildStorageRow(array|null &$row, array $parents, mixed $item, mixed $default_settings, bool $has_data) {
$fieldName = $this->getFieldDefinition()->getName();
$lengthField = strlen($fieldName);
$name = end($parents);
$row['name'] = [
'#type' => 'machine_name',
'#description' => $this->t('A unique machine-readable name containing only letters, numbers, or underscores. This will be used in the column name on the field table in the database.'),
'#default_value' => $name,
'#parents' => [...$parents, 'name'],
'#disabled' => $has_data,
'#maxlength' => 63 - $lengthField,
'#required' => TRUE,
'#title_display' => 'invisible',
'#attributes' => [
'pattern' => '[a-z_0-9]+',
],
'#machine_name' => [
'exists' => [$this, 'machineNameExists'],
'standalone' => TRUE,
],
];
$row['type'] = [
'#type' => 'select',
'#parents' => [...$parents, 'type'],
'#default_value' => $item['type'] ?? 'string',
'#disabled' => $has_data,
'#required' => TRUE,
'#options' => $this->subfieldTypes(),
];
$row['max_length'] = [
'#type' => 'number',
'#parents' => [...$parents, 'max_length'],
'#description' => $this->t('The maximum length of the subfield in characters.'),
'#default_value' => $item['max_length'] ?? $default_settings["max_length"],
'#disabled' => $has_data,
'#min' => 1,
'#states' => [
'visible' => [
":input[name*='[storage][$name][type]']" => [
['value' => 'string'],
['value' => 'char'],
['value' => 'varchar'],
],
],
],
];
$row['unsigned'] = [
'#type' => 'checkbox',
'#parents' => [...$parents, 'unsigned'],
'#title' => $this->t('Unsigned'),
'#default_value' => $item['unsigned'] ?? $default_settings['unsigned'],
'#disabled' => $has_data,
'#states' => [
'visible' => [
":input[name*='[storage][$name][type]']" => [
['value' => 'integer'],
['value' => 'float'],
['value' => 'numeric'],
],
],
],
];
$row['size'] = [
'#type' => 'select',
'#parents' => [...$parents, 'size'],
'#default_value' => $item['size'] ?? $default_settings['size'],
'#disabled' => $has_data,
'#options' => [
'normal' => $this->t('Normal'),
'big' => $this->t('Big'),
'medium' => $this->t('Medium'),
'small' => $this->t('Small'),
'tiny' => $this->t('Tiny'),
],
'#states' => [
'visible' => [
":input[name*='[storage][$name][type]']" => [
['value' => 'integer'],
['value' => 'serial'],
['value' => 'float'],
['value' => 'text'],
['value' => 'blob'],
],
],
],
];
$row['precision'] = [
'#type' => 'number',
'#parents' => [...$parents, 'precision'],
'#description' => $this->t('The total number of digits to store in the database, including those to the right of the decimal.'),
'#default_value' => $item['precision'] ?? $default_settings['precision'],
'#disabled' => $has_data,
'#min' => 10,
'#max' => 32,
'#states' => [
'visible' => [
":input[name*='[storage][$name][type]']" => [
['value' => 'numeric'],
['value' => 'float'],
],
],
],
];
$row['scale'] = [
'#type' => 'number',
'#parents' => [...$parents, 'scale'],
'#description' => $this->t('The number of digits to the right of the decimal.'),
'#default_value' => $item['scale'] ?? $default_settings['scale'],
'#disabled' => $has_data,
'#min' => 0,
'#max' => 10,
'#states' => [
'visible' => [":input[name*='[storage][$name][type]']" => ['value' => 'numeric']],
],
];
$row['datetime_type'] = [
'#type' => 'radios',
'#parents' => [...$parents, 'datetime_type'],
'#description' => $this->t('Choose the type of date to create.'),
'#default_value' => $item['datetime_type'] ?? $default_settings['datetime_type'],
'#disabled' => $has_data,
'#options' => [
'datetime' => $this->t('Date and time'),
'date' => $this->t('Date'),
'time' => $this->t('Time'),
'year' => $this->t('Year'),
'month' => $this->t('Month'),
'week' => $this->t('Week'),
'timestamp' => $this->t('Timestamp'),
],
'#states' => [
'visible' => [
":input[name*='[storage][$name][type]']" => [
['value' => 'datetime_iso8601'],
['value' => 'date'],
],
],
],
];
}
/**
* Submit handler for the StorageConfigEditForm.
*
* This handler is added in data field. Module since it has to be placed
* directly on the submit button (which we don't have access to in our
* ::storageSettingsForm() method above).
*/
public static function submitStorageConfigEditForm(array &$form, FormStateInterface $form_state) {
// Re-key our column settings and overwrite the values in form_state so that
// we have clean settings saved to the db.
$columns = [];
$parents = ['field_storage', 'subform', 'settings', 'storage'];
$items = $form_state->getValue($parents) ?? [];
foreach ($items as $item) {
$columns[$item['name']] = $item;
unset($columns[$item['name']]['remove']);
}
self::cleanSettings($columns);
$form_state->setValue(['field_storage', 'subform', 'settings', 'columns'], $columns);
$form_state->setValue(['field_storage', 'subform', 'settings', 'storage'], NULL);
// Reset the field storage config property - it will be recalculated
// when accessed via the property definition getter.
// @see Drupal\field\Entity\FieldStorageConfig::getPropertyDefinitions()
// If we don't do this, an exception is thrown during the table update that
// is very difficult to recover from since the original field tables have
// already been removed at that point.
$field_storage_config = $form_state->getBuildInfo()['callback_object']->getEntity();
$field_storage_config->set('propertyDefinitions', NULL);
}
/**
* {@inheritdoc}
*/
public static function cleanSettings(&$settings) {
foreach ($settings as &$setting) {
switch ($setting['type']) {
case 'date':
case 'datetime_iso8601':
unset($setting['size']);
unset($setting['scale']);
unset($setting['unsigned']);
unset($setting['precision']);
unset($setting['max_length']);
break;
case 'float':
case 'numeric':
unset($setting['datetime_type']);
break;
case 'file':
case 'boolean':
case 'integer':
case 'entity_reference':
unset($setting['scale']);
unset($setting['precision']);
unset($setting['datetime_type']);
break;
case 'uri':
case 'blob':
case 'json':
case 'text':
case 'email':
case 'serial':
case 'string':
case 'telephone':
unset($setting['scale']);
unset($setting['unsigned']);
unset($setting['precision']);
unset($setting['datetime_type']);
break;
}
}
}
/**
* {@inheritdoc}
*/
public static function defaultFieldSettings() {
return [
'field_settings' => [],
] + parent::defaultFieldSettings();
}
/**
* {@inheritdoc}
*/
public function fieldSettingsForm(array $form, FormStateInterface $form_state): array {
$settings = $this->getSettings();
if ($form_state->isRebuilding()) {
$formSettings = $form_state->getValue('settings') ?? $form_state->getUserInput()['settings'];
$field_settings = $formSettings['field_settings'] ?? [];
$settings['field_settings'] = $field_settings;
}
else {
$field_settings = $this->getSetting('field_settings');
}
// Remove unnecessary data from $settings.
unset($settings["field_settings"]);
$types = static::subfieldTypes();
$element = [
'#type' => 'fieldset',
'#title' => $this->t('Sub-field settings'),
];
$pluginService = \Drupal::service('plugin.manager.data_field.type');
$fieldName = $this->getFieldDefinition()->getName();
$entityType = $this->getEntity()->getEntityTypeId();
$subfields = $settings["storage"] ?? $settings["columns"];
$request_stack = \Drupal::service('request_stack');
$post = $request_stack->getCurrentRequest()->request->all();
if (!empty($post["field_storage"]["subform"]["settings"])) {
$subfields = [];
foreach ($post["field_storage"]["subform"]["settings"]["storage"] as $item) {
if (!empty($item['name'])) {
$subfields[$item['name']] = $item;
}
}
}
foreach ($subfields as $subfield => $item) {
$type = $item['type'];
$plugin = $pluginService->createInstance($type);
$title = $subfield;
// Update title with custom label, if available.
$title = !empty($field_settings[$subfield]['label']) ? $field_settings[$subfield]['label'] : $title;
$title = ucfirst(str_replace('_', ' ', $title));
$title .= ' - ' . $types[$type];
if (empty($field_settings[$subfield])) {
$field_settings[$subfield] = [];
}
// Update field settings with common properties.
$field_settings[$subfield] += [
'machine_name' => $subfield,
'field_name' => $fieldName,
'entity_type' => $entityType,
];
$element['field_settings'][$subfield] = [
'#type' => 'details',
'#title' => $title,
'#open' => FALSE,
'#tree' => TRUE,
] + $plugin->fieldSettingsForm($field_settings[$subfield], $item);
}
return $element;
}
/**
* {@inheritdoc}
*
* @todo Find a way to disable constraints for default field values.
*/
public function getConstraints(): array {
$constraint_manager = \Drupal::typedDataManager()->getValidationConstraintManager();
$constraints = parent::getConstraints();
$settings = $this->getSettings();
$subConstraints = [];
$pluginService = \Drupal::service('plugin.manager.data_field.type');
foreach ($this->getSetting('columns') as $subfield => $column) {
$subfield_type = $column['type'];
$field_settings = [];
if (!empty($settings['field_settings'][$subfield])) {
$field_settings += $settings['field_settings'][$subfield];
}
$field_settings += $column;
$plugin = $pluginService->createInstance($subfield_type);
$subConstraints[$subfield] = $plugin->getConstraints($field_settings);
}
$constraints[] = $constraint_manager->create('ComplexData', $subConstraints);
return $constraints;
}
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition): array {
$pluginService = \Drupal::service('plugin.manager.data_field.type');
// Determine the source of subfields data.
$settings = $field_definition->getSettings();
$subfields = $field_definition->getSetting('columns');
$request_stack = \Drupal::service('request_stack');
$post = $request_stack->getCurrentRequest()->request->all();
if (!empty($post["field_storage"]["subform"]["settings"]["storage"])) {
$subfields = [];
foreach ($post["field_storage"]["subform"]["settings"]["storage"] as $item) {
if (!empty($item['name'])) {
$subfields[$item['name']] = $item;
}
}
}
// Process subfields to generate schema.
$columns = [];
foreach ($subfields as $subfield => $item) {
// Use the provided name or the default subfield name.
$subfield = !empty($item['name']) ? $item['name'] : $subfield;
// Check and Add new colum if it is not exists.
if (!empty($settings["columns"]) && empty($settings['columns'][$subfield])) {
$settings['columns'][$subfield] = $item;
$field_definition->setSettings($settings);
}
// Create an instance of the data field plugin.
$type = $item['type'];
$plugin = $pluginService->createInstance($type);
// Generate schema for the subfield.
$schema = $plugin->schema($item);
// Add a description to the subfield value column.
$schema['columns']['value']['description'] = ucfirst($subfield) . ' subfield value.';
// Add the subfield to the columns array.
$columns[$subfield] = $schema['columns']['value'];
}
return ['columns' => $columns];
}
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties = [];
$pluginService = \Drupal::service('plugin.manager.data_field.type');
$originColumns = $columns = $field_definition->getSetting('columns');
$request_stack = \Drupal::service('request_stack');
$post = $request_stack->getCurrentRequest()->request->all();
// Beware [storage] Only for datafield.
if (!empty($post["field_storage"]["subform"]["settings"]["storage"]) && !empty($post["default_value_input"])) {
$fieldName = array_key_first($post["default_value_input"]);
$field_name = $field_definition->getName();
if ($field_name == $fieldName) {
$columns = $post["field_storage"]["subform"]["settings"]["storage"];
}
}
$updateSchemas = FALSE;
foreach ($columns as $colName => &$subfield) {
$type = $subfield['type'] ?? 'string';
$plugin = $pluginService->createInstance($type);
$name = $subfield['name'] ?? '';
if (!empty($name)) {
$properties[$name] = $plugin->propertyDefinitions(
$subfield +
[
'field_definition' => $field_definition,
]
);
}
$subfield = $plugin->schema($subfield)['columns']['value'];
$subfield['name'] = $name;
if ($colName != $name || !isset($originColumns[$name])) {
$updateSchemas = TRUE;
}
}
// Update schema if column names have changed.
if ($updateSchemas) {
self::updateSchema($field_definition, $columns, $originColumns);
}
return $properties;
}
/**
* Drupal 10.2 check field schema when we update a subfield.
*
* It must have a column added to the schema before checking.
*
* {@inheritdoc}
*/
public static function updateSchema(FieldStorageDefinitionInterface $field_definition, $columns, $originColumns) {
if (!empty($columns)) {
$entity_type_id = $field_definition->getTargetEntityTypeId();
$field_name = $field_definition->getName();
$schema = \Drupal::database()->schema();
$entityType = \Drupal::entityTypeManager()->getStorage($entity_type_id);
if (!method_exists($entityType, 'getTableMapping')) {
return;
}
$table_mapping = $entityType->getTableMapping([$field_name => $field_definition]);
$table_names = $table_mapping->getDedicatedTableNames();
foreach ($columns as $colName => $spec) {
// Update schema name colum.
$name = $spec['name'] ?? '';
if (empty($name)) {
continue;
}
$column_name = $field_name . '_' . $name;
$column_name_origin = $field_name . '_' . $colName;
foreach ($table_names as $table_name) {
if ($schema->tableExists($table_name) && $spec) {
if ($colName != $name && $schema->fieldExists($table_name, $column_name_origin)) {
$schema->changeField($table_name, $column_name_origin, $column_name, $spec);
}
elseif (!$schema->fieldExists($table_name, $column_name)) {
$schema->addField($table_name, $column_name, $spec);
}
}
}
}
}
}
/**
* {@inheritdoc}
*/
public function isEmpty(): bool {
$settings = $this->getSettings();
if (empty($settings["columns"])) {
return FALSE;
}
$getValue = $this->getValue();
foreach ($settings["columns"] as $subfield => $item) {
$value = $getValue[$subfield] ?? NULL;
$type = $item['type'] ?? NULL;
if ($type === 'boolean') {
if ($value == 1) {
return FALSE;
}
continue;
}
if (!empty($getValue[$subfield]) && $item["type"] == 'uri' && is_array($getValue[$subfield])) {
$value = $getValue[$subfield]['url'] ?? NULL;
}
if (in_array($item['type'], ['entity_reference', 'file'])) {
if (is_array($value) || is_object($value)) {
if (!empty($value)) {
return FALSE;
}
continue;
}
if (is_numeric($value)) {
return FALSE;
}
continue;
}
if ($value !== NULL && $value !== '') {
return FALSE;
}
}
return TRUE;
}
/**
* Returns available subfield storage types.
*/
public static function subfieldTypes(): array {
$types = \Drupal::service('plugin.manager.data_field.type');
$plugin_definitions = $types->getDefinitions();
$result = [];
array_walk($plugin_definitions, function ($object) use (&$result) {
$result[$object["id"]] = $object["label"];
});
return $result;
}
/**
* {@inheritdoc}
*/
public static function fieldSettingsToConfigData(array $settings): array {
foreach ($settings['field_settings'] as $subfield => $item) {
if (empty($item['allowed_values'])) {
continue;
}
$structured_values = [];
if (is_string($item['allowed_values'])) {
$explode = explode("\r", str_replace("\n", '', $item['allowed_values']));
$item['allowed_values'] = $explode;
if (count($explode) > 1) {
$item['allowed_values'] = [];
foreach ($explode as $val) {
$extract = explode('|', $val);
$value = $extract[0];
$label = !empty($extract[1]) ? $extract[1] : $extract[0];
$item['allowed_values'][$value] = $label;
}
}
}
foreach ($item['allowed_values'] as $value => $label) {
$structured_values[] = [
'value' => $value,
'label' => str_replace('_', ' ', $label),
];
}
$settings['field_settings'][$subfield]['allowed_values'] = $structured_values;
}
return $settings;
}
/**
* {@inheritdoc}
*/
public static function fieldSettingsFromConfigData(array $settings): array {
if (empty($settings['field_settings'])) {
return $settings;
}
foreach ($settings['field_settings'] as $field => $subfield) {
if (empty($subfield['allowed_values'])) {
continue;
}
$structured_values = [];
foreach ($subfield['allowed_values'] as $item) {
$structured_values[$item['value']] = $item['label'];
}
$settings["field_settings"][$field]["allowed_values"] = $structured_values;
}
return $settings;
}
/**
* Checks if list option is allowed for a given sub-field type.
*/
public static function isListAllowed(string $subfield_type): bool {
$list_types = array_keys(self::subfieldTypes());
$notAllowed = [
'boolean', 'text', 'datetime_iso8601', 'date', 'json', 'blob', 'file', 'entity_reference',
];
return in_array($subfield_type, $list_types) && !in_array($subfield_type, $notAllowed);
}
/**
* {@inheritdoc}
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition): array {
$settings = $field_definition->getSettings();
$data = [];
$pluginService = \Drupal::service('plugin.manager.data_field.type');
foreach ($settings['columns'] as $subfield => $item) {
// If allowed values are limited pick one of them from field settings.
if (!empty($settings['field_settings'][$subfield]['list'])) {
$data[$subfield] = array_rand($settings['field_settings'][$subfield]['allowed_values']);
continue;
}
$type = $item['type'] ?? 'string';
$plugin = $pluginService->createInstance($type);
$data[$subfield] = $plugin->generateSampleValue($item);
}
return $data;
}
/**
* Check for duplicate names on our columns settings.
*/
public function machineNameExists($value, array $form, FormStateInterface $form_state) {
$count = 0;
$parents = ['field_storage', 'subform', 'settings', 'storage'];
$settings = $form_state->getValue($parents) ?? [];
foreach ($settings as $item) {
if ($item['name'] == $value) {
$count++;
}
}
return $count > 1;
}
/**
* Callback for both ajax-enabled buttons.
*
* Selects and returns the fieldset with the names in it.
*/
public function actionCallback(array &$form, FormStateInterface $form_state) {
return $form['settings']['storage'] ?? [];
}
/**
* Submit handler for the "add-one-more" button.
*
* Increments the max counter and causes a rebuild.
*/
public static function addSubmit(array &$form, FormStateInterface $form_state) {
$form_state->set('add', TRUE);
// @todo drupal 11.2 replace by ajax command on #field_combined.
$request = \Drupal::request();
$request->query->count() == 0 ? $form_state->setRebuild() : $form_state->setRedirect(\Drupal::routeMatch()
->getRouteName(), \Drupal::routeMatch()->getRawParameters()->all());
}
/**
* Submit handler for the "remove one" button.
*
* Decrements the max counter and causes a form rebuild.
*/
public static function removeSubmit(array &$form, FormStateInterface $form_state) {
$form_state->set('remove', $form_state->getTriggeringElement()['#delta']);
$form_state->setRebuild();
}
/**
* {@inheritdoc}
*/
public static function mainPropertyName() {
return NULL;
}
}
