Ajax elements in Drupal form tables
Maybe you have banged your head against the wall trying to figure out why if you add an Ajax button (or any other element) inside a table, it just doesn’t work. I have.
I was building a complex form that needed to render some table rows, nicely formatted and have some operations buttons to the right to edit/delete the rows. All this via Ajax. You know when you estimate things and you go like: yeah, simple form, we render table, add buttons, Ajax, replace with text fields, Save, done. Right? Wrong. You render the table, put the Ajax buttons in the last column and BAM! Hours later, you wanna punch someone. When Drupal renders tables, it doesn’t process the #ajax
definition if you pass an element in the column data
key.
Well, here’s a neat little trick to help you out in this case: #pre_render
.
What we can do is add our buttons outside the table and use a #pre_render
callback to move the buttons back into the table where we want them. Because by that time, the form is processed and Drupal doesn’t really care where the buttons are. As long as everything else is correct as well.
So here’s what a very basic buildForm()
method can look like. Remember, it doesn’t do anything just ensures we can get our Ajax callback triggered.
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['#id'] = $form['#id'] ?? Html::getId('test');
$rows = [];
$row = [
$this->t('Row label'),
[]
];
$rows[] = $row;
$form['buttons'] = [
[
'#type' => 'button',
'#value' => $this->t('Edit'),
'#submit' => [
[$this, 'editButtonSubmit'],
],
'#executes_submit_callback' => TRUE,
// Hardcoding for now as we have only one row.
'#edit' => 0,
'#ajax' => [
'callback' => [$this, 'ajaxCallback'],
'wrapper' => $form['#id'],
]
],
];
$form['table'] = [
'#type' => 'table',
'#rows' => $rows,
'#header' => [$this->t('Title'), $this->t('Operations')],
];
$form['#pre_render'] = [
[$this, 'preRenderForm'],
];
return $form;
}
First, we ensure we have an ID on our form so we have something to replace via Ajax. Then we create a row with two columns: a simple text and an empty column (where the button should go, in fact).
Outside the form, we create a series of buttons (1 in this case), matching literally the rows in the table. So here I hardcode the crap out of things but you’d probably loop the same loop as for generating the rows. On top of the regular Ajax shizzle, we also add a submit callback just so we can properly capture which button gets pressed. This is so that on form rebuild, we can do something with it (up to you to do that).
Finally, we have the table element and a general form pre_render
callback defined.
And here are the two referenced callback methods:
/**
* {@inheritdoc}
*/
public function editButtonSubmit(array &$form, FormStateInterface $form_state) {
$element = $form_state->getTriggeringElement();
$form_state->set('edit', $element['#edit']);
$form_state->setRebuild();
}
/**
* Prerender callback for the form.
*
* Moves the buttons into the table.
*
* @param array $form
* The form.
*
* @return array
* The form.
*/
public function preRenderForm(array $form) {
foreach (Element::children($form['buttons']) as $child) {
// The 1 is the cell number where we insert the button.
$form['table']['#rows'][$child][1] = [
'data' => $form['buttons'][$child]
];
unset($form['buttons'][$child]);
}
return $form;
}
First we have the submit callback which stores information about the button that was pressed, as well as rebuilds the form. This allows us to manipulate the form however we want in the rebuild. And second, we have a very simple loop of the declared buttons which we move into the table. And that’s it.
Of course, our form should implement Drupal\Core\Security\TrustedCallbackInterface
and its method trustedCallbacks()
so Drupal knows our pre_render
callback is secure:
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['preRenderForm'];
}
And that’s pretty much it. Now the Edit button will trigger the Ajax, rebuild the form and you are able to repurpose the row to show something else: perhaps a textfield to change the hardcoded label we did? Up to you.
Hope this helps.