You are here

class ComponentSectionForm in Module Builder 8.3

Generic form for entering a section of data for a component.

This determines which properties of the component to show from the values of the entity type's code_builder annotation.

Hierarchy

Expanded class hierarchy of ComponentSectionForm

See also

\Drupal\module_builder\EntityHandler\ComponentSectionFormHandler

File

src/Form/ComponentSectionForm.php, line 22

Namespace

Drupal\module_builder\Form
View source
class ComponentSectionForm extends ComponentFormBase {

  /**
   * Gets the names of properties this form should show.
   *
   * @return string[]
   *   An array of property names.
   */
  protected function getFormComponentProperties(DataItem $data) {

    // Get the list of component properties this section form uses from the
    // handler, which gets them from the entity type annotation.
    $component_entity_type_id = $this->entity
      ->getEntityTypeId();
    $component_sections_handler = $this->entityTypeManager
      ->getHandler($component_entity_type_id, 'component_sections');
    $operation = $this
      ->getOperation();
    $component_properties_to_use = $component_sections_handler
      ->getSectionFormComponentProperties($operation);
    return $component_properties_to_use;
  }

  /**
   * Title callback.
   *
   * @see \Drupal\module_builder\Routing\ComponentRouteProvider
   */
  public function title(Request $request, $entity_type, $op, $title) {

    // Get the entity request parameter. We can't use it as a function parameter
    // because we want this to work with any entity type.
    $entity = $request->attributes
      ->get($entity_type);
    return $this
      ->t($title, [
      '%label' => $entity
        ->label(),
    ]);
  }

  /**
   * {@inheritdoc}
   */
  public function form(array $form, FormStateInterface $form_state) {
    $form = parent::form($form, $form_state);
    $form = $this
      ->componentPropertiesForm($form, $form_state);
    $form['#attached']['library'][] = 'module_builder/typed_data_defaults';
    $form['#attached']['drupalSettings']['moduleBuilder']['typedDataDefaults']['defaults'] = [];
    return $form;
  }

  /**
   * Add form elements for the specified component properties.
   *
   * @param $form
   *  The form array.
   * @param FormStateInterface $form_state
   *  The form state object.
   *
   * @return
   *  The form array.
   */
  protected function componentPropertiesForm($form, FormStateInterface $form_state) {
    if (!$form_state
      ->has('data')) {

      // The first time we show this form, create the typed data object.
      $component_data = $this
        ->getComponentDataObject();
      $first_load = TRUE;
      $form_state
        ->set('data', $component_data);
    }
    else {

      // During an AJAX rebuild, get the data from the form state.
      $component_data = $form_state
        ->get('data');
    }

    // Get the properties that this form section should show.
    $component_properties_to_use = $this
      ->getFormComponentProperties($component_data);
    if (!empty($first_load)) {

      // Warn about properties in the entity annotation that are not in the
      // data.
      $undefined_properties = array_diff($component_properties_to_use, array_keys($component_data
        ->getProperties()));
      foreach ($undefined_properties as $property_name) {
        $this
          ->messenger()
          ->addError(t("The property '@name' is not defined in Drupal Code Builder. You should ensure you are using an up-to-date version.", [
          '@name' => $property_name,
        ]));
      }
    }
    $component_properties_to_use = array_intersect($component_properties_to_use, array_keys($component_data
      ->getProperties()));

    // Set #tree on the data element.
    $form['module']['#tree'] = TRUE;
    foreach ($component_properties_to_use as $property_name) {
      $this
        ->buildFormElement($form['module'], $form_state, $component_data->{$property_name});
    }

    // Put the data back into the form state, as the building of the form
    // elements may have caused changes.
    $form_state
      ->set('data', $component_data);

    // Developer trapdoor: disable AJAX for easier debugging of the form.
    if (FALSE) {

      // TODO: AAAAAARGH why can't this be done with Iterator classes???
      static::removeAjax($form);
    }
    return $form;
  }

  /**
   * Helper to remove all ajax from the form.
   *
   * Use this when debugging, as if an ajax request is crashing, it's best to
   * turn ajax off and use normal submission to see the error messages
   * immediately rather than pick them out of the log.
   */
  protected static function removeAjax(&$element) {
    foreach ($element as $key => &$value) {
      if (is_array($value) && isset($value['#ajax'])) {
        unset($value['#ajax']);
      }
      if (is_array($value)) {
        static::removeAjax($value);
      }
    }
  }

