Skip to content

Instantly share code, notes, and snippets.

@msankhala
Forked from Eyal-Shalev/ComboForm.php
Created June 20, 2021 05:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save msankhala/6820593c4ce202810c81aa452e718f70 to your computer and use it in GitHub Desktop.
Save msankhala/6820593c4ce202810c81aa452e718f70 to your computer and use it in GitHub Desktop.
An example form object (Drupal 8) that combines multiple forms into itself.
<?php
namespace Drupal\sandbox\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\node\Entity\Node;
use Drupal\user\Entity\User;
class ComboForm extends FormBase {
/**
* This constant is used as a key inside the main form state object to gather
* all the inner form state objects.
* @const
* @see getInnerFormState()
*/
const INNER_FORM_STATE_KEY = 'inner_form_state';
const MAIN_SUBMIT_BUTTON = 'submit';
/**
* @var \Drupal\Core\Form\FormInterface[]
*/
protected $innerForms = [];
/**
* The ComboForm constructor needs to initialize the inner form objects
* that this form will later use.
*
* Because both the user and the node forms are entity forms then an entity
* object needs to be assigned to them.
*
* Because the node entity has bundle, the bundle (type) must be defined
* before the form is generated.
*
* @TODO If the node type is customizable then a custom controller is needed.
*
* @TODO should this form allow edits? If so then the form objects should
* be assigned with the edited entities.
*/
public function __construct() {
$this->innerForms['user'] = \Drupal::entityTypeManager()
->getFormObject('user', 'default')
->setEntity(User::create());
$this->innerForms['node'] = \Drupal::entityTypeManager()
->getFormObject('node', 'default')
->setEntity(Node::create([
'type' => 'article'
]));
}
/**
* {@inheritDoc}
*/
public function getFormId() {
return implode('__', [
'combo_form',
$this->innerForms['user']->getFormId(),
$this->innerForms['node']->getFormId()
]);
}
/**
* The build form needs to take care of the following:
* - Creating a custom form state object for each inner form (and keep it
* inside the main form state.
* - Generating a render array for each inner form.
* - Handle compatibility issues such as #process array and action elements.
*
* {@inheritDoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['#process'] = \Drupal::service('element_info')->getInfoProperty('form', '#process', []);
$form['#process'][] = '::processForm';
foreach ($this->innerForms as $key => $inner_form_object) {
$inner_form_state = static::createInnerFormState($form_state, $inner_form_object, $key);
// By placing the actual inner form inside a container element (such as
// details) we gain the freedom to alter the wrapper of the inner form
// with little damage to the render element attributes of the inner form.
$inner_form = ['#parents' => [$key]];
$inner_form = $inner_form_object->buildForm($inner_form, $inner_form_state);
$form[$key] = [
'#type' => 'details',
'#title' => $this->t('Inner form: %key', ['%key' => $key]),
'form' => $inner_form
];
$form[$key]['form']['#type'] = 'container';
$form[$key]['form']['#theme_wrappers'] = \Drupal::service('element_info')->getInfoProperty('container', '#theme_wrappers', []);
unset($form[$key]['form']['form_token']);
// The process array is called from the FormBuilder::doBuildForm method
// with the form_state object assigned to the this (ComboForm) object.
// This results in a compatibility issues because these methods should
// be called on the inner forms (with their assigned FormStates).
// To resolve this we move the process array in the inner_form_state
// object.
if (!empty($form[$key]['form']['#process'])) {
$inner_form_state->set('#process', $form[$key]['form']['#process']);
unset($form[$key]['form']['#process']);
}
else {
$inner_form_state->set('#process', []);
}
// The actions array causes a UX problem because there should only be a
// single save button and not multiple.
// The current solution is to move the #submit callbacks of the submit
// element to the inner form element root.
if (!empty($form[$key]['form']['actions'])) {
if (isset($form[$key]['form']['actions'][static::MAIN_SUBMIT_BUTTON])) {
$form[$key]['form']['#submit'] = $form[$key]['form']['actions'][static::MAIN_SUBMIT_BUTTON]['#submit'];
}
unset($form[$key]['form']['actions']);
}
}
// Default action elements.
$form['actions'] = [
'#type' => 'actions',
static::MAIN_SUBMIT_BUTTON => [
'#type' => 'submit',
'#value' => $this->t('Save'),
'#validate' => ['::validateForm'],
'#submit' => ['::submitForm']
]
];
return $form;
}
/**
* This method will be called from FormBuilder::doBuildForm during the process
* stage.
* In here we call the #process callbacks that were previously removed.
*
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* @param array $complete_form
* @return array
* The altered form element.
*
* @see \Drupal\Core\Form\FormBuilder::doBuildForm()
*/
public function processForm(array &$element, FormStateInterface &$form_state, array &$complete_form) {
foreach ($this->innerForms as $key => $inner_form) {
$inner_form_state = static::getInnerFormState($form_state, $key);
foreach ($inner_form_state->get('#process') as $callback) {
// The callback format was copied from FormBuilder::doBuildForm().
$element[$key]['form'] = call_user_func_array($inner_form_state->prepareCallback($callback), array(&$element[$key]['form'], &$inner_form_state, &$complete_form));
}
}
return $element;
}
/**
* {@inheritDoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
/** @var \Drupal\Core\Form\FormValidatorInterface $form_validator */
$form_validator = \Drupal::service('form_validator');
foreach ($this->innerForms as $form_key => $inner_form) {
$inner_form_state = static::getInnerFormState($form_state, $form_key);
// Pass through both the form elements validation and the form object
// validation.
$inner_form->validateForm($form[$form_key]['form'], $inner_form_state);
$form_validator->validateForm($inner_form->getFormId(), $form[$form_key]['form'], $inner_form_state);
foreach ($inner_form_state->getErrors() as $error_element_path => $error) {
$form_state->setErrorByName($form_key . '][' . $error_element_path, $error);
}
}
}
/**
* {@inheritDoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
/** @var \Drupal\Core\Form\FormSubmitterInterface $form_submitter */
$form_submitter = \Drupal::service('form_submitter');
foreach ($this->innerForms as $key => $inner_form) {
$inner_form_state = static::getInnerFormState($form_state, $key);
// The form state needs to be set as submitted before executing the
// doSubmitForm method.
$inner_form_state->setSubmitted();
$form_submitter->doSubmitForm($form[$key]['form'], $inner_form_state);
}
}
/**
* Before returning the innerFormState object, we need to set the
* complete_form, values and user_input properties from the main form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The main form state.
* @param string $key
* The key used to store the inner form state.
* @return \Drupal\Core\Form\FormStateInterface
* The inner form state.
*/
protected static function getInnerFormState(FormStateInterface $form_state, $key) {
/** @var \Drupal\Core\Form\FormStateInterface $inner_form_state */
$inner_form_state = $form_state->get([static::INNER_FORM_STATE_KEY, $key]);
$inner_form_state->setCompleteForm($form_state->getCompleteForm());
$inner_form_state->setValues($form_state->getValues() ? : []);
$inner_form_state->setUserInput($form_state->getUserInput() ? : []);
return $inner_form_state;
}
/**
* After the initialization of the inner form state, we need to assign it with
* the inner form object and set it inside the main form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The main form state.
* @param \Drupal\Core\Form\FormInterface $form_object
* The inner form object
* @param string $key
* The key used to store the inner form state.
* @return \Drupal\Core\Form\FormStateInterface
* The inner form state.
*/
protected static function createInnerFormState(FormStateInterface $form_state, FormInterface $form_object, $key) {
$inner_form_state = new FormState();
$inner_form_state->setFormObject($form_object);
$form_state->set([static::INNER_FORM_STATE_KEY, $key], $inner_form_state);
return $inner_form_state;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment