Hi there,

Well it sounds simple but I can't figure out how to do it.
I'd like to add a button with an ajax callback to each row inside a normal table. I have the following code:

  $form['button'] = array(
    'row2' => array(
      '#type' => 'button',
      '#value' => 'click',
      '#ajax' => array(
        'callback' => 'ajax_callback_function',
      ),
    ),
  );
  $form['table'] = array(
    '#theme' => 'table',
    '#header' => array(t('header1'),t('header2')),
    '#rows' => array(array(
      'row1' => 'value1',
      'row2' => array(
        'data' => array(
          '#type' => 'button',
          '#value' => 'click',
          '#ajax' => array(
            'callback' => 'ajax_callback_function',
          ),
        )
      ),
    )),
  );
  return $form;

Now the $form['button'] makes the button inside the form element but outside the table and when clicked it does process through ajax to the callback like expected.
Unfortunatly the button inside the table is displayed as a normal button and when clicked it simply submits the form. It seems to completely ignore the #ajax attribute.

Does anyone know why and how to get this to work?

Thanks in advance,
SanderJP

Comments

vm’s picture

This question is better served in the module development and code questions forum. Please edit the opening post and move it. Thanks.

nevets’s picture

What are actually trying to achieve, what is the purpose of the button?

Depending on the purpose, an alternative approach is to use an ajax link (which can be themed to looked like a button).

sanderjp’s picture

I need to modify a value in a custom database table.
Do you mean something like this?
Seems like a lot of work when it's so simple with the button outside the table. I don't understand why it doesn't work inside the table.

jaypan’s picture

The problem here is a difficult one to see right away.

You are passing everything through an element with #theme table. The problem is that this function will build the data into a table, but it does not call drupal_render() on any of the elements, as the theme is for a table in general, not specifically for a table in a form.

Since drupal_render() is not called on the element, the element does not work properly.

To get around this, you need to create your own theme function that will render the button AND theme the table.

Example:

 function some_form($form, &$form_state)
{
  // Declare an element that has a theme
  $form['table'] = array
  (
    '#theme' => 'my_custom_form_theme',
  );
  // Add your elements to that element
  $form['table']['my_button'] = array
  (
    '#type' => 'button',
    '#ajax' => array(), // some ajax stuff
    '#value' => t('Push me'),
  );
  // Another element to be added to the table
  $form['table']['some_value'] = array
  (
    '#markup' => t('Some value'),
  );
  return $form;
}

// Implement hook_theme() to register your theme function
function my_module_theme()
{
  // Register our theme
  $theme['my_custom_form_theme'] = array
  (
    // Theme functions for forms always take a render element
    // of 'form'
    'render element' => 'form',
  );

  return $theme;
}

// Our custom theme function
function theme_my_custom_form_theme($variables)
{
  // The form is located in $variables['form']
  $form = $variables['form'];

  $header = array(t('Button'), t('Some value'));

  $row = array();
  // We need to call drupal_render() on any form element
  // we use in this function
  $row[] = drupal_render($form['my_button']);
  $row[] = drupal_render($form['some_value']);
  $rows = array($row);

  // Build our table
  $output = theme('table', array('header' => $header, 'rows' => $rows));

  // At the end of any theme function, drupal_render_children() needs to be
  // called on the form element to ensure that any remaining elements are
  // properly rendered. Not doing this will cause your form to fail in some situations
  return $output . drupal_render_children($form);
}

Contact me to contract me for D7 -> D10/11 migrations.

sanderjp’s picture

Thank you very much for the explanation and elaborate example, I will test this tonight.

burkeker’s picture

Thanks, Jaypan - I have just proved this works. Now I only need to figure out how to iterate multiple rows.

lmeurs’s picture

Short explanation (correct me if I am wrong!): AJAX enabled form elements appear invisible to Drupal's Form API when they remain inside a "#theme" => "table" render array. This is probably due to render() using element_children() which does not look into a table's #header or #rows elements.

A way to workaround this is to add dummy elements to the form which are visible to the Form API and bind AJAX events to the original elements to trigger the dummy elements. I shared this workaround at http://stackoverflow.com/a/31098784/328272.

Example of implementing the workaround per AJAX enabled element when building a form:

    /**
     * My form.
     */
    function MY_MODULE_my_form($form, &$form_state) {
      // Define button. NB: An ID and name are required!
      $my_button = array(
        '#type' => 'button',
        '#id' => 'test_button',
        '#name' => 'test_button',
        '#value' => t('Button label'),
        '#ajax' => array('callback' => '_MY_MODULE_ajax_callback'),
      );
      // Call table AJAX workaround function on the button.
      MY_MODULE_table_ajax_workaround($my_button, $form);

      // Define table.
      $form['my_table'] = array(
        '#theme' => 'table',
        '#header' => array(
          array('data' => t('Column header')),
        ),
        '#rows' => array(
          'data' => array(
            array('data' => $my_button),
          )
        ),
      );

      return $form;
    }

    /**
     * Workaround for AJAX enabled form elements that remain inside tables.
     *
     * Problem: AJAX enabled form elements appear invisible to Drupal's Form API
     * when they remain inside a #theme => table render array. This is probably due
     * to render() using element_children() which does not look into a table's
     * #header or #rows elements.
     *
     * Workaround: Add dummy elements to the form which are visible to the Form API
     * and bind AJAX events to the original elements to trigger the dummy elements.
     *
     * Based on:
     * @link http://stackoverflow.com/questions/1981781
     *
     * Shared at:
     * @link http://stackoverflow.com/a/31098784/328272
     *
     * Another workaround is the render elements using custom theme functions, but
     * this seems slightly more complicated and rendered elements are no longer
     * alterable.
     * @link https://www.drupal.org/node/2101557#comment-7920151
     */
    function MY_MODULE_table_ajax_workaround($element, &$form, $js_ajax_options = array()) {
      // Add dummy element.
      $form['table_ajax_workaround'][$element['#id']] = array(
        '#type' => 'value',
        '#name' => $element['#name'],
        '#value' => $element['#value'],
        '#ajax' => $element['#ajax'],
      );

      // Bind AJAX event to the original element, default properties are used. See
      // Drupal.ajax in misc/ajax.js.
      $js_setting['ajax'][$element['#id']] = drupal_array_merge_deep(array(
        'event' => 'mousedown',
        'keypress' => true, // Use lowercase booleans to support IE.
        'prevent' => 'click',
        'url' => base_path() . 'system/ajax',
        'submit' => array(
          '_triggering_element_name' => $element['#name'],
          '_triggering_element_value' => $element['#value'],
        ),
      ), $js_ajax_options);

      // Add JS setting.
      drupal_add_js($js_setting, 'setting');
    }

Laurens Meurs
wiedes.nl

jaypan’s picture

If it works, great. But the example I showed earlier is the 'Drupal way'.

Contact me to contract me for D7 -> D10/11 migrations.

CaDyMaN’s picture

The main problem is that the method ajax_pre_render_element is not used
What you can do is something like this:

if(!empty($element['#ajax'])) {
$element = ajax_pre_render_element ($element)
}

For me the workaround is ok but is not completed. Is not working as it should.