0

I'm trying to build a customized, Excel-style bulk edit form for one specific, multi-value field used in multiple nodes. I thought a tableselect form render element would be a good start, but I'm having a hard time with validation: when any user input is incorrect, the form correctly reports the error, but also looses all changes to input values.

For whatever reason, #default_value does not work for input fields nested inside a table or tableselect (see comment on d.o and my comments in code below). The workaround is to create dummy form storage values

$form['nested_input_field'] = ['#type' => 'value']

manually set the nested input field #name to the corresponding dummy values, and also manually set the nested input field #value. This works as long as the user enters all valid data. But when validation fails, the entire $form_state->getValues() and all user input changes are lost, including valid ones.

Here is a (trimmed, simplyfied) version of my code so far

class BulkEditForm extends FormBase {
  public function cbSubmitForm(array &$form, FormStateInterface $form_state) {
    $form_state->set('num_pricerows', $form_state->get('num_pricerows') + 1);
    $form_state->setRebuild();
  }

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

  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->messenger()->addStatus($this->t('Not done yet. Fix validation errors loosing data first.'));
    $form_state->setRebuild();
  }


  public function validateForm(array &$form, FormStateInterface $form_state) {
    foreach ($form_state->getValue(['pt', 'nif']) as $idx => $row) {
      foreach ($form_state->getValue(['pt', 'nif', $idx]) as $key => $value) {
        if ($key == 'to') {
          if ($form_state->getValue(['pt', 'nif', $idx, 'from']) >= $form_state->getValue(['pt', 'nif', $idx, 'to'])) {
            $form_state->setErrorByName('pt][nif]['.$idx.']['.$key, $this->t('"To" must always be greater than "From"'));
          }
        }
      }
    }
  }

  public function buildForm(array $form, FormStateInterface $form_state) {
    $roomIds = [1,2,3];
    $rooms = $this->entityTypeManager->getStorage('node')->loadMultiple($roomIds);

    $form['#tree'] = TRUE;
    $form['pt'] = [
      '#type' => 'container',
      '#prefix' => '<div id="pt-ajax-wrapper">',
      '#suffix' => '</div>',
      '#tree' => TRUE,
    ];
    // @see https://api.drupal.org/comment/62637#comment-62637
    // needed to copy values from nested input inside tableselect into $form_state
    $form['pt']['nif'] = [
      '#type' => 'value',
      '#tree' => TRUE,
    ];
    foreach ($rooms as $room) {
      $form['pt']['room-'.$room->id()] = [
        '#type' => 'value',
        '#value' => $room->get('title')->value
      ];
    }

    $header = [
      'from' => $this->t('From'),
      'to' => $this->t('To'),
    ];
    foreach ($rooms as $room) {
      $header['room-' . $room->id()] = $room->get('title')->value;
    }

    if ($form_state->get('num_pricerows') === NULL) {
      $form_state->set('num_pricerows', count($prices));
    }

    for ($i=1; $i<=$form_state->get('num_pricerows'); $i++) {
      $options[$i]['from'] = [
        'data' => [
          '#type' => 'date',
          '#title_display' => 'invisible',
          '#title' => $this->t('From'),
          // #name must be set to store nested tableselect input field value in $form[#name]
          '#name' => 'pt[nif]['.$i.'][from]',
          // #default_value does not work inside tableselect, use #value instead
          '#value' => $form_state->getValue(['pt', 'nif', $i, 'from']),
          '#required' => true,
          '#attributes' => ['class' => ['pt__input--date']],
        ],
      ];
      $options[$i]['to'] = [
        'data' => [
          '#type' => 'date',
          '#title_display' => 'invisible',
          '#title' => $this->t('To'),
          '#name' => 'pt[nif][' . $i . '][to]',
          '#value' => $form_state->getValue(['pt', 'nif', $i, 'to']),
          '#required' => true,
          '#attributes' => ['class' => ['pt__input--date']],
        ],
      ];
      foreach ($rooms as $room) {
        $options[$i]['room-' . $room->id()] = [
          'data' => [
            '#type' => 'number',
            '#title_display' => 'invisible',
            '#title' => $this->t('Price'),
            '#min' => 0,
            '#name' => 'pt[nif][' . $i . '][room-' . $room->id() . ']',
            '#value' => $form_state->getValue(['pt', 'nif', $i, 'room-'.$room->id()]),
            '#required' => true,
            '#attributes' => ['class' => ['pt__input--price']],
          ],
        ];
      }
    }

    $form['pt']['pt_ts'] = [
      '#type' => 'tableselect',
      '#header' => $header,
      '#options' => $options,
      '#sticky' => TRUE,
      '#tree' => TRUE,
    ];

    $form['add_pricerow'] = [
      '#type' => 'submit',
      '#value' => t('Add price'),
      '#submit' => ['::cbSubmitForm'],
      //'#limit_validation_errors' => [],
      '#ajax' => [
        'callback' => '::cbAddRow',
        'wrapper' => 'pt-ajax-wrapper',
      ],
    ];

    $form['submit'] = [
      '#value' => t('Save prices'),
      '#type' => 'submit',
    ];
    return $form;
  }
}

Already tried, didn't work: The raw $form_state->getUserInput() is empty too when validation sets form errors.

The problem is specific to nested input fields in tableselect, "normal", not-nested fields using #default_value instead of #value do not loose input changes after validation errors.

Has anybody an idea how to prevent loosing all input changes when some validation errors occur?

Your Answer

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

Browse other questions tagged or ask your own question.