Skip to content

Instantly share code, notes, and snippets.

@ao5357
Created May 4, 2021 11:53
Show Gist options
  • Select an option

  • Save ao5357/16b746ffedadfc46482e0e5c6b9c5c21 to your computer and use it in GitHub Desktop.

Select an option

Save ao5357/16b746ffedadfc46482e0e5c6b9c5c21 to your computer and use it in GitHub Desktop.
Stash a complex AJAX form
<?php
namespace Drupal\nypl_ip_checker\Form;
/**
* @file
* Contains \Drupal\contact_info\Form\NyplIpCheckerAdminForm.
*/
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
/**
* Class AdminForm.
*/
class NyplIpCheckerAdminForm extends ConfigFormBase {
/**
* Constructs a new NyplIpCheckerAdminForm.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
*/
public function __construct(ConfigFactoryInterface $config_factory) {
parent::__construct($config_factory);
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames(): array {
return [
'nypl_ip_checker.nypl_ip_checker_admin_form',
];
}
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return 'nypl_ip_checker_admin_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
// Get the config this form saves to, for default_values.
$config = $this->config('nypl_ip_checker.settings');
// Determine if an AJAX callback is rebuilding the form.
$addElement = $form_state->get('addElement');
$removeElement = $form_state->get('removeElement');
// Make that form a tree.
$form['#tree'] = TRUE;
$form_state->setCached(FALSE);
// Form description.
$form['description'] = [
'#type' => 'item',
'#markup' => $this->t('This form stores the RegEx patterns associated with various NYPL locations.'),
];
// Fieldset with one or more fields holding onsite RegEx patterns.
$form['onsite'] = [
'#type' => 'fieldset',
'#title' => $this->t('On-site patterns'),
'#description' => $this->t('Broad RegEx patterns indicating whether an IP is within or outside NYPL property.'),
];
// Fill in onsite pattern field if there is no existing value.
$form['onsite']['pattern'][0] = [
'#type' => 'textfield',
'#title' => $this->t('RegEx pattern'),
'#default_value' => '',
];
// Fill in saved config values, if present.
$onsite_patterns = $config->get('onsite.pattern');
if (isset($onsite_patterns) && count($onsite_patterns)) {
$form['onsite']['pattern'] = [];
foreach ($onsite_patterns as $i => $val) {
$form['onsite']['pattern'][$i] = [
'#type' => 'textfield',
'#title' => $this->t('RegEx pattern'),
'#default_value' => $val,
];
}
}
// Check if the AJAX triggered an add.
if (isset($addElement) && $addElement === ['onsite', 'pattern']) {
$form['onsite']['pattern'][] = [
'#type' => 'textfield',
'#title' => $this->t('RegEx pattern'),
'#default_value' => '',
];
}
// Check if the AJAX triggered a remove.
if (isset($removeElement) && $removeElement === ['onsite', 'pattern']) {
array_pop($form['onsite']['pattern']);
}
$form['onsite'] = $this->appendAddRemoveActions($form['onsite']);
// Fieldset of fieldsets holding match patterns for specific locations.
$form['mappings'] = [
'#type' => 'fieldset',
'#title' => $this->t('Location patterns'),
];
// Create one blank fieldset if no saved values present.
$blankMapping = [
'#type' => 'fieldset',
'#title' => $this->t('Location'),
'pattern' => [
0 => [
'#type' => 'textfield',
'#title' => $this->t('RegEx pattern'),
'#default_value' => '',
],
],
];
// Include a zeroth element in order to make AJAX play nice.
$form['mappings']['location'][0] = [
'#type' => 'item',
'#markup' => $this->t('Each location has a require node reference, plus one or more patterns.'),
];
$form['mappings']['location'][] = [
'#type' => 'item',
'#markup' => $this->t('Neither zero nor 1 are good.'),
];
$form['mappings']['location'][] = $blankMapping;
$form['mappings']['location'][] = $blankMapping;
// Fill in saved config values, if present.
$mappings = $config->get('mappings');
if (isset($mappings) && count($mappings)) {
$form['mappings']['location'] = [];
foreach ($mappings as $i => $val) {
$form['mappings']['location'][$i] = [
'#type' => 'fieldset',
'#title' => $this->t('Location'),
'pattern' => [
0 => [
'#type' => 'textfield',
'#title' => $this->t('RegEx pattern'),
'#default_value' => $val[0]['pattern'],
],
],
];
}
}
// Check if the AJAX triggered an add for a whole mapping.
if (isset($addElement) && count($addElement) === 2
&& $addElement === ['mappings', 'location']
) {
$form['mappings']['location'][] = $blankMapping;
}
// Check if the AJAX triggered a remove for a whole mapping.
if (isset($removeElement) && count($addElement) === 2
&& $removeElement === ['mappings', 'location']
) {
array_pop($form['mappings']['location']);
}
//$form['mappings'] = $this->appendAddRemoveActions($form['mappings']);
// Check if the AJAX triggered add/remove on patterns within a location.
if (isset($addElement) && count($addElement) === 4
&& in_array('mappings', $addElement)
&& in_array('location', $addElement)
) {
$form['mappings']['location'][$addElement[2] - 1]['pattern'][] = [
'#type' => 'textfield',
'#title' => $this->t('RegEx pattern'),
'#default_value' => '',
];
}
if (isset($removeElement) && count($removeElement) === 4
&& in_array('mappings', $removeElement)
&& in_array('location', $removeElement)
) {
array_pop($form['mappings']['location'][$addElement[2] - 1]['pattern']);
}
// Add in actions for all the mappings/locations after the parent.
foreach ($form['mappings']['location'] as $i => $set) {
if ($i >= 1) {
$form['mappings']['location'][$i] = $this->appendAddRemoveActions($form['mappings']['location'][$i]);
}
}
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$values = $form_state->getValues();
$config = $this->configFactory->getEditable('nypl_ip_checker.settings');
$config->set('onsite.pattern', $values['onsite']['pattern']);
$config->save();
parent::submitForm($form, $form_state);
}
/**
* Add the add/remove buttons to elements that take such AJAX toggles.
*
* @param array $element
* A form element with its constituent parts.
*
* @return array
* The element with the buttons appended.
*/
public function appendAddRemoveActions(array $element): array {
$element['actions'] = [
'#type' => 'actions',
'add' => [
'#type' => 'submit',
'#value' => $this->t('Add'),
'#submit' => ['::addCallback'],
'#ajax' => [
'callback' => '::addRemoveAjaxCallback',
'wrapper' => 'nypl-ip-checker-admin-form',
],
],
'remove' => [
'#type' => 'submit',
'#value' => $this->t('Remove'),
'#submit' => ['::removeCallback'],
'#ajax' => [
'callback' => '::addRemoveAjaxCallback',
'wrapper' => 'nypl-ip-checker-admin-form',
],
],
];
return $element;
}
/**
* Callback for both ajax-enabled buttons.
*
* Selects and returns the fieldset with the names in it.
*/
public function addRemoveAjaxCallback(array &$form, FormStateInterface $form_state) {
return $form;
}
/**
* AJAX callback for adding an element within the form.
*
* @param array $form
* The form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form's state, as defined in Drupal core.
*/
public function addCallback(array &$form, FormStateInterface $form_state) {
// Get the array keys of the triggering element within $form.
$trigger = $form_state->getTriggeringElement()['#parents'];
// Add/remove actions add two levels, which we remove here.
$parent = array_splice($trigger, 0, -2);
// Use the helper method to get the parent element in the form.
$parentElement = $this->addressFormFromArray($form, $parent);
// Get the non-hashtagged array elements of the parent.
$elements = Element::children($parentElement);
// Assuming the actions are last, get the element key for the field to add.
$addElement = $elements[count($elements) - 2];
// Append the add element to the overall form keys.
$parent[] = $addElement;
// Pass the information needed to add a row via $form_state.
$form_state->set('addElement', $parent);
$form_state->setRebuild();
}
/**
* Submit handler for the "remove one" button.
*
* Decrements the max counter and causes a form rebuild.
*/
public function removeCallback(array &$form, FormStateInterface $form_state) {
// Get the array keys of the triggering element within $form.
$trigger = $form_state->getTriggeringElement()['#parents'];
// Add/remove actions add two levels, which we remove here.
$parent = array_splice($trigger, 0, -2);
// Use the helper method to get the parent element in the form.
$parentElement = $this->addressFormFromArray($form, $parent);
// Get the non-hashtagged array elements of the parent.
$elements = Element::children($parentElement);
// Assuming the actions are last, get the element key for the field to add.
$addElement = $elements[count($elements) - 2];
// Append the add element to the overall form keys.
$parent[] = $addElement;
// Pass the information needed to add a row via $form_state.
$form_state->set('removeElement', $parent);
$form_state->setRebuild();
}
/**
* Helper to get a form element from an array of keys.
*
* @param array $form
* The form.
* @param array $addresses
* Array keys for an element in the form of count($addresses) depth.
*
* @return array
* The $form element referenced from the addresses.
*/
public function addressFormFromArray(array $form, array $addresses): array {
$addressed = $form;
foreach ($addresses as $i => $key) {
$addressed = $addressed[$key];
}
return $addressed;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment