Profile picture for user admin
Daniel Sipos
24 Nov 2020

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.

Profile picture for user admin

Daniel Sipos

CEO @ Web Omelette

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.

Contact us

Comments

Gastón Dall' Oglio 04 Dec 2020 19:15

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

Gastón Dall' Oglio 09 Dec 2020 16:57

I have just made public the gist indicated in previous post

Sorry i didn't notice the gist was private.

Add new comment