13

I'm attempting to allow the user to dynamically choose a number of fields based on a dropdown box using an ajax call, but I can't seem to get the ajax call to rebuild the form afterwards.

<?php
class AJAXexample extends BlockBase {
    public function blockForm($form, FormStateInterface $form_state) {
        if (empty($form_state->getValue('number'))) {
            $form_state->setValue('number', 3);
        } 
        $form['columnNum'] = [
            '#title'   => t('Number of Columns'),
            '#type'    => 'select',
            '#options' => [
                1         => '1',
                2         => '2',
                3         => '3',
                4         => '4',
            ],
            '#default_value' => $this->configuration['columnNum'],
            '#empty_option'  => t('-select-'),
            '#ajax'          => [
                'callback'      => [$this, 'columnCallback'],
            ],
        ];
        for ($i = 0; $i < $form_state->getValue('number'); $i += 1) {
            $form['column'][$i] = [
                $i => [
                    '#type'       => 'details',
                    '#title'      => t('Column '.$numTitle),
                    '#open'       => FALSE,
                    'columnTitle' => [
                        '#type'      => 'textfield',
                        '#title'     => t('Column Title'),
                        '#value'     => $config[0]['columnTitle'],
                    ],  
                ],
            ];  
        return $form;
    }

    public function columnCallback(array &$form, FormStateInterface $form_state) {
        $form_state->setValue('number', 10);
        $form_state->setRebuild(true);
        return $form;
    }
}

The number of textfields is based on the form_state variable 'number'. The callback columnCallback changes the form_state variable to 10, and is fired when the 'columnNum' form field is changed. However the form isn't rebuilt with the new number of fields even though $form_state->setRebuild(); is called. Is there a way to get the form to rebuild after an ajax call?

NOTE: I have already tried techniques such as replacing or appending the items of the form inside of the actual ajax call but when that happens none of the input into the replaced fields get passed to $form_state.

UPDATE: After attempting 4k4's solution I receive an error

Recoverable fatal error: Argument 1 passed to Drupal\Core\Render\MainContent\AjaxRenderer::renderResponse() must be of the type array, null given, called in /Library/WebServer/Documents/aaep/web/core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php on line 89 and defined in Drupal\Core\Render\MainContent\AjaxRenderer->renderResponse() (line 45 of /Library/WebServer/Documents/aaep/web/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php).

The belief is that the error occurs because $form['column'] is returning null despite being created as a container in the blockForm function. I've attempted calling the callback in other ways like

'#ajax' => [
    'callback' => '::columnCallback',
]

and

'#ajax' => [
    'callback' => [$this, '\Drupal\my_examples\Plugin\Block\AJAXexample::columnCallback'],
]

But I receive the same error. Curiously, when I change the callback to return the entire $form instead of just $form['column'] it repeats the form (a copy of the form appears below the current form) and still without the proper number of columns.

  • Might be a typo but double checking, are you aware that in columnCallback the first argument is a typo (no space between array and &$form)? – Kevin Aug 15 '16 at 15:26
4
+150

First problem is to handle the value for the column number. On the first build get it from configuration, on a rebuild get it from the user input and put it in $columnNum.

The second is to decide, what part of the form changes in AJAX and put this in a div container with the id columns-wrapper.

class AJAXexample extends BlockBase {
    public function blockForm($form, FormStateInterface $form_state) {
        $columnNum = empty($form_state->getValue('columnNum')) ? $this->configuration['columnNum'] : $form_state->getValue('columnNum');
        $form['columnNum'] = [
            '#title'   => t('Number of Columns'),
            '#type'    => 'select',
            '#options' => [
                1         => '1',
                2         => '2',
                3         => '3',
                4         => '4',
            ],
            '#default_value' => $this->configuration['columnNum'],
            '#empty_option'  => t('-select-'),
            '#ajax'          => [
                'callback'      => [$this, 'columnCallback'],
                'wrapper'       => 'columns-wrapper', 
            ],
        ];
        $form['column'] = [
            '#type' => 'container',
            '#attributes' => ['id' => 'columns-wrapper'],
        ];
        for ($i = 0; $i < $columnNum; $i += 1) {
            $form['column'][$i] = [
                $i => [
                    '#type'       => 'details',
                    '#title'      => t('Column '.$numTitle),
                    '#open'       => FALSE,
                    'columnTitle' => [
                        '#type'      => 'textfield',
                        '#title'     => t('Column Title'),
                        '#value'     => $config[0]['columnTitle'],
                    ],  
                ],
            ];  
        return $form;
    }

In the callback we only need to return the ajax wrapper.

public function columnCallback(array&$form, FormStateInterface $form_state) {
    return $form['column'];
}

Drupal rebuilds the form on every ajax request and puts it in the parameter $form of the callback. It would make no sense to try to rebuild it again.

  • I receive an error after the ajax request is called. 'HTTP Result Code: 200' StatusText: OK ResponseText: – Matt Aug 15 '16 at 17:06
  • The test things I performed a die(print_r($form_state->getValues())); and it did properly display the proper columnNum value. It just errors out otherwise. – Matt Aug 15 '16 at 17:08
  • I've put the changes in your code for demonstration. Can't help with debugging without error messages with line numbers. – 4k4 Aug 15 '16 at 17:56
  • 1
    Did you remove the syntax error from @Kevin's comment? Are there php errors in the error log? There should be plenty when testing new code like this. – 4k4 Aug 15 '16 at 20:47
  • 1
    Traced the error, it means return $form['column'] is null, because the return value gets unchecked in renderResponse(). Might still be a problem with the parameter list of the callback, because we put at least a container in that form key and this would prevent this error. – 4k4 Aug 15 '16 at 21:37
2

I guess you're missing wrapper method in your '#ajax' (next to callback) which consist the HTML id attribute of the area where the content returned by the callback should be placed. See: Ajax API. Then you've to make sure such container id exist.

Code example (simplified):

public function blockForm($form, FormStateInterface $form_state) {
    $form['wrapper'] = array(
        '#type' => 'container',
        '#attributes' => array('id' => 'data-wrapper'),
        );
    $form['wrapper']['columnNum'] = [
        '#title'   => t('Number of Columns'),
        '#type'    => 'select',
        '#options' => [1 => '1', 2 => '2'],
        '#default_value' => $this->configuration['columnNum'],
        '#ajax'          => [
            'callback'   => '::columnCallback',
            'wrapper'    => 'data-wrapper',
        ],
    ];
}
public function columnCallback(array &$form, FormStateInterface $form_state) {
    return $form['wrapper'];
}

For complete code example, see: How to Add more option for type radios use Ajax in Drupal 8.

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.