3

Related to my previous question: Form fields created via Ajax are missing element attributes

I have a date field and a series of text and number fields which need their default values to change based on the value of the date field. I've updated my code so it functions this way:

public function buildForm(array $form, FormStateInterface $form_state) {
    $form['date'] = array(
        '#type' => 'date',
        '#ajax' => [
            'callback' => '::dateCallback',
            'wrapper' => 'wrapper',
        ],
        '#default_value' => date("Y-m-d")
    );
    $form['wrapper'] = [
        '#type' => 'container',
        '#attributes' => ['id' => 'wrapper'],
    ];

    //insert logic to determine value of a variable called $field1    
    $form['wrapper']['options'] = array(
        '#type' => 'fieldset',
        '#attributes' => array('class' => array('options'))
    );
    $form['wrapper']['options']['field1'] = array(
        '#type' => 'textfield',
        '#title' => "Field 1",
        '#value' => $field1
    );
    $form['wrapper']['table'] = array(
        '#type' => 'table',
        '#title' => t('Table'),
        '#header' => array("H1", "H2", "H3"),
        '#attributes' => array('class' => array('custom-table'))
    );
    //insert logic to generate table rows
    return $form;
}

/**
 * Implements callback for Ajax event on date selection.
 *
 * @param array $form
 *   From render array.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   Current state of form.
 *
 * @return array
 *   Date selection section of the form.
 */
public function dateCallback(array &$form, FormStateInterface $form_state) {
    return $form['wrapper'];
}

There are 2 problems I am having with this:

1) Implementing it in this way, such that the #value property is changed when the date field is changed, technically works, however if the user then chooses to modify any of the textfield values, that is not reflected in the $form_state object and is overridden by whatever #value was last set to in buildForm().

2) I have also tried using #default_value instead, but the value in the field will not update if it already has something in it (which it usually does from the first time #default_value is set in buildForm()). I have seen some threads like this one suggesting to unset the value in $form_state before setting #default_value again, however all such suggestions are for Drupal 7 where $form_state is a completely different data structure.

Is there a known way to do what I am trying to accomplish?

Update: I tried method 2 again by adding $form_state->unsetValue('field1'); before declaring $form['wrapper']['options']['field1'], but this did not change the behaviour.

2

Method 1 setting #value does indeed make it impossible for the user to input data.

So the method 2 setting #default_value is I think still the best approach. As the old trick from D7, unsetting the value in $form_state, doesn't work anymore, you can try this one:

Create the field in a subkey, which is incremented for each request. Set the property #tree on the first level of the field, so you get back the field value as array [$key => $value]:

$form['wrapper']['options']['field1']['#tree'] = TRUE;

if ($form_state->hasValue('field1')) {
  $key = key($form_state->getValue('field1'));
  $value_field1 = current($form_state->getValue('field1')); 
  $key++;
}
else {
  $key = '0';
  $value_field1 = NULL;
}

// insert logic to determine new field value
// but check, if there is user input in $value_field1, that you don't want to overwrite

$form['wrapper']['options']['field1'][$key] = [
  '#type' => 'textfield',
  '#title' => 'Text 1',
  '#default_value' => $value_field1,
];
  • Thanks for the reply, I've implemented this code and according to my debug logs it is working fine and setting #default_value to the appropriate value. However on the form it is only ever showing the form fields that would be at $key = 0 (confirmed by inspecting element attributes). Is this the expected behaviour? I am not sure how to get it to show the fields that would be at index $key instead. – saramm1 May 25 '17 at 14:19
  • Drupal renders all keys in a form render array, I see no reason why this is not happening. Btw. there is only ever one version of the field, because on a rebuild the field with the last key is not built again. If you have on a rebuild in an ajax request still the field in key 0 then the key didn't increment. – 4k4 May 25 '17 at 14:35
  • Oops, it was a typo in one of my $form indexes.... This is working perfectly. Thank you! – saramm1 May 25 '17 at 14:41
  • @4k4 You might consider refactoring the use of the each function, it's been deprecated and using it is highly discouraged – Clive Sep 3 '18 at 10:41
  • @Clive, thanks, this is indeed deprecated in PHP 7.2 – 4k4 Sep 3 '18 at 11:37
0

