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.

Daniel Sipos
Danny founded WEBOMELETTE in 2012 as a passion project, mostly writing about Drupal problems he faced day to day, as well as about new technologies and things that he thought other developers would find useful. Now he now manages a team of developers and designers, delivering quality products that make businesses successful.
Comments
Add name to buttons in table to work properly
Hi Danny! Your site is very good, I always read it and learn a lot, thank you!!
I don't know if we are talking about the exact same problem, but recently I had to put buttons in a table and after googling I solved it by adding the name attribute with a different value to each one. Here in a real example in case it can be useful to someone (code in Spanish, sorry) https://gist.github.com/tongadall/42752373db34c48f5a966c408ae6a0e1
I have just made public the gist indicated in previous post
Sorry i didn't notice the gist was private.
Thanks! This saved me after…
Thanks! This saved me after some hair pulling while working on Search API UI
Add new comment