3

Firstly, a working select element with an AJAX callback:

public function buildForm(array $form, FormStateInterface $form_state) {

  $form['select'] = [
    '#type' => 'select',
    '#options' => [
      0 => 'ZERO',
      1 => 'ONE',
      2 => 'TWO',
    ],
    '#ajax' => [
      'callback' => [$this, 'ajaxSelectChanged'],
      'event' => 'change',
    ],
  ];

  $form['submit'] = [
    '#type' => 'submit',
    '#value' => t('Submit'),
  ];

  return $form;
}

public function ajaxSelectChanged(array &$form, FormStateInterface $form_state) {
  $selection = $form_state->getValue('select');
  // Do something with the selection.
}

Now I want to put the select element inside a tableselect element...

This comment shows how form elements may be added inside the rows of a tableselect (which is also a form element).

Here's my attempt:

public function buildForm(array $form, FormStateInterface $form_state) {

  // Build the sortable table header.
  $select_options = [
    0 => 'ZERO',
    1 => 'ONE',
    2 => 'TWO',
  ];

  $form['table'] = [
    '#type' => 'tableselect',
    '#header' => [
      'name' => $this->t('Name'),
      'select' => $this->t('Select'),
    ],
    '#options' => [
      'alice' => [
        'name' => $this->t('Alice'),
        'select' => [
          'data' => [
            '#type' => 'select',
            '#options' => $select_options,
            '#ajax' => [
              'callback' => [$this, 'ajaxSelectChanged'],
              'event' => 'change',
            ],
          ],
        ],
      ],
      'bob' => [
        'name' => $this->t('Bob'),
        'select' => [
          'data' => [
            '#type' => 'select',
            '#options' => $select_options,
            '#ajax' => [
              'callback' => [$this, 'ajaxSelectChanged'],
              'event' => 'change',
            ],
          ],
        ],
      ],
    ],
    '#empty' => $this->t('There is nothing to see here.'),
  ];

  $form['submit'] = [
    '#type' => 'submit',
    '#value' => t('Submit'),
  ];

  return $form;
}

public function ajaxSelectChanged(array &$form, FormStateInterface $form_state) {
  $selection = $form_state->getValues();
  // Do something with the selection.
}

It works, except for the AJAX callback, which never runs.

Tableselect AJAX Form

  • Am I doing something wrong?
  • Is there a way to let form elements inside a table trigger AJAX callbacks when they are changed?
1

The reason that #ajax doesn't work for your tableselect options is that the render array is inside a render attribute - render attributes are not processed as render element children.

That means that they don't receive an ID, or a name. And that the RenderElement::preRenderAjaxForm does not process them.

There are several ways to circumvent this problem.

Option 1: Create a hidden ajax elements below the table and trigger them via ajax. Option 2: Run the required processing yourself.

Here is a sample implementation of Option 2:

class SandboxForm extends FormBase {
  public function getFormId() {
    return 'sandbox';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {
    // Build the sortable table header.
    $select_options = [
      0 => 'ZERO',
      1 => 'ONE',
      2 => 'TWO',
    ];

    $form['table'] = [
      '#type' => 'tableselect',
      '#header' => [
        'name' => $this->t('Name'),
        'select' => $this->t('Select'),
      ],
      '#empty' => $this->t('There is nothing to see here.'),
      '#options' => [
        'alice' => [
          'name' => $this->t('Alice'),
          'select' => [
            'data' => [
              '#type' => 'select',
              '#options' => $select_options,
              '#ajax' => [
                'callback' => [$this, 'ajaxSelectChanged'],
                'event' => 'change',
              ],
            ],
          ],
        ],
        'bob' => [
          'name' => $this->t('Bob'),
          'select' => [
            'data' => [
              '#type' => 'select',
              '#options' => $select_options,
              '#ajax' => [
                'callback' => [$this, 'ajaxSelectChanged'],
                'event' => 'change',
              ],
            ],
          ],
        ],
      ],
      '#process' => [
        '::processTable',
        [Tableselect::class, 'processTableselect']
      ]
    ];

    $form['submit'] = [
      '#type' => 'submit',
      '#value' => t('Submit'),
      '#ajax' => [
        'wrapper' => 'test',
        'callback' => [static::class, 'ajaxSelectChanged'],
      ],
    ];

    return $form;
  }

  public function processTable(&$element, FormStateInterface $form_state, &$complete_form) {
    foreach (array_keys($element['#options']) as $option) {
      foreach (array_keys($element['#options'][$option]) as $col) {
        if (is_array($element['#options'][$option][$col]) && isset($element['#options'][$option][$col]['data'])) {
          $element['#options'][$option][$col]['data']['#name'] = implode('-', [$element['#name'], $option, $col]);
          $element['#options'][$option][$col]['data']['#id'] = implode('-', [$element['#id'], $option, $col]);
          $element['#options'][$option][$col]['data'] = RenderElement::preRenderAjaxForm($element['#options'][$option][$col]['data']);
        }
      }
    }
    return $element;
  }

  public function validateForm(array &$form, FormStateInterface $form_state) {
    parent::validateForm($form, $form_state); // TODO: Change the autogenerated stub
  }


  public static function ajaxSelectChanged(array &$form, FormStateInterface $form_state) {
    $selection = $form_state->getValue('select');
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
    $selection = $form_state->getValue('select');
  }

}
P.S.

I know that for regular table elements you can pass render array by not using the #rows attribute and passing them as children. I tried it here and it failed, though I could be missing something.

  • This works! However, it also triggers the first (enabled) submit button on the outer form. Can that be prevented? If it's a long answer, I'll ask in a separate question. – rudolfbyker Feb 2 '17 at 10:55
  • Because this is a core problem with Drupal tableselect, any solution would be some kind of hack and might cause these kinds of problems. – Eyal Feb 2 '17 at 22:43
3

Here is my solution:

  • Add placeholders (like a simple string, or an empty array) to the table row, where the select lists should go.
  • Add select lists below the table, and check that they have fully functional AJAX callbacks.
  • Register a #pre_render function on the form which moves the select lists into the table.

In the buildForm function:

$form['#pre_render'] = [[$this, 'preRender']];

And then in the form class:

public static function preRender(array $form) {
  foreach ($form['table']['#options'] as $delta => &$row) {
    $row['select_lists']['data'] = $form['select_lists'][$delta];
    unset($form['select_lists'][$delta]);
  }
  return $form;
}

I also tried hook_form_alter, but it runs too early, and messes up the AJAX. Apparently, a pre_render function runs after the AJAX stuff has been set up by Drupal, and then we can move the element anywhere on the page without affecting the AJAX functionality.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.