ComponentSectionForm.php in Module Builder 8.3
Namespace
Drupal\module_builder\FormFile
src/Form/ComponentSectionForm.phpView source
<?php
namespace Drupal\module_builder\Form;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use MutableTypedData\Data\DataItem;
use MutableTypedData\Exception\InvalidInputException;
use Symfony\Component\HttpFoundation\Request;
/**
* 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.
*
* @see \Drupal\module_builder\EntityHandler\ComponentSectionFormHandler
*/
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';
}
}
Classes
Name | Description |
---|---|
ComponentSectionForm | Generic form for entering a section of data for a component. |