There is another way to do the same: Set #value property on ajax callback.

    <?php

    namespace Drupal\drupal_miseries\Form;

    use Drupal\Core\Form\FormBase;
    use Drupal\Core\Form\FormStateInterface;

    class AjaxResponse extends FormBase {
      protected $ts_values = [
        'ts1' => [
          'values1' => [
            'id' => 1,
            'description' => 'Valor1',
            'status' => '1',
          ],
          'values2' => [
              'id' => 2,
              'description' => 'Valor2',
              'status' => 2,
          ],
        ],
      ];

      public function getFormId() {
        return 'AjaxResponseForm';
      }

      public function ajaxTableSelect(array &$form, FormStateInterface $form_state) {
        $element = $form_state->getTriggeringElement();
        if (isset($element) && $element['#type'] == 'checkbox') {
          $check_value = $element['#return_value'];
          $textfield_ts = $this->ts_values['ts1'][$check_value]['description'];
          $form['ts_textfield']['textfield']['#value'] = $textfield_ts;
        }

        return $form['ts_textfield'];
      }

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

        $form['ts_container'] = [
          '#type' => 'container',
          '#attributes' => ['id' => 'ts-container'],
        ];

        $header = [
          'id' => $this->t('ID'),
          'description' => $this->t('Description'),
          'status' => $this->t('Status'),
        ];

        $form['tableselect'] = [
          '#type' => 'tableselect',
          '#description' => $this->t('Test Tableselect'),
          '#header' => $header,
          '#options' => $this->ts_values['ts1'],
          '#empty' => $this->t('No values'),
          '#ajax' => [
            'callback' => [$this, 'ajaxTableSelect'],
            'wrapper' => 'ts-textfield',
          ],
        ];

        $form['ts_textfield'] = [
          '#type' => 'container',
          '#attributes' => ['id' => 'ts-textfield'],
        ];

        $form['ts_textfield']['textfield'] = [
          '#type' => 'textfield',
          '#description' => $this->t('Value fill by ajax'),
          '#title' => $this->t('Ajax Textfield'),
        ];

        $form['actions'] = [
          '#type' => 'actions',
        ];

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

        return $form;
      }

      public function submitForm(array &$form, FormStateInterface $form_state) {
        dpm($form_state->getValue('tableselect'));
        dpm($form_state->getValue('textfield'));
      }
    }

When you click on any tableselect checkbox, textfield is set with checkbox value => if you click 'submit' checkbox value is assigned to textfield.

When you click on any tableselect checkbox, textfield is set with checkbox value => if you change textfield manually and click 'submit' new value will be assigned to textfield.

but.......Is this the correct way to do that?

0

A solution to append a default value to the field after callback but with option for user to change input:

add the value to field attributes: '#attributes' => array('value' => array($form_state->getValue('option2'))),

        $o = [];
    $form['option1'] = array(
        '#type' => 'select',
        '#size' => 1,
        '#options' => ['a' => 'A', 'b' => 'B'],
        '#title' => $this->t('option 1'),
        '#required' => TRUE,
        '#ajax' => array(
            'callback' => array($this, 'next'),
            'wrapper' => 'option2',
        ),
    );

    if ($form_state->getValue('option1') == 'a') {
        $o = [1 => 'one', 2 => 'two'];
    }
    if ($form_state->getValue('option1') == 'b') {
        $o = [3 => 'three', 4 => 'four'];
    }

    $form['option2'] = array(
        '#type' => 'select',
        '#size' => 1,
        '#options' => $o,
        '#title' => t('option 2'),
        '#required' => TRUE,
        '#prefix' => "<div id='option2'",
        '#suffix' => '</div>',
        '#ajax' => array(
            'callback' => array($this, 'get_data'),
            'wrapper' => 'data',
        ),
    );

    $form['data'] = [
        '#type' => 'textfield',
        '#id' => 'test',
        '#title' => t("choice") . " " . $form_state->getValue('option2'),
        '#default_value' => $form_state->getValue('option2'),
        '#size' => 5,
        '#attributes' => array('value' => array($form_state->getValue('option2'))),
        '#prefix' => "<div id='data'",
        '#suffix' => '</div>',
    ];

    $form['actions']['submit'] = array(
        '#type' => 'submit',
        '#value' => $this->t('Test'),
        '#suffix' => ''
    );

    return $form;
}

/**
 * callback functions
 */
public function next(array &$form, FormStateInterface $form_state) {
    return $form['option2'];
}

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

public function validateForm(array &$form, FormStateInterface $form_state) {

}

public function submitForm(array &$form, FormStateInterface $form_state) {
    dpm($form_state->getValue('data'));
}
0

I've solved this by using the AJAX callback to reload the page, rather than return the form. For example:

$path = '/path/to/form';
$response = new AjaxResponse();
$response->addCommand(new RedirectCommand($path));
return $response;

It's slower than returning the form, but is a lot less code, especially if you're talking about a complex form with a lot of default values.

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.