diff --git a/modules/json_form_widget/README.md b/modules/json_form_widget/README.md new file mode 100644 index 0000000000..51d4748d63 --- /dev/null +++ b/modules/json_form_widget/README.md @@ -0,0 +1,100 @@ +## High level + +```mermaid +sequenceDiagram + participant FormBuilder + participant FieldTypeRouter + participant Element Handlers + participant SchemaUiHandler + + loop each $property in $form + FormBuilder ->> FieldTypeRouter: getFormElement() + FieldTypeRouter ->> Element Handlers: handler functions
on helper classes + Note over FieldTypeRouter, Element Handlers: Initial build of property
Elements + Element Handlers ->> FormBuilder: Return default element for $property + end + + FormBuilder ->> SchemaUiHandler: applySchemaUi() + Note over FormBuilder, SchemaUiHandler: Now apply SchemaUi to full $form + loop each $property + SchemaUiHandler ->> SchemaUiHandler: applyOnBaseField() + SchemaUiHandler ->> SchemaUiHandler: handlePropertySpec() + Note over SchemaUiHandler, SchemaUiHandler: See handlePropertySpec()
internals diagram + end + SchemaUiHandler ->> FormBuilder: Return $form with SchemaUi alterations + +``` + +## The handlePropertySpec() method + +As called from withing SchemaUiHandler::applyShemaUi + +```mermaid +sequenceDiagram + participant handlePropertySpec + + +``` + +## The initial build + +```mermaid +graph TD + getForm["FormBuilder::getJsonForm()"] --> eachProp["foreach $properties"] + eachProp --> getElement["FieldTypeRouter::getFormElement()"] + getElement --> switch[Switch $type] + switch --> object{object} + + object -- true --> handleObject["ObjectHelper::handleObjectElement()"] + handleObject --> generateObject["ObjectHelper::generateObjectElement()"] + generateObject --> generateProperties["ObjectHelper::generateProperties()"] + generateProperties -- recursion --> eachProp + + + object -- false --> array{array} + array -- true --> handleArray["ArrayHelper::handleArrayElement()"] + handleArray --> complex{Items are objects?} + complex -- no --> buildSimple["ArrayHelper::buildSimpleArrayElement()"] + complex -- yes --> buildComplex["ArrayHelper::buildComplexArrayElement()"] + buildComplex --> handleObject + + array -- false --> string["string"] + string -- true --> handleString["StringHelper::handleStringElement()"] + string -- false --> integer["integer"] + integer -- true --> handleInteger["IntegerHelper::handleIntegerElement()"] + switch --> eachProp + eachProp --> getForm +``` + +## Customizing widgets w/SchemaUI + +```mermaid +flowchart-elk TD + getForm["FormBuilder::getJsonForm()"] --> applySchemaUi["SchemaUiHandler::applySchemaUi()"] + applySchemaUi --> eachProp2["foreach schemaUI property"] + eachProp2 --> applyOnBaseField["SchemaUiHandler::applyOnBaseField()"] + eachProp2 --> handlePropertySpec + subgraph s1["applyOnBaseField()"] + applyOnBaseField --> updateWidgets["SchemaUiHandler::updatewidgets()"] + updateWidgets --> disableFields["SchemaUiHandler::disableFields()"] + disableFields --> addPlaceholders["SchemaUiHandler::addPlaceholders()"] + addPlaceholders --> changeFieldDescriptions["SchemaUiHandler::changeFieldDescriptions()"] + changeFieldDescriptions --> changeFieldTitle["SchemaUiHandler::changeFieldTitle()"] + end + subgraph s2["handlePropertySpec"] + handlePropertySpec["SchemaUiHandler::handlePropertySpec()"] --> what{"what is it"} + what -- array --> eachArrayElement + eachArrayElement --> applyOnArrayFields + + applyOnArrayFields --> eachArrayElementField + eachArrayElementField --> inSpec{"Does SchemaUI
contain config for
this field?"} + inSpec -- yes --> handlePropertySpec + inSpec -- no --> applyOnBaseFieldRec["SchemaUiHandler::applyOnBaseField()"] + + what -- object --> applyOnObjectFields + applyOnObjectFields --> eachObjField["foreach object property in the SchemaUi spec"] + eachObjField --> applyOnBaseFieldRec + + end + +``` diff --git a/modules/json_form_widget/json_form_widget.module b/modules/json_form_widget/json_form_widget.module index 444a8d4849..ccdef19e86 100644 --- a/modules/json_form_widget/json_form_widget.module +++ b/modules/json_form_widget/json_form_widget.module @@ -35,7 +35,7 @@ function json_form_widget_remove_one(array &$form, FormStateInterface $form_stat /** * Update count property by the given offset. * - * @param FormStateInterface $form_state + * @param \Drupal\Core\Form\FormStateInterface $form_state * Form state. * @param int $offset * Offset to change count by. diff --git a/modules/json_form_widget/src/ArrayHelper.php b/modules/json_form_widget/src/ArrayHelper.php index 09b13e19fe..05900610ce 100644 --- a/modules/json_form_widget/src/ArrayHelper.php +++ b/modules/json_form_widget/src/ArrayHelper.php @@ -2,9 +2,10 @@ namespace Drupal\json_form_widget; -use Drupal\Core\Form\FormStateInterface; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\DependencyInjection\DependencySerializationTrait; +use Drupal\Core\Form\FormStateInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -30,9 +31,7 @@ class ArrayHelper implements ContainerInjectionInterface { public FieldTypeRouter $builder; /** - * Inherited. - * - * @{inheritdocs} + * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static( @@ -56,7 +55,7 @@ public function setBuilder(FieldTypeRouter $builder): void { } /** - * Update wrapper element of the triggering button after build. + * Shared AJAX callback function for all array buttons. * * @param array $form * Newly built form render array. @@ -66,7 +65,7 @@ public function setBuilder(FieldTypeRouter $builder): void { * @return array * Field wrapper render array. */ - public function addOrRemoveButtonCallback(array &$form, FormStateInterface $form_state): array { + public function arrayActionButtonCallback(array &$form, FormStateInterface $form_state): array { // Retrieve triggering button element. $button = $form_state->getTriggeringElement(); // Extract full heritage for the triggered button. @@ -96,37 +95,136 @@ public function addOrRemoveButtonCallback(array &$form, FormStateInterface $form * Handle form element for an array. */ public function handleArrayElement(array $definition, ?array $data, FormStateInterface $form_state, array $context): array { - // Extract field name from field definition and min items from field schema - // for later reference. + // Extract field name from field definition and min items from field schema. $field_name = $definition['name']; $min_items = $definition['schema']->minItems ?? 0; - // Build context name. + $context_name = self::buildContextName($context); - // Determine number of form items to generate. $item_count = $this->getItemCount($context_name, count($data ?? []), $min_items, $form_state); + $is_required = in_array($field_name, $this->builder->getSchema()->required ?? []); - // Determine if this field is required. - $required_fields = $this->builder->getSchema()->required ?? []; - $field_required = in_array($field_name, $required_fields); // Build the specified number of field item elements. - $field_properties = []; + $items = []; for ($i = 0; $i < $item_count; $i++) { - $property_required = $field_required && ($i < $min_items); - $field_properties[] = $this->buildArrayElement($definition, $data[$i] ?? NULL, $form_state, array_merge($context, [$i]), $property_required); + $property_required = $is_required && ($i < $min_items); + $items[] = $this->buildArrayItemElement($definition, $data[$i] ?? NULL, $form_state, array_merge($context, [$i]), $property_required); } // Build field element. - return [ - '#type' => 'fieldset', - '#title' => ($definition['schema']->title ?? $field_name), - '#description' => ($definition['schema']->description ?? ''), + $element = [ + '#type' => 'fieldset', + '#title' => ($definition['schema']->title ?? $field_name), + '#description' => ($definition['schema']->description ?? ''), '#description_display' => 'before', - '#prefix' => '
', - '#suffix' => '
', - '#tree' => TRUE, - 'actions' => $this->buildActions($item_count, $min_items, $field_name, $context_name), - $field_name => $field_properties, + '#prefix' => '
', + '#suffix' => '
', + '#tree' => TRUE, + '#required' => $is_required, + 'actions' => [ + '#type' => 'actions', + 'actions' => [ + 'add' => $this->buildAction($this->t('Add one'), 'addOne', $field_name, $context_name), + ], + ], + $field_name => $items, ]; + return $element; + } + + /** + * Build a single element from an array. + * + * @param array $definition + * Field definition. + * @param mixed $data + * Field data. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. + * @param string[] $context + * Field context. + * @param bool $required + * Whether the field is required. + * + * @return array + * Render array for the array element. + */ + protected function buildArrayItemElement(array $definition, $data, FormStateInterface $form_state, array $context, bool $required): array { + // Use the simple or complex method depending on whether items are objects. + if (isset($definition['schema']->items->properties)) { + $element = $this->buildComplexArrayElement($definition, $data, $form_state, $context); + } + else { + $element = $this->buildSimpleArrayElement($definition, $data, $context); + } + // If we show the element on the form, it's required. + $element['#required'] = $required; + return $element; + } + + /** + * Returns single simple element from array. + * + * @param array $definition + * Field definition. + * @param mixed $data + * Field data. + * @param string[] $context + * Field context. + * + * @return array + * Render array for the simple array element. + */ + protected function buildSimpleArrayElement(array $definition, $data, array $context): array { + return [ + '#type' => 'fieldset', + '#attributes' => [ + 'data-parent' => $definition['name'], + 'class' => ['json-form-widget-array-item'], + ], + 'field' => array_filter([ + '#type' => 'textfield', + '#title' => $definition['schema']->items->title ?? NULL, + '#default_value' => $data, + ]), + 'actions' => $this->buildElementActions($definition['name'], self::buildContextName($context)), + ]; + } + + /** + * Flatten array element fieldset w/buttons for processing. + * + * @param array $element + * A form element. + */ + public static function flattenArrayElementFieldset(array &$element): void { + if (isset($element['field']) && $element['#type'] == 'fieldset') { + $element = ['#required' => ($element['#required'] ?? FALSE)] + $element['field']; + } + } + + /** + * Returns single complex element from array. + * + * @param array $definition + * Field definition. + * @param mixed $data + * Field data. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. + * @param string[] $context + * Field context. + * + * @return array + * Render array for the complex array element. + */ + protected function buildComplexArrayElement(array $definition, $data, FormStateInterface $form_state, array $context): array { + $subdefinition = [ + 'name' => $definition['name'], + 'schema' => $definition['schema']->items, + ]; + $element = $this->objectHelper->handleObjectElement($subdefinition, $data, $form_state, $context, $this->builder); + $element[$definition['name']]['actions'] = $this->buildElementActions($definition['name'], self::buildContextName($context)); + return $element; } /** @@ -200,85 +298,182 @@ public static function buildCountProperty(string $context_name): array { } /** - * Helper function to build form field actions. - */ - protected function buildActions(int $item_count, int $min_items, string $parent, string $context_name): array { - $actions = []; - - // Build add action. - $actions['add'] = $this->buildAction($this->t('Add one'), 'json_form_widget_add_one', $parent, $context_name); - // Build remove action if there are more than the minimum required elements - // in this field array. - if ($item_count > $min_items) { - $actions['remove'] = $this->buildAction($this->t('Remove one'), 'json_form_widget_remove_one', $parent, $context_name); - } - - return [ - '#type' => 'actions', - 'actions' => $actions, - ]; - } - - /** - * Helper function to get action. + * Helper function to build an action button. + * + * @param string $title + * Button title. + * @param string $method + * Button submit method; should be a static method from this class. + * @param string $parent + * The parent element for the action; usually the current field name. + * @param string $context_name + * The context name, output of ::buildContextName(). */ - protected function buildAction(string $title, string $function, string $parent, string $context_name): array { - return [ + protected function buildAction(string $title, string $method, string $parent, string $context_name): array { + $action = [ '#type' => 'submit', '#name' => $context_name, '#value' => $title, - '#submit' => [$function], + '#submit' => [self::class . '::' . $method], '#ajax' => [ - 'callback' => [$this, 'addOrRemoveButtonCallback'], - 'wrapper' => self::buildWrapperIdentifier($context_name), + 'callback' => [$this, 'arrayActionButtonCallback'], + 'wrapper' => self::buildWrapperIdentifier($parent), ], '#attributes' => [ 'data-parent' => $parent, - 'data-context' => $context_name, ], '#limit_validation_errors' => [], ]; + return $action; } /** - * Handle single element from array. + * Build the remove/reorder actions for a single element. + * + * @param string $parent + * Parent element name. + * @param string $context_name + * Data context. * - * Chooses whether element is simple or complex. + * @return array{#type: string, remove: array} + * Actions render array. */ - protected function buildArrayElement(array $definition, $data, FormStateInterface $form_state, array $context, bool $required): array { - // If this element's definition has properties defined... - $element = isset($definition['schema']->items->properties) ? - // Attempt to build a complex element, otherwise... - $this->buildComplexArrayElement($definition, $data, $form_state, $context) : - // Build a simple element. - $this->buildSimpleArrayElement($definition, $data); - - // Set element requirement. - $element['#required'] = $required; + protected function buildElementActions(string $parent, string $context_name) { + return [ + '#type' => 'actions', + 'remove' => $this->buildAction($this->t('Remove'), 'remove', $parent, $context_name), + 'move_up' => $this->buildAction($this->t('Move Up'), 'moveUp', $parent, $context_name), + 'move_down' => $this->buildAction($this->t('Move Down'), 'moveDown', $parent, $context_name), + ]; + } - return $element; + /** + * Submit function for element "remove" button. + * + * @param array $form + * Form render array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. + */ + public static function remove(array &$form, FormStateInterface $form_state) { + $button_element = $form_state->getTriggeringElement(); + $parent = $button_element['#attributes']['data-parent']; + $parents = $button_element['#parents']; + $element_index = str_replace("{$parent}-", '', $button_element['#name']); + $count_property = self::buildCountProperty($parent); + $user_input = $form_state->getUserInput(); + + // Update the user input to remove the specific element. + $key_exists = NULL; + static::trimParents($parents, $element_index); + $input_values = &NestedArray::getValue($user_input, $parents, $key_exists); + if ($key_exists) { + unset($input_values[$element_index]); + // Re-index the array to maintain proper keys. + $input_values = \array_values($input_values); + } + + $form_state->setUserInput($user_input); + + // Modify stored item count. The form rebuilds before the alter, so it needs + // to be one more than the current item count to avoid removing twice. + $item_count = count($input_values); + $form_state->set($count_property, $item_count); + + $form_state->setRebuild(); } /** - * Returns single simple element from array. + * Submit function for element "move up" button. + * + * @param array $form + * Form render array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. */ - protected function buildSimpleArrayElement(array $definition, $data): array { - return array_filter([ - '#type' => 'textfield', - '#title' => $definition['schema']->items->title ?? NULL, - '#default_value' => $data, - ]); + public static function moveUp(array &$form, FormStateInterface $form_state) { + return static::moveElement($form_state, -1); } /** - * Returns single complex element from array. + * Submit function for element "move down" button. + * + * @param array $form + * Form render array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. */ - protected function buildComplexArrayElement(array $definition, $data, FormStateInterface $form_state, array $context): array { - $subdefinition = [ - 'name' => $definition['name'], - 'schema' => $definition['schema']->items, - ]; - return $this->objectHelper->handleObjectElement($subdefinition, $data, $form_state, $context, $this->builder); + public static function moveDown(array &$form, FormStateInterface $form_state) { + return static::moveElement($form_state, 1); + } + + /** + * Common function to move element within array by the given offset. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. + * @param int $offset + * Offset to move the element by. + */ + protected static function moveElement(FormStateInterface $form_state, int $offset) { + $button_element = $form_state->getTriggeringElement(); + $parent = $button_element['#attributes']['data-parent']; + $parents = $button_element['#parents']; + $element_index = str_replace("{$parent}-", '', $button_element['#name']); + $user_input = $form_state->getUserInput(); + + // Update the user input to change the order. + $key_exists = NULL; + static::trimParents($parents, $element_index); + $input_values = &NestedArray::getValue($user_input, $parents, $key_exists); + if ($key_exists) { + $moved_element = array_splice($input_values, $element_index, 1); + array_splice($input_values, $element_index + $offset, 0, $moved_element); + // Re-index the array to maintain proper keys. + $input_values = \array_values($input_values); + } + + $form_state->setUserInput($user_input); + $form_state->setRebuild(); + } + + /** + * Submit function for array "add one" button. + * + * @param array $form + * Form render array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Form state. + */ + public static function addOne(array &$form, FormStateInterface $form_state) { + $button_element = $form_state->getTriggeringElement(); + $count_property = static::buildCountProperty($button_element['#name']); + // Modify stored item count. + $item_count = $form_state->get($count_property) ?? 0; + $item_count++; + $form_state->set($count_property, $item_count); + $form_state->setRebuild(); + } + + /** + * Utility function to trim the triggering element's parents array. + * + * Used to get the correct position in the user input array for modifications. + * + * @param array $parents + * Parents array. + * @param int $element_index + * Element index. + */ + public static function trimParents(array &$parents, int $element_index): void { + for ($i = count($parents) - 1; $i >= 0; $i--) { + if ($parents[$i] == $element_index) { + $ei_position = $i; + break; + } + } + $offset = 0 - (count($parents) - $ei_position); + \array_splice($parents, $offset); } } diff --git a/modules/json_form_widget/src/FormBuilder.php b/modules/json_form_widget/src/FormBuilder.php index 0eef2582f6..38f2771117 100644 --- a/modules/json_form_widget/src/FormBuilder.php +++ b/modules/json_form_widget/src/FormBuilder.php @@ -68,7 +68,7 @@ public function __construct( SchemaRetriever $schema_retriever, FieldTypeRouter $router, SchemaUiHandler $schema_ui_handler, - LoggerInterface $loggerChannel + LoggerInterface $loggerChannel, ) { $this->schemaRetriever = $schema_retriever; $this->router = $router; diff --git a/modules/json_form_widget/src/Plugin/Field/FieldWidget/JsonFormWidget.php b/modules/json_form_widget/src/Plugin/Field/FieldWidget/JsonFormWidget.php index 57a632e976..3c260f3a6f 100644 --- a/modules/json_form_widget/src/Plugin/Field/FieldWidget/JsonFormWidget.php +++ b/modules/json_form_widget/src/Plugin/Field/FieldWidget/JsonFormWidget.php @@ -3,14 +3,14 @@ namespace Drupal\json_form_widget\Plugin\Field\FieldWidget; use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; use Drupal\json_form_widget\FormBuilder; +use Drupal\json_form_widget\ValueHandler; use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Field\FieldDefinitionInterface; use Symfony\Component\HttpFoundation\RequestStack; -use Drupal\json_form_widget\ValueHandler; /** * Plugin implementation of the 'json_form_widget'. @@ -82,7 +82,7 @@ public function __construct( array $third_party_settings, FormBuilder $builder, ValueHandler $value_handler, - RequestStack $request_stack + RequestStack $request_stack, ) { parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); $this->builder = $builder; diff --git a/modules/json_form_widget/src/SchemaUiHandler.php b/modules/json_form_widget/src/SchemaUiHandler.php index 8d47b9a5eb..b8e67532f6 100644 --- a/modules/json_form_widget/src/SchemaUiHandler.php +++ b/modules/json_form_widget/src/SchemaUiHandler.php @@ -66,7 +66,7 @@ public static function create(ContainerInterface $container) { public function __construct( SchemaRetriever $schema_retriever, LoggerInterface $loggerChannel, - WidgetRouter $widget_router + WidgetRouter $widget_router, ) { $this->schemaRetriever = $schema_retriever; $this->schemaUi = FALSE; @@ -199,7 +199,13 @@ public function flattenArrays(mixed $spec, array $element) { unset($element[$spec->child][$key]); } } - $element[$spec->child][0]['#default_value'] = $default_value; + + if (isset($element[$spec->child][0]['field'])) { + $element[$spec->child][0]['field']['#default_value'] = $default_value; + } + else { + $element[$spec->child][0]['#default_value'] = $default_value; + } return $element; } @@ -210,6 +216,9 @@ private function formatArrayDefaultValue($item) { if (!empty($item['#default_value'])) { return [$item['#default_value'] => $item['#default_value']]; } + if (!empty($item['field']['#default_value'])) { + return [$item['field']['#default_value'] => $item['field']['#default_value']]; + } return []; } diff --git a/modules/json_form_widget/src/ValueHandler.php b/modules/json_form_widget/src/ValueHandler.php index 8902f50533..79f4dfd94a 100644 --- a/modules/json_form_widget/src/ValueHandler.php +++ b/modules/json_form_widget/src/ValueHandler.php @@ -148,9 +148,12 @@ public function handleArrayValues($formValues, $property, $schema) { */ private function flattenArraysInArrays($value) { $data = []; + if (isset($value['actions'])) { + unset($value['actions']); + } if (is_array($value)) { foreach ($value as $item) { - $data[] = $this->cleanSelectId($item); + $data[] = is_array($item) ? $this->flattenArraysInArrays($item) : $this->cleanSelectId($item); } } elseif (!empty($value)) { diff --git a/modules/json_form_widget/src/WidgetRouter.php b/modules/json_form_widget/src/WidgetRouter.php index 79047a42d1..00a0663558 100644 --- a/modules/json_form_widget/src/WidgetRouter.php +++ b/modules/json_form_widget/src/WidgetRouter.php @@ -147,6 +147,7 @@ public function handleListElement(mixed $spec, array $element) { * The dropdown element configured. */ public function getDropdownElement(mixed $element, mixed $spec, mixed $titleProperty = FALSE) { + ArrayHelper::flattenArrayElementFieldset($element); $element['#type'] = $this->getSelectType($spec); $element['#options'] = $this->getDropdownOptions($spec->source, $titleProperty); if ($element['#type'] === 'select_or_other_select') { diff --git a/modules/json_form_widget/tests/src/Unit/ArrayHelperTest.php b/modules/json_form_widget/tests/src/Unit/ArrayHelperTest.php index 7819dc8bd4..2f88b400d8 100644 --- a/modules/json_form_widget/tests/src/Unit/ArrayHelperTest.php +++ b/modules/json_form_widget/tests/src/Unit/ArrayHelperTest.php @@ -2,20 +2,20 @@ namespace Drupal\Tests\json_form_widget\Unit; -use PHPUnit\Framework\TestCase; -use Drupal\json_form_widget\ArrayHelper; -use MockChain\Chain; use Drupal\Component\DependencyInjection\Container; use Drupal\Core\Form\FormState; use Drupal\Core\Logger\LoggerChannelFactory; use Drupal\Core\StringTranslation\TranslationManager; +use Drupal\json_form_widget\ArrayHelper; use Drupal\json_form_widget\FieldTypeRouter; use Drupal\json_form_widget\IntegerHelper; use Drupal\json_form_widget\ObjectHelper; use Drupal\json_form_widget\SchemaUiHandler; use Drupal\json_form_widget\StringHelper; use Drupal\metastore\SchemaRetriever; +use MockChain\Chain; use MockChain\Options; +use PHPUnit\Framework\TestCase; /** * Test class for ArrayHelper. @@ -79,7 +79,7 @@ public function testComplex() { $result = $array_helper->handleArrayElement($definition, [], $form_state, $context); $expected = $this->getExpectedComplexArrayElement(); unset($result['actions']); - unset($result['distribution'][0]['distribution']['schema']['schema']['fields']['actions']); + unset($result['distribution'][0]['distribution']['actions']); $this->assertEquals($expected, $result); } @@ -182,10 +182,10 @@ private function getExpectedObject() { "#required" => FALSE, ], ], - ] + ], ], ], - ] + ], ], ], "#required" => FALSE,