1

I am trying to get multiple forms to work in a views display. All of the forms uses AJAX, but they appear to be interfering with each other.

One form is the Views Bulk Operations form that turns the views table into a 'viewsForm'. The second form is a 'quick edit' form available on each row in the view. The problem is when I submit any of the forms from the quick edit, it tries to also submit the 'viewsForm' form (which it shouldn't be doing), resulting in validation errors on that form. It's also not picking up my ajax callback for my custom form, as witnessed by the 'ajax callback is empty or not callable' in the dblog.

If I disable views bulk operations, this works as intended, but with multiple forms on the page, I can't figure out how to tell the 'submit' button I am using to only submit the form belonging to the submit button.

I have provided my formbuilder class for reference

<?php

namespace Drupal\request_system\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Ajax\HightlightCommand;

/**
 * Provides a Request System form.
 */
class QuickEditForm extends FormBase {

  public $sub_id = 0;
  public $entity_id = 0;

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'request_system_quick_edit-' . $this->sub_id;
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    $entity = \Drupal::entityTypeManager()->getStorage('lms_request')->load($this->entity_id);
    
    $options = [];

    $options['_none'] = '- Select One -';

    if ($entity->bundle() == 'book_request') {
      $statuses = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadByProperties([
        'vid' => 'book_request_status',
      ]);
    }

    // Only show 'available from vendor' and 'Pending'
    foreach ($statuses as $status) {
      if ($status->getName() == 'Pending' || $status->getName() == 'Available From Vendor') {
        $options[$status->id()] = $status->getName();
      }
      
      if ($status->id() == $entity->field_request_status->getString()) {
        $options[$status->id()] = $status->getName();
      }
    }

    $form['quick_edit'] = [
      '#type' => 'container',
      '#id' => 'quick-edit-wrapper-'. $this->sub_id,
    ];

    $form['quick_edit']['status'] = [
      '#type' => 'fieldset',
      '#title' => 'Status Updates',
      '#name' => 'update-wrapper',
    ];

    if (!$entity->field_aph_shipment_number->isEmpty() || !$entity->field_library_shipment_number->isEmpty()) {
      $form['quick_edit']['status']['value'] = [
        '#type' => 'item',
        '#title' => 'Request Status: ',
        '#markup' => \Drupal::entityTypeManager()->getStorage('taxonomy_term')->load($entity->field_request_status->getString())->getName(),
      ];

      // Display message indicating item is part of a shipment
      $form['quick_edit']['status']['shipment_number'] = [
        '#markup' => 'This request is part of a shipment.',
      ];
    }
    else {
      // Disable this field if the request status is not 'Pending' or 'Available from Vendor', or if the item belongs to a shipment
      $form['quick_edit']['status']['value'] = [
        '#type' => 'select',
        '#title' => 'Status',
        '#options' => $options,
        '#default_value' => $entity->field_request_status->getString(),
      ];

      if ($entity->field_request_status->getString() != \Drupal\request_system\Controller\RequestSystemController::getStatus('Pending') && $entity->field_request_status->getString() != \Drupal\request_system\Controller\RequestSystemController::getStatus('Available From Vendor')) {
        $form['quick_edit']['status']['value']['#disabled'] = TRUE;
      }
    }

    $form['quick_edit']['status']['message'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Message'),
    ];

    $form['quick_edit']['status']['notify_user'] = [
      '#type' => 'checkbox',
      '#title' => 'Notify Borrower',
    ];

    // Allow editing of the APH catalog #

    $form['quick_edit']['other'] = [
      '#type' => 'fieldset',
      '#title' => 'Other',
      '#name' => 'other-wrapper',
    ];

    $form['quick_edit']['other']['aph_catalog_number'] = [
      '#type' => 'textfield',
      '#title' => 'APH Catalog #',
      '#default_value' => $entity->field_attached_copy_aph_number->getString(),
      '#description' => $entity->field_attached_copy_main_record->isEmpty() ? '' : 'Unable to change APH catalog number when a main record is assigned.',
      '#disabled' => $entity->field_attached_copy_main_record->isEmpty() ? FALSE : TRUE,
    ];

    $form['quick_edit']['id'] = [
      '#type' => 'hidden',
      '#value' => $this->entity_id,
    ];

    $form['quick_edit']['actions'] = [
      '#type' => 'actions',
    ];
    $form['quick_edit']['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Save'),
      '#ajax' => [
        'callback' => '::quickEditAjax',
        'wrapper' => 'quick-edit-wrapper-'. $this->sub_id,
      ],
      '#validate' => '::validate',
      '#limit_validation_errors' => [['id'],['status']],
      '#submit' => ['::quickEditAjaxSubmit'],
    ];

    // $form['quick_edit']['actions']['cancel'] = [
    //   '#type' => 'submit',
    //   '#value' => 'Cancel',
    // ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    $values = $form_state->getValues();

    $entity = \Drupal::entityTypeManager()->getStorage('lms_request')->load($values['id']);

    if ($values['status'] == \Drupal\request_system\Controller\RequestSystemController::getStatus("Shipped From Loan Library")) {
      if (count($entity->field_imcid->referencedEntities()) == 0) {
        $form_state->setErrorByName('status','Cannot mark this item shipped since no library item is attached.');
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {

  }

  public function quickEditAjax(&$form, FormStateInterface $form_state) {
    $values = $form_state->getValues();

    if ($form_state->hasAnyErrors()) {
      $form['status_messages'] = [
        '#type' => 'status_messages',
        '#weight' => -1000,
      ];
      $form['#sorted'] = FALSE;
    }
    
    $response = new AjaxResponse();

    $status = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->load($values['status']);
    $response->addCommand(new ReplaceCommand('.request-status-'. $values['id'],$status->getName()));
    $response->addCommand(new ReplaceCommand('#quick-edit-wrapper-'. $this->sub_id,$form));
    $response->addCommand(new HightlightCommand('#row-'. $values['id']));

    return $response;
  }

  public function quickEditAjaxSubmit(&$form, FormStateInterface $form_state) {
    $values = $form_state->getValues();

    // First load the entity
    $entity = \Drupal::entityTypeManager()->getStorage('lms_request')->load($values['id']);

    $entity->set('field_request_status',$values['status']);
    $entity->save();

    $form_state->setRebuild();
  }

}

And the way I am rendering the form is via

$form = \Drupal::classResolver()->getInstanceFromDefinition('Drupal\request_system\Form\QuickEditForm');
$form->sub_id = $entity->id();
$form->entity_id = $entity->id();
$build['form'] = \Drupal::formBuilder()->getForm($form);

It appears that when the view is rendered, it's combining all of the forms into a single , so when you click 'Save' on the sub-form, it's actually submitting the views bulk operations form. I am not sure how to resolve this or to get it to stop doing this.

I have been banging my head on this for two weeks trying to get this resolved, and can't figure out what the problem is.

Any help would be GREATLY appreciated.

EDIT: I have attached a screenshot of what we are trying to achieve. Checkbox on the left is views bulk operation, 'quick edit' form is on the far right via 'expandable table column'

Showcase of functionality

8
  • 2
    Having a <form> inside another <form> is invalid HTML, it's up to the browser what happens in that situation. I think I'm right in saying all modern browsers will just ignore the inner <form>s, and any nested inputs will implicitly be made part of the outer form (which would match your description of events). You might have to think about altering the VBO form instead of creating your own if you want to keep the VBO functionality (or alter the VBO functionality in such a way that it doesn't wrap the table with a <form>)
    – Clive
    Apr 5 at 19:17
  • VBO isn't wrapping the table in a form, views does it using the viewsForm method it exposes, so it would most likely require altering how Views works to do something like that. I am already using form_alter on the form to change some of the stuff that VBO does, but it does it at the view level, not individual row level which is what I need it to do.
    – Ex0r
    Apr 5 at 19:25
  • Ok plan B is probably out of the window then, that would be a pain to re-implement. The row level must be available though, as VBO uses it - I'm guessing there's a field handler which uses the module's API to get a form context so it can render the checkbox per-row. Perhaps your approach could be the same. You'd need to make your functionality operate within the VBO form and possibly alter its validation/etc, but given the nature of the implementation and what HTML allows, I'm not sure what the alternative would be
    – Clive
    Apr 5 at 19:31
  • By "VBO form" I mean "the form which VBO convinces Views to create via ViewsBulkOperationsBulkForm::viewsForm". I'm just looking at the code and I can see why you're stuck
    – Clive
    Apr 5 at 19:35
  • Yeah, it doesn't look like what we are trying to achieve is possible. VBO uses viewsForm to create a form element of type #checkbox in the column location it's set to in the view, but that's not ideal as that is not what we want to do, as the column for the quickedit is in an entirely different column, and ideally would work independent of the views bulk operations functionality.
    – Ex0r
    Apr 5 at 19:42

1 Answer 1

0

If I disable views bulk operations, this works as intended, but with multiple forms on the page, I can't figure out how to tell the 'submit' button I am using to only submit the form belonging to the submit button.

Without reviewing the entire code, if you have multiple Ajax forms on a page it should help to use unique submit form keys the same way you do this for the wrapper:

$form['quick_edit']['actions']['submit' . $this->sub_id] = [
  ...
  '#ajax' => [
    'callback' => '::quickEditAjax',
    'wrapper' => 'quick-edit-wrapper-'. $this->sub_id,
  ],

In the other case, you can't nest forms as discussed in the comments.

In general you need to place your form elements inside the existing form, which then can do their own thing using submit and ajax callbacks and ignoring the rest of the form. This can be done in a simple form alter hook or a more complicated system. For entity forms Drupal has subforms implemented in field widget plugins, for Views bulk forms field plugins implementing viewsForm($form, $form_state). If you use one of those then go along with the way they do it.

4
  • I am using hook_form_alter right now to alter the VBO operations and workflow. I attempted to use that to add individual per row fields to the form, but it doesn't place them in the correct column/field location that it should, and doesn't render all of the form fields, either. I am currently using a custom views field plugin to render the forms in their correct table column, but the ajax submit handlers and buttons when clicked are actually triggering the vbo submit handler and validation. Do you have a link to Drupals documentation for subforms? I can't find anything to demonstrate its us
    – Ex0r
    Apr 6 at 12:53
  • Subforms? They are automatically built for field widgets. Which would be a completely different approach, not using a Views bulk form, but displaying entities in a grid where you could attach an entity form for each entity.
    – 4uk4
    Apr 6 at 13:18
  • I am using VBO for different functionality. I am essentially trying to build two different forms on the page. One so you can perform quick actions on individual row items inline, and one so you can select multiple rows and perform different 'bulk actions' on them. It doesn't appear like I can do what I want to do as long as views bulk operations is in place, which stinks. I will have to come up with another solution.
    – Ex0r
    Apr 6 at 13:28
  • The form I am rendering right now on each row is a custom views field that uses the rendering code above to render a new form on each row. The field plugin is a 'fake' field that just renders output and a form to the row in the appropriate field column in the table. There doesn't appear to be a way to create a views field widget, or if there is I am missing it in the documentation.
    – Ex0r
    Apr 6 at 13:34

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.