  /**
   * Builds the form element for a data item.
   *
   * This is called recursively for complex and multi-valued data items.
   *
   * @param array &$form
   *   The parent form element (or the entire form), passed by reference. The
   *   data item's element is placed with an array key that is its machine
   *   name.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   * @param \MutableTypedData\Data\DataItem $data
   *   The data item.
   */
  protected function buildFormElement(&$form, FormStateInterface $form_state, DataItem $data) {
    $element = [];

    // Determine whether to handle multiple data as a single element or a set
    // of deltas.
    // Multiple-valued data gets a set of items that can be added and removed
    // with AJAX buttons...
    $use_multiple_deltas = $data
      ->isMultiple();

    // ... with exceptions: simple data with options that is multiple-
    // valued is just a SELECT element.
    if (!$data
      ->isComplex() && $data
      ->hasOptions()) {
      $use_multiple_deltas = FALSE;
    }

    // ... multiple simple data without options is shown as a text area.
    if (!$data
      ->isComplex()) {
      $use_multiple_deltas = FALSE;
    }

    // Case 1: multiple deltas each handled as a separate form element, within
    // a details wrapper.
    if ($use_multiple_deltas) {
      $element = $this
        ->buildMultipleDeltaFormElement($form, $form_state, $data);
    }
    elseif ($data
      ->getDefinition()
      ->isComplex()) {
      $element = $this
        ->buildComplexFormElement($form, $form_state, $data);
    }
    else {

      // Case 3A: element with options.
      if ($data
        ->hasOptions()) {
        $options = [];
        $options_have_descriptions = FALSE;
        foreach ($data
          ->getOptions() as $value => $option) {
          $options[$value] = $option
            ->getLabel();
          if ($description = $option
            ->getDescription()) {

            // Set the description on each value. This is a not-terribly-well
            // documented feature in FormAPI. This relies on us not clobbering
            // $element further on!
            $element[$value]['#description'] = $description;
            $options_have_descriptions = TRUE;
          }
        }
        $options_count = count($options);
        if ($data
          ->isMultiple()) {
          $element_type = 'checkboxes';

          // Build up a default value array for the checkboxes from the data's
          // delta items.
          $default_value = [];
          foreach ($data as $delta => $delta_item) {
            $default_value[$delta_item->value] = $delta_item->value;
          }
        }
        else {
          if ($options_count > 8 && !$options_have_descriptions) {
            $element_type = 'select';
            $default_value = $data->value;
          }
          else {
            $element_type = 'radios';
            $default_value = $data->value;
          }
        }
        natcasesort($options);
        $element += [
          '#type' => $element_type,
          '#title' => $data
            ->getLabel(),
          '#description' => $data
            ->getDescription(),
          '#default_value' => $default_value,
          '#options' => $options,
          // ARGH why isn't this happening automatically like it's supposed to?
          '#empty_option' => $data
            ->isRequired() ? $this
            ->t('- Select -') : $this
            ->t('- None -'),
          '#empty_value' => NULL,
        ];

        // Special handling for injected services: textfield with autocomplete.
        if (in_array($data
          ->getName(), [
          'injected_services',
          'container_services',
          'mocked_services',
        ])) {
          $element['#type'] = 'textfield';

          // This needs to be massive to allow lots of services!
          $element['#maxlength'] = 512;
          $element['#description'] .= ' ' . $this
            ->t("Enter a comma-separated list of names.");
          $element['#default_value'] = implode(', ', $default_value);
          $element['#autocomplete_route_name'] = 'module_builder.autocomplete';
          $element['#autocomplete_route_parameters'] = [
            'property_address' => $data
              ->getAddress(),
          ];

          // Remove the options, as it makes FormAPI think the value must be
          // compared against them.
          unset($element['#options']);
        }
        if ($data
          ->isVariantProperty()) {

          // Put this above the 'Update variant properties' button; compare
          // with the weight set on that.
          $element['#weight'] = -20;
          $wrapper_id = Html::getId($data
            ->getParent()
            ->getAddress() . '-mutable-wrapper');
          $variant_property_form_address = explode(':', $data
            ->getAddress());
          $variant_property_address = array_merge($variant_property_form_address);
          $values = $form_state
            ->getValues();
          $variant_value = NestedArray::getValue($values, $variant_property_address);
          if (isset($variant_value)) {
            $data
              ->set($variant_value);
          }
        }
      }
      elseif ($data
        ->getType() == 'boolean') {
        $element += [
          '#type' => 'checkbox',
          '#title' => $data
            ->getLabel(),
          '#description' => $data
            ->getDescription(),
          '#default_value' => $data->value,
        ];
      }
      elseif ($data
        ->isMultiple()) {
        $element += [
          '#type' => 'textarea',
          '#title' => $data
            ->getLabel(),
          '#description' => $data
            ->getDescription(),
          '#default_value' => implode("\n", $data
            ->export()),
        ];
      }
      else {
        $element += [
          '#type' => 'textfield',
          '#title' => $data
            ->getLabel(),
          '#description' => $data
            ->getDescription(),
          '#default_value' => $data->value,
        ];
      }

      // Make form elements required if the data is required, unless there is
      // a default, in which case, either the JS will set it, or data validation
      // will set it on submission, so there's no need to force the user to
      // enter something.
      if ($data
        ->isRequired() && !$data
        ->getDefault()) {
        $element['#required'] = TRUE;
      }
      $element['#attributes']['data-typed-data-address'] = $data
        ->getAddress();

      // dsm($data->getDefault());
      // Note need parentheses around the assignment because of precedence
      // relative to &&.
      if (($default = $data
        ->getDefault()) && $default
        ->getType() == 'expression') {
        $expression = $default
          ->getExpressionWithAbsoluteAddresses($data);

        // Prefix custom EL functions with the JS namespace.
        // TODO: Would be nice to get the names from the EL rather than
        // hardcode them!
        $expression = str_replace('get(', 'DataAddressExpressionLanguage.get(', $expression);
        $expression = str_replace('machineToClass(', 'DataAddressExpressionLanguage.machineToClass(', $expression);
        $expression = str_replace('machineToLabel(', 'DataAddressExpressionLanguage.machineToLabel(', $expression);
        $expression = str_replace('stripBefore(', 'DataAddressExpressionLanguage.stripBefore(', $expression);
        $dependencies = $default
          ->getDependencies();
        if (!empty($dependencies)) {

          // CHEAT; for now only ever one dependency!
          // TODO: this only works for addresses that go up only one level!
          $dependencies[0] = str_replace('..:', $data
            ->getParent()
            ->getAddress() . ':', $dependencies[0]);
        }
        $element['#attached']['drupalSettings']['moduleBuilder']['typedDataDefaults']['defaults'][$this
          ->getFormElementNameFromData($data)] = [
          'dependencies' => $dependencies,
          'expression' => $expression,
        ];
        foreach ($dependencies as $dependency) {
          $element['#attached']['drupalSettings']['moduleBuilder']['typedDataDefaults']['reactions'][$dependency] = $this
            ->getFormElementNameFromData($data);
        }
      }
    }
    $element['#tree'] = TRUE;
    $form_key = $data
      ->getName();
    $form[$form_key] = $element;
  }
  protected function getFormElementNameFromData($data_item) {
    $pieces = explode(':', $data_item
      ->getAddress());
    $name = array_shift($pieces);
    foreach ($pieces as $piece) {
      $name .= "[{$piece}]";
    }
    return $name;
  }

  /**
   * Builds a multi-valued form element.
   *
   * Helper for buildFormElement().
   *
   * Note that buildFormElement() is responsible for some attributes of the
   * element.
   */
  protected function buildMultipleDeltaFormElement(&$form, FormStateInterface $form_state, DataItem $data) {

    // Set up a wrapper for AJAX.
    $wrapper_id = Html::getId($data
      ->getAddress() . '-add-more-wrapper');

    // Use 'details' rather than 'container' so there's a visual indicator
    // of the multi-valued property.
    $element = [
      '#type' => 'details',
      '#title' => $data
        ->getLabel(),
      '#open' => TRUE,
      '#attributes' => [
        'id' => $wrapper_id,
      ],
    ];
    foreach ($data as $delta => $delta_item) {
      $this
        ->buildFormElement($element, $form_state, $delta_item);

      // Set the label on each delta item to differentiate it from the overall
      // element label.
      $element[$delta]['#title'] = $delta_item
        ->getLabel();

      // Doesn't work; see removeItemSubmit().
      // $element[':' . $delta_item->getName() . '_remove_button'] = [
      //   '#type' => 'submit',
      //   // Needs to be full address for uniquess in the whole form.
      //   '#name' => $data->getAddress() . '_remove_item',
      //   '#value' => t('Remove item'),
      //   // Hack?
      //   '#input' => $delta,
      //   '#limit_validation_errors' => [],
      //   '#submit' => ['::removeItemSubmit'],
      //   '#ajax' => [
      //     'callback' => '::itemButtonAjax',
      //     'wrapper' => $wrapper_id,
      //     'effect' => 'fade',
      //   ],
      // ];
    }
    if ($data
      ->mayAddItem()) {
      if (count($data)) {
        $button_label = $this
          ->t('Add another @label item', [
          '@label' => $data
            ->getLabel(),
        ]);
      }
      else {
        $button_label = $this
          ->t('Add a @label item', [
          '@label' => $data
            ->getLabel(),
        ]);
      }
      $element[':add_button'] = [
        '#type' => 'submit',
        // This allows FormAPI to figure out which button is the triggering
        // element. The name must be unique across all buttons in the form,
        // otherwise, the first matching name will be taken by FormAPI as being
        // the button that was clicked, with unexpected results.
        // See \Drupal\Core\Form\FormBuilder::elementTriggeredScriptedSubmission().
        '#name' => $data
          ->getAddress() . '_add_more',
        '#value' => $button_label,
        '#limit_validation_errors' => [],
        '#submit' => [
          '::addItemSubmit',
        ],
        '#data_address' => $data
          ->getAddress(),
        '#ajax' => [
          'callback' => '::itemButtonAjax',
          'wrapper' => $wrapper_id,
          'effect' => 'fade',
        ],
        '#prefix' => '<div>',
        '#suffix' => '</div>',
      ];
    }
    if (count($data)) {
      $element[':remove_button'] = [
        '#type' => 'submit',
        // Needs to be full address for uniquess in the whole form.
        '#name' => $data
          ->getAddress() . '_remove_item',
        '#value' => $this
          ->t('Remove last item'),
        '#limit_validation_errors' => [],
        '#submit' => [
          '::removeItemSubmit',
        ],
        '#data_address' => $data
          ->getAddress(),
        '#ajax' => [
          'callback' => '::itemButtonAjax',
          'wrapper' => $wrapper_id,
          'effect' => 'fade',
        ],
        '#prefix' => '<div>',
        '#suffix' => '</div>',
      ];
    }
    return $element;
  }

  /**
   * Builds a form element with multiple child elements.
   *
   * Helper for buildFormElement().
   *
   * Note that buildFormElement() is responsible for some attributes of the
   * element.
   */
  protected function buildComplexFormElement(&$form, FormStateInterface $form_state, DataItem $data) {

    // Set up a wrapper for AJAX.
    $wrapper_id = Html::getId($data
      ->getAddress() . '-complex-wrapper');
    $element = [
      '#type' => 'details',
      '#title' => $data
        ->getLabel(),
      '#open' => TRUE,
      '#attributes' => [
        'id' => $wrapper_id,
      ],
    ];

    // Don't show an optional and single-valued complex element until the user
    // requests it. This is to keep the form clear, and to prevent validation
    // errors of the complex element has required properties but the user
    // doesn't want it.
    if (!$data
      ->isRequired() && $data
      ->isEmpty() && !$data
      ->isDelta()) {
      $element[':add_button'] = [
        '#type' => 'submit',
        // This allows FormAPI to figure out which button is the triggering
        // element. The name must be unique across all buttons in the form,
        // otherwise, the first matching name will be taken by FormAPI as being
        // the button that was clicked, with unexpected results.
        // See \Drupal\Core\Form\FormBuilder::elementTriggeredScriptedSubmission().
        '#name' => $data
          ->getAddress() . '_add',
        '#value' => $this
          ->t('Add @component', [
          '@component' => $data
            ->getLabel(),
        ]),
        '#limit_validation_errors' => [],
        '#submit' => [
          '::addComplexDataSubmit',
        ],
        '#data_address' => $data
          ->getAddress(),
        '#ajax' => [
          'callback' => '::complexButtonAjax',
          'wrapper' => $wrapper_id,
          'effect' => 'fade',
        ],
        '#prefix' => '<div>',
        '#suffix' => '</div>',
      ];
      return $element;
    }
    foreach ($data as $data_item) {
      $this
        ->buildFormElement($element, $form_state, $data_item);
    }
    if (!$data
      ->isRequired() && !$data
      ->isEmpty() && !$data
      ->isDelta()) {
      $element[':remove_button'] = [
        '#type' => 'submit',
        // This allows FormAPI to figure out which button is the triggering
        // element. The name must be unique across all buttons in the form,
        // otherwise, the first matching name will be taken by FormAPI as being
        // the button that was clicked, with unexpected results.
        // See \Drupal\Core\Form\FormBuilder::elementTriggeredScriptedSubmission().
        '#name' => $data
          ->getAddress() . '_remove',
        '#value' => $this
          ->t('Remove @component', [
          '@component' => $data
            ->getLabel(),
        ]),
        '#limit_validation_errors' => [],
        '#submit' => [
          '::removeComplexDataSubmit',
        ],
        '#data_address' => $data
          ->getAddress(),
        '#ajax' => [
          'callback' => '::complexButtonAjax',
          'wrapper' => $wrapper_id,
          'effect' => 'fade',
        ],
        '#prefix' => '<div>',
        '#suffix' => '</div>',
      ];
    }

    // NO! has to go immediately after the variant property!
    if ($data
      ->isMutable()) {
      if (count($data
        ->getProperties()) == 1 && $data
        ->getVariantData()->value) {
        $element['count_notice'] = [
          '#type' => 'container',
          'notice' => [
            '#plain_text' => $this
              ->t("The @variant variant has no additional properties.", [
              '@variant' => $data
                ->getVariantData()
                ->getLabel(),
            ]),
          ],
        ];
      }

      // Set up a wrapper for AJAX.
      // Note that we don't to use Html::getUniqueId() because the data's
      // address is already unique, and furthermore, we don't WANT uniqueness
      // because we want the same data item to produce the same HTML ID when
      // we're looking at the variant property form element further on.
      // TODO: this assumes only one root data item in the form, as another
      // data item could have the same addresses!
      $wrapper_id = Html::getId($data
        ->getAddress() . '-mutable-wrapper');
      $element['#attributes'] = [
        'id' => $wrapper_id,
      ];

      // WARNING: assumes the form structure!
      $mutable_property_form_address = explode(':', $data
        ->getAddress());
      $variant_property_address = array_merge($mutable_property_form_address, [
        $data
          ->getVariantData()
          ->getName(),
      ]);
      $element[':update_variant'] = [
        '#type' => 'submit',
        // Needs to be full address for uniquess in the whole form.
        '#name' => $data
          ->getAddress() . '_update_variant',
        // TODO: customisable!
        '#value' => $data
          ->isEmpty() ? $this
          ->t('Set variant') : $this
          ->t('Change variant and delete data for this item'),
        // We need to validate the variant property so we get its value in the
        // submit handler for this button.
        '#limit_validation_errors' => [
          $variant_property_address,
        ],
        '#element_validate' => [
          '::updateVariantValidate',
        ],
        '#submit' => [
          '::updateVariantSubmit',
        ],
        '#data_address' => $data
          ->getVariantData()
          ->getAddress(),
        '#variant_data_name' => $data
          ->getVariantData()
          ->getName(),
        '#ajax' => [
          'callback' => '::variantButtonAjax',
          'wrapper' => $wrapper_id,
          'effect' => 'fade',
        ],
        '#prefix' => '<div>',
        '#suffix' => '</div>',
        '#weight' => -10,
      ];

      // Also tweak the weight of the variant property so it goes above the
      // button to change the variant.
      $variant_property_name = $data
        ->getVariantData()
        ->getName();
      $element[$variant_property_name]['#weight'] = -20;
    }
    return $element;
  }

  /**
   * Submission handler for the "Add another item" buttons.
   */
  public static function addItemSubmit(array $form, FormStateInterface $form_state) {
    $button = $form_state
      ->getTriggeringElement();

    // Get the data item, using the address set in the button.
    $data = $form_state
      ->get('data');
    $data_item = $data
      ->getItem($button['#data_address']);

    // Add a new delta item.
    $data_item
      ->createItem();
    $form_state
      ->set('data', $data);
    $form_state
      ->setRebuild();
  }

  /**
   * Submission handler for the "Remove item" buttons.
   */
  public static function removeItemSubmit(array $form, FormStateInterface $form_state) {
    $button = $form_state
      ->getTriggeringElement();

    // Get the data item, using the address set in the button.
    $data = $form_state
      ->get('data');
    $multiple_data_item = $data
      ->getItem($button['#data_address']);

    // dsm($multiple_data_item);
    // We could remove any of the items here, but the problem is then that
    // FormAPI appears to put the currently entered values back into the form
    // elements whose deltas have closed the gap, which makes it look like it
    // was the last one that was removed anyway.
    $last_delta = count($multiple_data_item) - 1;
    unset($multiple_data_item[$last_delta]);
    $form_state
      ->set('data', $data);
    $form_state
      ->setRebuild();
  }

  /**
   * Ajax callback for the item count buttons.
   *
   * This returns the new page content to replace the page content made obsolete
   * by the form submission.
   */
  public static function itemButtonAjax(array $form, FormStateInterface $form_state) {
    $button = $form_state
      ->getTriggeringElement();

    // Go up in the form, to the widgets container.
    $button_array_parents = $button['#array_parents'];
    $widgets_container_parents = array_slice($button_array_parents, 0, -1);
    $element = NestedArray::getValue($form, $widgets_container_parents);
    return $element;
  }
  public static function addComplexDataSubmit(array $form, FormStateInterface $form_state) {
    $button = $form_state
      ->getTriggeringElement();
    $data = $form_state
      ->get('data');
    $complex_data_item = $data
      ->getItem($button['#data_address']);

    // Access the data to cause it to instantiate.
    $complex_data_item
      ->access();
    $form_state
      ->set('data', $data);
    $form_state
      ->setRebuild();
  }
  public static function removeComplexDataSubmit(array $form, FormStateInterface $form_state) {
    $button = $form_state
      ->getTriggeringElement();
    $data = $form_state
      ->get('data');
    $complex_data_item = $data
      ->getItem($button['#data_address']);
    $complex_data_item
      ->unset();
    $form_state
      ->set('data', $data);
    $form_state
      ->setRebuild();
  }
  public static function complexButtonAjax(array $form, FormStateInterface $form_state) {
    $button = $form_state
      ->getTriggeringElement();

    // Go up in the form, to the widgets container.
    $button_array_parents = $button['#array_parents'];
    $widgets_container_parents = array_slice($button_array_parents, 0, -1);
    $element = NestedArray::getValue($form, $widgets_container_parents);
    return $element;
  }

  /**
   * Validate handler for the update variant button.
   *
   * This removes variant-dependent values if the variant has changed.
   */
  public static function updateVariantValidate(array $form, FormStateInterface $form_state) {
    $button = $form_state
      ->getTriggeringElement();

    // WTF FormAPI?
    // Why does this
    if (array_pop($button['#parents']) != ':update_variant') {
      return;
    }

    // Get the value of the mutable data variant property.
    $values_address = $button['#parents'];
    $values_address[] = $button['#variant_data_name'];
    $values = $form_state
      ->getValues();
    $submitted_variant_value = NestedArray::getValue($values, $values_address);

    // Checking the variant property form element is #required hasn't happened
    // yet at this point. So bail and let FormAPI set a form error.
    if (empty($submitted_variant_value)) {
      return;
    }

    // Get the containing variant data item, using the address set in the
    // button.
    $data = $form_state
      ->get('data');
    $variant_data_item = $data
      ->getItem($button['#data_address']);

    // Clean up the values if the current submission is changing the variant
    // property.
    // We can't determine whether this is happening by comparing the form
    // state's variant value with the data item's variant value, because
    // validateForm()'s call to CleanUpValues() has already set the values on
    // the data item, and then set that back in the form state. Which is ugly,
    // because it means a button validator such as this one doesn't have a
    // proper picture of what is going on. TODO: look at core entity forms and
    // see whether they set the entity back on the form after building it in
    // validation. A quick look suggests that they don't.
    $variant_properties = $variant_data_item
      ->getParent()
      ->getProperties();
    $complex_values_address = $button['#parents'];
    $complex_values = NestedArray::getValue($values, $complex_values_address);
    $cleaned_complex_values = array_intersect_key($complex_values, $variant_properties);
    NestedArray::setValue($values, $complex_values_address, $cleaned_complex_values);
    $form_state
      ->setValues($values);

    // TODO: this bit WOULD work if validateForm() weren't updating the data
    // item stored in the form state. As it stands, it does nothing because
    // the two values will be equal even when the variant is being changed.
    if ($submitted_variant_value != $variant_data_item->value) {
      $complex_values_address = $button['#parents'];
      $complex_values = NestedArray::getValue($values, $complex_values_address);
      $complex_values = array_intersect_key($complex_values, [
        $button['#variant_data_name'] => TRUE,
      ]);
      NestedArray::setValue($values, $complex_values_address, $complex_values);
      $form_state
        ->setValues($values);
    }
  }

  /**
   * Submission handler for the "Update variants" buttons.
   */
  public static function updateVariantSubmit(array $form, FormStateInterface $form_state) {
    $button = $form_state
      ->getTriggeringElement();

    // dsm($button);
    // Get the value of the mutable data variant property.
    $values_address = array_slice($button['#parents'], 0, -1);
    $values_address[] = $button['#variant_data_name'];

    // Get the containing variant data item, using the address set in the
    // button.
    $data = $form_state
      ->get('data');
    $variant_data_item = $data
      ->getItem($button['#data_address']);
    $values = $form_state
      ->getValues();
    $variant_value = NestedArray::getValue($values, $values_address);
    $variant_data_item->value = $variant_value;
    $form_state
      ->set('data', $data);

    // dsm($data);
    $form_state
      ->setRebuild();
  }

  /**
   * Ajax callback for the variant buttons.
   *
   * This returns the new page content to replace the page content made obsolete
   * by the form submission.
   */
  public static function variantButtonAjax(array $form, FormStateInterface $form_state) {
    $button = $form_state
      ->getTriggeringElement();

    // Go up in the form, to the widgets container.
    $button_array_parents = $button['#array_parents'];

    // Get the address of the containing multiple data item.
    // WARNING: this assumes the 'data' form element is at the top in the
    // form structure!
    $widgets_container_parents = array_slice($button_array_parents, 0, -1);
    $element = NestedArray::getValue($form, $widgets_container_parents);
    return $element;
  }

  /**
   * Ajax callback for the variant elements.
   *
   * This returns the new page content to replace the page content made obsolete
   * by the form submission.
   */
  public static function variantElementAjax(array $form, FormStateInterface $form_state) {
    $variant_element = $form_state
      ->getTriggeringElement();

    // Go up in the form, to the widgets container.
    $variant_element_array_parents = $variant_element['#array_parents'];

    // Get the address of the containing multiple data item.
    // WARNING: this assumes the 'data' form element is at the top in the
    // form structure!
    if ($variant_element['#type'] == 'radios') {
      $widgets_container_parents = array_slice($variant_element_array_parents, 0, -2);
    }
    elseif ($variant_element['#type'] == 'select') {
      $widgets_container_parents = array_slice($variant_element_array_parents, 0, -1);
    }
    $element = NestedArray::getValue($form, $widgets_container_parents);
    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    parent::validateForm($form, $form_state);
    $data = $form_state
      ->get('data');

    // EntityForm::submitForm() has already called $form_state->cleanValues().
    $data_values = $form_state
      ->getValue($data
      ->getName());

    // ARGH. Because FormValidator does general validation before element or
    // button validation, we call this BEFORE button-specific validation can do
    // things to clean up things specific to the button's action, such as
    // removing data for a changed variant.
    $this
      ->cleanUpValues($data_values, $data);

    // Clear the properties we use in this form, so we don't merge with what's
    // already there. Note that $data is initially loaded from the component
    // entity when the form is built.
    $component_properties_to_use = $this
      ->getFormComponentProperties($data);
    foreach ($component_properties_to_use as $property_name) {

      // dsm("CLEAR $property_name");
      $data
        ->removeItem($property_name);
    }
    try {
      $data
        ->set($data_values);
    } catch (InvalidInputException $e) {
      $form_state
        ->setError($form, $this
        ->t("There was a problem with the form data."));
    }

    // Validate the data and set any violations as form errors.
    // TODO: we've validating the whole data, some of which doesn't appear on
    // the form -- but there shouldn't be violations outside of the form, since
    // those would have been caught when their form page was saved! In theory!
    $violations = $data
      ->validate();
    foreach ($violations as $address => $violation_messages) {
      $form_address = explode(':', $address);
      $key_exists = NULL;
      $form_element = NestedArray::getValue($form, $form_address, $key_exists);

      // Some form elements group all the deltas of a data item together, such
      // as injected services and textareas. In that case, there is no element
      // for the actual delta, and the error should be set on the parent
      // address.
      if (!$key_exists) {
        array_pop($form_address);

        // dsm($form_address);
        $form_element = NestedArray::getValue($form, $form_address, $key_exists);
      }

      // Filter the violations to those elements shown on this form section.
      // The 2nd element of the address corresponds to the names in the
      // $component_properties_to_use array (since the 1st element is 'module).
      if (!in_array($form_address[1], $component_properties_to_use)) {
        continue;
      }
      foreach ($violation_messages as $violation_message) {
        $form_state
          ->setError($form_element, $violation_message);
      }
    }

    // TODO: not sure we should do this here! it means that element and button
    // validators have an incorrect picture of what is going on!
    // TODO: figure out why values filled in by validation don't make it to the
    // form at this point!
    $form_state
      ->set('data', $data);
  }

  /**
   * Copies top-level form values to entity properties
   *
   * This should not change existing entity properties that are not being edited
   * by this form.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity the current form should operate upon.
   * @param array $form
   *   A nested array of form elements comprising the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   */
  protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
    $data = $form_state
      ->get('data');

    // EntityForm::submitForm() has already called $form_state->cleanValues().
    $data_values = $form_state
      ->getValue($data
      ->getName());
    $this
      ->cleanUpValues($data_values, $data);

    // We need to preserve any data for properties not on this form, but
    // completely clear properties that are on this form so we don't merge into
    // any existing data.
    // Note that $data is initially loaded from the component entity when the
    // form is built.
    $component_properties_to_use = $this
      ->getFormComponentProperties($data);
    foreach ($component_properties_to_use as $property_name) {
      $data
        ->removeItem($property_name);
    }
    try {
      $data
        ->set($data_values);
    } catch (InvalidInputException $e) {
      watchdog_exception('module_builder', $e);
      $this
        ->messenger()
        ->addError($this
        ->t("There was a problem with the form data. The component was not saved."));
      return;
    }

    // Set the name and ID, which use form elements outside of MTD.
    // This only applies to the 'name' component section form.
    if ($form_state
      ->getValue('id')) {
      $data->root_name = $form_state
        ->getValue('id');
      $data->readable_name = $form_state
        ->getValue('name');
    }

    // Let validation fill in required values with defaults, which we didn't
    // mark as required in the form.
    $data
      ->validate();
    $data_export = $data
      ->export();
    $entity
      ->set('data', $data_export);
  }

  /**
   * Recursively clean up the submitted form values.
   *
   * Note that this is called by validateForm(), and so is run BEFORE any
   * button-specific validators. This means that it can't rely on any action-
   * specific clean up of values.
   *
   * @param array &$array
   *   An array of form values, passed by reference. This will be altered in
   *   place.
   * @param \MutableTypedData\Data\DataItem $data
   *   The data item that corresponds to the form values array.
   */
  protected function cleanUpValues(&$array, DataItem $data) {

    // Clean up the data for the old variant if mutable data is having its
    // variant changed.
    if ($data
      ->isMutable() && !$data
      ->isMultiple()) {
      $variant_property_name = $data
        ->getVariantData()
        ->getName();
      if ($array[$variant_property_name] != $data
        ->getVariantData()->value) {

        // The variant property has been changed. Remove everything from the
        // array except for the variant property.
        $array = [
          $variant_property_name => $array[$variant_property_name],
        ];
      }
    }
    foreach ($array as $key => &$value) {

      // Remove buttons.
      if (substr($key, 0, 1) == ':') {
        unset($array[$key]);
        continue;
      }

      // Single checkbox.
      // TODO ARRRGH can't figure out how to safely get a child item in all
      // circumstances.
      // See https://github.com/joachim-n/mutable-typed-data/issues/3
      if (!is_numeric($key) && $data->{$key}
        ->getType() == 'boolean') {
        $array[$key] = (bool) $array[$key];
        continue;
      }

      // Remove empty values, so for example, empty checkboxes in a set don't
      // try to set an empty value on the data item.
      if (is_null($value)) {
        unset($array[$key]);
        continue;
      }

      // Remove empty mutable data. This means that we remove the empty option
      // during an AJAX call to add a delta to a multi-valued mutable item,
      // which can't be set on the data as a variant can't be set to an empty
      // value.
      // TODO: needs test coverage.
      if ($data
        ->isMutable()) {
        if (empty($value)) {
          unset($array[$key]);
          continue;
        }
      }
      if (is_array($value)) {
        if (is_numeric($key)) {
          $this
            ->cleanUpValues($value, $data[$key]);
        }
        elseif ($data->{$key}
          ->hasOptions()) {

          // We're dealing with checkboxes. Convert the keyed array to an numeric
          // array.
          $value = array_values(array_filter($value));
        }
        else {
          $this
            ->cleanUpValues($value, $data->{$key});
        }

        // If the value array is now empty (because recursively cleaning it has
        // removed all its keys), remove it.
        if (empty($value)) {
          unset($array[$key]);
        }

        // For FKW reasons, the form state values get order mixed up when
        // the variant type is set on an additional variant. The new value is
        // first in the array, which MTD won't accept because it expects deltas
        // to be in the correct order. Values are in the right order in
        // updateVariantSubmit(), but in the wrong order when
        // when copyFormValuesToEntity() is called.
        // I've given up figuring out why FormAPI does this so here is a hack
        // to fix the values.
        // See https://www.drupal.org/project/module_builder/issues/3173604
        if (!empty($value) && is_numeric(array_keys($value)[0])) {
          ksort($value);
        }
      }
      else {

        // Some single values need special handling too.
        // Handle a textarea.
        if ($data->{$key}
          ->isMultiple() && !$data->{$key}
          ->isComplex() && !$data->{$key}
          ->hasOptions()) {

          // Text area line breaks are weird, apparently.
          $values = preg_split("@[\r\n]+@", $value);
          $values = array_filter($values);
          $array[$key] = $values;
        }
        if (in_array($data->{$key}
          ->getName(), [
          'injected_services',
          'container_services',
          'mocked_services',
        ])) {

          // Form elements for injected services need special handling.
          $value = preg_split("@[,]+@", $value);
          $value = array_filter($value);
          $array[$key] = array_map('trim', $value);
        }
        elseif (!is_numeric($key) && $data->{$key}
          ->hasOptions() && !$data->{$key}
          ->isRequired()) {

          // Options elements that are not required should be cleaned up, so we
          // don't set an empty string as the data value.
          unset($array[$key]);
        }
      }
    }
  }

  /**
   * Builds the form element for a component.
   *
   * This builds the root level form element, or an element for any part of
   * the property info array that is an array of properties. This is recursed
   * into by elementCompound().
   *
   * @param array $property_address
   *  The property address for the component. This is an array that gives the
   *  location of this component's properties list in the complete property info array
   *  in static::$componentDataInfo. For the root, this will be an empty array;
   *  for a child compound property this will be an address of the form
   *  parent->properties->child->properties.
   * @param array $value_address
   *  The value address for the form element to be created. This is similar to
   *  the property address, but will include items for compound property deltas.
   *  This ensures that buttons and item counts in form storage are unique for
   *  compound elements which are themselves children of multi-valued compound
   *  elements.
   * @param $form_value_address
   *  The form values address for the component. This is used to set the
   *  #parents property on the form element we create, so that the form values
   *  structure matches the original data structure. This is different again
   *  from the other two addresses, as it does not include a level for the
   *  'properties' array, but does include deltas.
   *
   * @return array
   *   The form array for the component's element.
   */

  // TODO: mine this and helpers for old code.
  private function getCompomentElement($form_state, $property_address, $value_address, $form_value_address) {
    $component_element = [];
    $properties = NestedArray::getValue(static::$componentDataInfo, $property_address);

    // TODO: should this be carried through? Check whether preparing a compound
    // property can set values in the array.
    $component_data = [];
    foreach ($properties as $property_name => &$property_info) {

      // Prepare the single property: get options, default value, etc.
      $this->codeBuilderTaskHandlerGenerate
        ->prepareComponentDataProperty($property_name, $property_info, $component_data);

      // Skip the properties that we're not showing on this form section.
      if (!empty($property_info['hidden'])) {
        continue;
      }

      // Add the name of the current property to the address arrays.
      $property_component_address = $property_address;
      $property_component_address[] = $property_name;
      $property_value_address = $value_address;
      $property_value_address[] = $property_name;
      $property_form_value_address = $form_value_address;
      $property_form_value_address[] = $property_name;

      // Create a basic form element for the property.
      $property_element = [
        '#title' => $property_info['label'],
        '#required' => $property_info['required'],
        '#mb_property_address' => $property_component_address,
        '#mb_value_address' => $property_value_address,
        // Explicitly set this so we control the structure of the form
        // submission values. In particular, we don't want to have to pick data
        // out from the the structure the 'table' element would create.
        '#parents' => $property_form_value_address,
      ];
      if (isset($property_info['description'])) {
        $property_element['#description'] = $property_info['description'];
      }

      // Add description to properties that can get defaults filled in by
      // DCB in processing.
      if (!empty($property_info['process_default'])) {
        $property_element['#required'] = FALSE;
        $property_element['#description'] = (isset($property_element['#description']) ? $property_element['#description'] . ' ' : '') . t("Leave blank for a default value.");
      }

      // Determine the default value to present in the form element.
      // (Compound elements don't have a default value as they are just
      // containers, but we use the count of the array we get to determine how
      // many deltas to show.)
      $key_exists = NULL;
      $form_default_value = NestedArray::getValue($this->moduleEntityData, array_slice($property_form_value_address, 1), $key_exists);

      // If there is no value set in the module entity data, take the default
      // value that prepareComponentDataProperty() set.
      if (!$key_exists) {
        $form_default_value = $component_data[$property_name];
        if ($property_info['format'] == 'compound') {

          // Bit of a hack: for compound properties, zap the prepared default.
          // The problem is that this will cause a child element to appear in
          // the form, rather than starting with a zero delta.
          // This happens for example with the PHPUnit test component, where
          // the prepared default for the test_modules property tries to set a
          // module name derived from the test class name.
          // This will be fixed in DCB 3.3.x when we get the ability to do
          // defaults in JS.
          $form_default_value = [];
        }
      }

      // The type of the form element depends on the format of the component data
      // property.
      $format = $property_info['format'];
      $format_method = 'element' . ucfirst($format);
      if (!method_exists($this, $format_method)) {
        throw new \Exception("No method '{$format_method}' exists to handle property '{$property_name}' with format '{$format}'.");
        continue;
      }
      $handling = $this
        ->{$format_method}($property_element, $form_state, $property_info, $form_default_value);
      $property_form_value_address_key = implode(':', $property_form_value_address);
      $form_state
        ->set([
        'element_handling',
        $property_form_value_address_key,
      ], $handling);
      $component_element[$property_name] = $property_element;
    }
    return $component_element;
  }

  /**
   * Set form element properties specific to array component properties.
   *
   * @param &$element
   *  The form element for the component property.
   * @param FormStateInterface $form_state
   *  The form state.
   * @param $property_info
   *  The info array for the component property.
   * @param $form_default_value
   *  The default value for the form element.
   *
   * @return string
   *  The handling type to be applied to this element's value on submit.
   */
  protected function XelementArray(&$element, FormStateInterface $form_state, $property_info, $form_default_value) {
    if (isset($property_info['options'])) {
      if (isset($property_info['options_extra'])) {

        // Show an autocomplete textfield.
        // TODO: use Select or Other module for this when it has a stable
        // release.
        $element['#type'] = 'textfield';
        $element['#maxlength'] = 512;
        $element['#description'] = (isset($element['#description']) ? $element['#description'] . ' ' : '') . t("Enter multiple values separated with a comma.");
        $element['#autocomplete_route_name'] = 'module_builder.autocomplete';
        $element['#autocomplete_route_parameters'] = [
          'property_address' => implode(':', $element['#mb_property_address']),
        ];
        if ($form_default_value) {
          $form_default_value = implode(', ', $form_default_value);
        }
        $handling = 'autocomplete';
      }
      else {
        $element['#type'] = 'checkboxes';
        $element['#options'] = $property_info['options'];
        if (is_null($form_default_value)) {
          $form_default_value = [];
        }
        else {
          $form_default_value = array_combine($form_default_value, $form_default_value);
        }
        $handling = 'checkboxes';
      }
    }
    else {
      $element['#type'] = 'textarea';
      if (isset($element['#description'])) {
        $element['#description'] .= ' ';
      }
      else {
        $element['#description'] = '';
      }
      $element['#description'] .= t("Enter one item per line.");

      // Handle a property that DCB has added since the component was saved.
      if (empty($form_default_value) && !is_array($form_default_value)) {
        $form_default_value = [];
      }
      $form_default_value = implode("\n", $form_default_value);
      $handling = 'textarea';
    }
    $element['#default_value'] = $form_default_value;
    return $handling;
  }

  /**
   * Set form element properties specific to boolean component properties.
   *
   * @param &$element
   *  The form element for the component property.
   * @param FormStateInterface $form_state
   *  The form state.
   * @param $property_info
   *  The info array for the component property.
   * @param $form_default_value
   *  The default value for the form element.
   *
   * @return string
   *  The handling type to be applied to this element's value on submit.
   */
  protected function XelementBoolean(&$element, FormStateInterface $form_state, $property_info, $form_default_value) {
    $element['#type'] = 'checkbox';
    $element['#default_value'] = $form_default_value;
    return 'checkbox';
  }

  /**
   * Set form element properties specific to compound component properties.
   *
   * @param &$element
   *  The form element for the component property.
   * @param FormStateInterface $form_state
   *  The form state.
   * @param $property_info
   *  The info array for the component property.
   * @param $form_default_value
   *  The default value for the form element.
   *
   * @return string
   *  The handling type to be applied to this element's value on submit.
   */
  protected function XelementCompound(&$element, FormStateInterface $form_state, $property_info, $form_default_value) {

    // A compound property shows a details element, for which we recurse and
    // show another component.
    $element['#type'] = 'details';
    $element['#open'] = TRUE;

    // Figure out how many items to show.
    // If we're reloading the form in response to the 'add more' button, then
    // form storage dictates the item count.
    // If there's nothing set in form storage yet, it's the first time we're
    // here and the number of items in the entity tells us how many items to
    // show in the form.
    // Finally, if that's empty, then show no items, just a button to add one.
    $item_count = static::getCompoundPropertyItemCount($form_state, $element['#mb_value_address']);
    if (is_null($item_count)) {
      $item_count = count($form_default_value);
      static::setCompoundPropertyItemCount($form_state, $element['#mb_value_address'], $item_count);
    }
    if (empty($item_count)) {
      $item_count = 0;
      static::setCompoundPropertyItemCount($form_state, $element['#mb_value_address'], $item_count);
    }

    // Property cardinality overrides anything else.
    if (isset($property_info['cardinality'])) {
      $item_count = min($item_count, $property_info['cardinality']);
      if ($item_count == $property_info['cardinality']) {

        // We're at the maximum item count.
        $add_more = FALSE;
      }
      else {

        // We're not yet at the cardinality: we can add more.
        $add_more = TRUE;
      }
    }
    else {

      // Unlimited cardinality: can always add more.
      $add_more = TRUE;
    }

    // Set up a wrapper for AJAX.
    $wrapper_id = Html::getUniqueId(implode('-', $element['#mb_value_address']) . '-add-more-wrapper');

    // TODO - use   '#type' => 'container',?
    $element['#prefix'] = '<div id="' . $wrapper_id . '">';
    $element['#suffix'] = '</div>';

    // Show the items in a table. This is single-column, with all child
    // properties in the one cell, but we just want the striping for visual
    // clarity.
    $element['table'] = array(
      '#type' => 'table',
    );

    // The address in the properties array to find this component's properties
    // list.
    $component_properties_address = $element['#mb_property_address'];
    $component_properties_address[] = 'properties';
    $component_value_address = $element['#mb_value_address'];
    $component_value_address[] = 'properties';
    $property_form_value_address = $element['#parents'];
    for ($delta = 0; $delta < $item_count; $delta++) {
      $row = [];
      $delta_value_address = $component_value_address;
      $delta_value_address[] = $delta;
      $delta_form_value_address = $property_form_value_address;
      $delta_form_value_address[] = $delta;

      // Put all the properties into a single cell so it's a 1-column table.
      // TODO: WTF NO STRIPING IN SEVEN THEME???
      $delta_component_element = $this
        ->getCompomentElement($form_state, $component_properties_address, $delta_value_address, $delta_form_value_address, []);
      $row['row'] = $delta_component_element;
      $element['table'][$delta] = $row;
    }
    if ($add_more) {

      // Show a button to add items, if they can be added.
      $button_text = $item_count == 0 ? t('Add a @label item', [
        '@label' => strtolower($property_info['label']),
      ]) : t('Add another @label item', [
        '@label' => strtolower($property_info['label']),
      ]);
      $element['actions']['add'] = array(
        '#type' => 'submit',
        // This allows FormAPI to figure out which button is the triggering
        // element. The name must be unique across all buttons in the form,
        // otherwise, the first matching name will be taken by FormAPI as being
        // the button that was clicked, with unexpected results.
        // See \Drupal\Core\Form\FormBuilder::elementTriggeredScriptedSubmission().
        '#name' => implode(':', $element['#mb_value_address']) . '_add_more',
        '#value' => $button_text,
        '#limit_validation_errors' => [],
        '#submit' => array(
          array(
            get_class($this),
            'addItemSubmit',
          ),
        ),
        '#ajax' => array(
          'callback' => array(
            get_class($this),
            'itemButtonAjax',
          ),
          'wrapper' => $wrapper_id,
          'effect' => 'fade',
        ),
      );
    }
    if ($item_count > 0) {
      $element['actions']['remove'] = [
        '#type' => 'submit',
        '#name' => implode(':', $element['#mb_value_address']) . '_remove_item',
        '#value' => t('Remove last item'),
        '#limit_validation_errors' => [],
        '#submit' => array(
          array(
            get_class($this),
            'removeItemSubmit',
          ),
        ),
        '#ajax' => array(
          'callback' => array(
            get_class($this),
            'itemButtonAjax',
          ),
          'wrapper' => $wrapper_id,
          'effect' => 'fade',
        ),
      ];
    }
    return 'compound';
  }

  /**
   * Set form element properties specific to array component properties.
   *
   * @param &$element
   *  The form element for the component property.
   * @param FormStateInterface $form_state
   *  The form state.
   * @param $property_info
   *  The info array for the component property.
   * @param $form_default_value
   *  The default value for the form element.
   *
   * @return string
   *  The handling type to be applied to this element's value on submit.
   */
  protected function XelementString(&$element, FormStateInterface $form_state, $property_info, $form_default_value) {
    if (isset($property_info['options'])) {
      $element['#type'] = 'select';
      $options = [];
      $element['#options'] = $property_info['options'];
      $element['#empty_value'] = '';
      if (empty($form_default_value)) {
        $form_default_value = '';
      }
      $handling = 'select';
    }
    else {
      $element['#type'] = 'textfield';
      $handling = 'textfield';
    }
    $element['#default_value'] = $form_default_value;
    return $handling;
  }

  /**
   * Returns an array of supported actions for the current entity form.
   */
  protected function actions(array $form, FormStateInterface $form_state) {

    // TODO: remove #mb_action, use #name instead.
    $actions['submit'] = array(
      '#type' => 'submit',
      '#value' => $this
        ->t('Save'),
      '#dropbutton' => 'mb',
      // Still no way to get a button's name, apparently?
      '#mb_action' => 'submit',
      '#submit' => array(
        '::submitForm',
        '::save',
      ),
    );
    if ($this
      ->getNextLink() != 'generate-form') {
      $actions['submit_next'] = array(
        '#type' => 'submit',
        '#value' => $this
          ->t('Save and go to next page'),
        '#dropbutton' => 'mb',
        '#mb_action' => 'submit_next',
        '#submit' => array(
          '::submitForm',
          '::save',
        ),
      );
    }
    $actions['submit_generate'] = array(
      '#type' => 'submit',
      '#value' => $this
        ->t('Save and generate code'),
      '#dropbutton' => 'mb',
      '#mb_action' => 'submit_generate',
      '#submit' => array(
        '::submitForm',
        '::save',
      ),
    );
    return $actions;
  }

  /**
   * Get the value for a property from the form values.
   *
   * This performs various processing depending on the form element type and the
   * property format:
   *  - explode textarea values
   *  - filter checkboxes and store only the keys
   *  - recurse into compound properties
   * The form build process leaves instructions for how to handle each value in
   * the 'element_handling' form state setting, so that here we don't need to
   * repeat the logic based on property info. Furthermore, we can't put this
   * a property info array into form state storage, because it contains closures,
   * which don't survive the serialization process in the database, and so the
   * property info would need to be run through DCB's preparation process all
   * over again.
   *
   * @param array $value_address
   *  The address array of the value in the form state values array. The final
   *  element of this is name of the property and the form element.
   * @param $value
   *  The incoming form value from the form element for this property.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return
   *  The processed value.
   */

  // TODO: check for stuff here to move to cleanUpValues().
  protected function XgetFormElementValue($value_address, $value, FormStateInterface $form_state) {

    // Retrieve the handling type from the form state.
    $property_form_value_address_key = implode(':', $value_address);
    $handling = $form_state
      ->get([
      'element_handling',
      $property_form_value_address_key,
    ]);
    switch ($handling) {
      case 'textarea':

        // Array format, without options: textarea.
        if (empty($value)) {
          $value = [];
        }
        else {

          // Can't split on just "\n" because for FKW reasons, linebreaks come
          // back through POST as Windows-style "\r\n".
          $value = preg_split("@[\r\n]+@", $value);
        }
        break;
      case 'autocomplete':

        // Array format, with extra options: textfield with autocomplete.
        // Only explode a non-empty string, as explode() will turn '' into an
        // array!
        if (!empty($value)) {

          // Textfield with autocomplete.
          $value = preg_split("@,\\s*@", $value);
        }
        break;
      case 'checkboxes':

        // Array format, with options: checkboxes.
        // Filter out empty values. (FormAPI *still* doesn't do this???)
        $value = array_filter($value);

        // Don't store values also in the keys, as some of these have dots in
        // them, which ConfigAPI doesn't allow.
        $value = array_keys($value);
        break;
      case 'compound':

        // Remove the item count buttons from the values.
        unset($value['actions']);
        unset($value['table']);
        foreach ($value as $delta => $item_value) {
          $delta_value_address = $value_address;
          $delta_value_address[] = $delta;

          // Recurse into the child property values.
          foreach ($item_value as $child_key => $child_value) {
            $delta_child_value_address = $delta_value_address;
            $delta_child_value_address[] = $child_key;
            $value[$delta][$child_key] = $this
              ->getFormElementValue($delta_child_value_address, $child_value, $form_state);
          }
        }
        break;
      case 'checkbox':
      case 'select':
      case 'textfield':

        // Nothing to do in these cases: $value is fine as it is.
        break;
      default:
        throw new \Exception("Unknown handling type: {$handling}.");
    }
    return $value;
  }

  /**
   * {@inheritdoc}
   */
  public function save(array $form, FormStateInterface $form_state) {
    $is_new = $this->entity
      ->isNew();
    $module = $this->entity;

    // dsm($module);
    $status = $module
      ->save();
    if ($status) {

      // Setting the success message.
      $this
        ->messenger()
        ->addStatus($this
        ->t('Saved the module: @name.', array(
        '@name' => $module->name,
      )));
    }
    else {
      $this
        ->messenger()
        ->addStatus($this
        ->t('The @name module was not saved.', array(
        '@name' => $module->name,
      )));
    }

    // Optionally advance to next tab or go to the generate page.
    $element = $form_state
      ->getTriggeringElement();
    switch ($element['#mb_action']) {
      case 'submit':
        $operation = $this
          ->getOperation();

        // For a new module, we need to redirect to its edit form, as staying
        // put would leave on the add form.
        if ($operation == 'add') {
          $operation = 'edit';
        }

        // For an existing module, we also redirect so that changing the machine
        // name of the module goes to the new URL.
        $url = $module
          ->toUrl($operation . '-form');
        $form_state
          ->setRedirectUrl($url);
        break;
      case 'submit_next':
        $next_link = $this
          ->getNextLink();
        $url = $module
          ->toUrl($next_link);
        $form_state
          ->setRedirectUrl($url);
        break;
      case 'submit_generate':
        $url = $module
          ->toUrl('generate-form');
        $form_state
          ->setRedirectUrl($url);
        break;
    }
  }

  /**
   * Get the next entity link after the one for the current form.
   *
   * @return
   *  The name of an entity link.
   */
  protected function getNextLink() {

    // Probably a more elegant way of figuring out where we currently are
    // with routes maybe?
    $operation = $this
      ->getOperation();

    // Special case for add and edit forms.
    if ($operation == 'default' || $operation == 'edit') {
      $operation = 'name';
    }
    $handler_class = $this->entityTypeManager
      ->getHandler('module_builder_module', 'component_sections');
    $form_ops = $handler_class
      ->getFormOperations();

    // Add in the 'name' operation, as the handler doesn't return it.
    $form_ops = array_merge([
      'name',
    ], $form_ops);
    $index = array_search($operation, $form_ops);
    return $form_ops[$index + 1] . '-form';
  }

}

Members

Namesort descending Modifiers Type Description Overrides
ComponentFormBase::$codeBuilder protected property The Drupal Code Builder wrapping service.
ComponentFormBase::$codeBuilderTaskHandlerGenerate protected property The DCB Generate Task handler.
ComponentFormBase::$sanityException protected property The exception thrown by DCB when initialized, if any.
ComponentFormBase::buildForm public function Form constructor. Overrides EntityForm::buildForm
ComponentFormBase::create public static function Instantiates a new instance of this class. Overrides FormBase::create
ComponentFormBase::getComponentDataObject protected function Gets the data object for the entity in the form.
ComponentFormBase::setEntity public function Sets the form entity. Overrides EntityForm::setEntity
ComponentFormBase::setGenerateTask public function Sets the generate task.
ComponentFormBase::__construct function Construct a new form object
ComponentSectionForm::actions protected function Returns an array of supported actions for the current entity form. Overrides EntityForm::actions 1
ComponentSectionForm::addComplexDataSubmit public static function
ComponentSectionForm::addItemSubmit public static function Submission handler for the "Add another item" buttons.
ComponentSectionForm::buildComplexFormElement protected function Builds a form element with multiple child elements.
ComponentSectionForm::buildFormElement protected function Builds the form element for a data item.
ComponentSectionForm::buildMultipleDeltaFormElement protected function Builds a multi-valued form element.
ComponentSectionForm::cleanUpValues protected function Recursively clean up the submitted form values.
ComponentSectionForm::complexButtonAjax public static function
ComponentSectionForm::componentPropertiesForm protected function Add form elements for the specified component properties.
ComponentSectionForm::copyFormValuesToEntity protected function Copies top-level form values to entity properties Overrides EntityForm::copyFormValuesToEntity 2
ComponentSectionForm::form public function Gets the actual form array to be built. Overrides EntityForm::form 3
ComponentSectionForm::getCompomentElement private function
ComponentSectionForm::getFormComponentProperties protected function Gets the names of properties this form should show. 3
ComponentSectionForm::getFormElementNameFromData protected function
ComponentSectionForm::getNextLink protected function Get the next entity link after the one for the current form.
ComponentSectionForm::itemButtonAjax public static function Ajax callback for the item count buttons.
ComponentSectionForm::removeAjax protected static function Helper to remove all ajax from the form.
ComponentSectionForm::removeComplexDataSubmit public static function
ComponentSectionForm::removeItemSubmit public static function Submission handler for the "Remove item" buttons.
ComponentSectionForm::save public function Form submission handler for the 'save' action. Overrides EntityForm::save
ComponentSectionForm::title public function Title callback.
ComponentSectionForm::updateVariantSubmit public static function Submission handler for the "Update variants" buttons.
ComponentSectionForm::updateVariantValidate public static function Validate handler for the update variant button.
ComponentSectionForm::validateForm public function Form validation handler. Overrides FormBase::validateForm 1
ComponentSectionForm::variantButtonAjax public static function Ajax callback for the variant buttons.
ComponentSectionForm::variantElementAjax public static function Ajax callback for the variant elements.
ComponentSectionForm::XelementArray protected function Set form element properties specific to array component properties.
ComponentSectionForm::XelementBoolean protected function Set form element properties specific to boolean component properties.
ComponentSectionForm::XelementCompound protected function Set form element properties specific to compound component properties.
ComponentSectionForm::XelementString protected function Set form element properties specific to array component properties.
ComponentSectionForm::XgetFormElementValue protected function
DependencySerializationTrait::$_entityStorages protected property An array of entity type IDs keyed by the property name of their storages.
DependencySerializationTrait::$_serviceIds protected property An array of service IDs keyed by property name used for serialization.
DependencySerializationTrait::__sleep public function 1
DependencySerializationTrait::__wakeup public function 2
EntityForm::$entity protected property The entity being used by this form. 7
EntityForm::$entityTypeManager protected property The entity type manager. 3
EntityForm::$moduleHandler protected property The module handler service.
EntityForm::$operation protected property The name of the current operation.
EntityForm::$privateEntityManager private property The entity manager.
EntityForm::actionsElement protected function Returns the action form element for the current entity form.
EntityForm::afterBuild public function Form element #after_build callback: Updates the entity with submitted data.
EntityForm::buildEntity public function Builds an updated entity object based upon the submitted form values. Overrides EntityFormInterface::buildEntity 2
EntityForm::getBaseFormId public function Returns a string identifying the base form. Overrides BaseFormIdInterface::getBaseFormId 5
EntityForm::getEntity public function Gets the form entity. Overrides EntityFormInterface::getEntity
EntityForm::getEntityFromRouteMatch public function Determines which entity will be used by this form from a RouteMatch object. Overrides EntityFormInterface::getEntityFromRouteMatch 1
EntityForm::getFormId public function Returns a unique string identifying the form. Overrides FormInterface::getFormId 10
EntityForm::getOperation public function Gets the operation identifying the form. Overrides EntityFormInterface::getOperation
EntityForm::init protected function Initialize the form state and the entity before the first form build. 3
EntityForm::prepareEntity protected function Prepares the entity object before the form is built first. 3
EntityForm::prepareInvokeAll protected function Invokes the specified prepare hook variant.
EntityForm::processForm public function Process callback: assigns weights and hides extra fields.
EntityForm::setEntityManager public function Sets the entity manager for this form. Overrides EntityFormInterface::setEntityManager
EntityForm::setEntityTypeManager public function Sets the entity type manager for this form. Overrides EntityFormInterface::setEntityTypeManager
EntityForm::setModuleHandler public function Sets the module handler for this form. Overrides EntityFormInterface::setModuleHandler
EntityForm::setOperation public function Sets the operation for this form. Overrides EntityFormInterface::setOperation
EntityForm::submitForm public function This is the default entity object builder function. It is called before any other submit handler to build the new entity object to be used by the following submit handlers. At this point of the form workflow the entity is validated and the form state… Overrides FormInterface::submitForm 17
EntityForm::__get public function
EntityForm::__set public function
FormBase::$configFactory protected property The config factory. 1
FormBase::$requestStack protected property The request stack. 1
FormBase::$routeMatch protected property The route match.
FormBase::config protected function Retrieves a configuration object.
FormBase::configFactory protected function Gets the config factory for this form. 1
FormBase::container private function Returns the service container.
FormBase::currentUser protected function Gets the current user.
FormBase::getRequest protected function Gets the request object.
FormBase::getRouteMatch protected function Gets the route match.
FormBase::logger protected function Gets the logger for a specific channel.
FormBase::redirect protected function Returns a redirect response object for the specified route. Overrides UrlGeneratorTrait::redirect
FormBase::resetConfigFactory public function Resets the configuration factory.
FormBase::setConfigFactory public function Sets the config factory for this form.
FormBase::setRequestStack public function Sets the request stack object to use.
LinkGeneratorTrait::$linkGenerator protected property The link generator. 1
LinkGeneratorTrait::getLinkGenerator Deprecated protected function Returns the link generator.
LinkGeneratorTrait::l Deprecated protected function Renders a link to a route given a route name and its parameters.
LinkGeneratorTrait::setLinkGenerator Deprecated public function Sets the link generator service.
LoggerChannelTrait::$loggerFactory protected property The logger channel factory service.
LoggerChannelTrait::getLogger protected function Gets the logger for a specific channel.
LoggerChannelTrait::setLoggerFactory public function Injects the logger channel factory.
MessengerTrait::$messenger protected property The messenger. 29
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.
RedirectDestinationTrait::$redirectDestination protected property The redirect destination service. 1
RedirectDestinationTrait::getDestinationArray protected function Prepares a 'destination' URL query parameter for use with \Drupal\Core\Url.
RedirectDestinationTrait::getRedirectDestination protected function Returns the redirect destination service.
RedirectDestinationTrait::setRedirectDestination public function Sets the redirect destination service.
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.
UrlGeneratorTrait::$urlGenerator protected property The url generator.
UrlGeneratorTrait::getUrlGenerator Deprecated protected function Returns the URL generator service.
UrlGeneratorTrait::setUrlGenerator Deprecated public function Sets the URL generator service.
UrlGeneratorTrait::url Deprecated protected function Generates a URL or path for a specific route based on the given parameters.