Score:1

Multiple forms on page

cn flag

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

cn flag
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>`)
Ex0r avatar
cn flag
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.
cn flag
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
cn flag
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
Ex0r avatar
cn flag
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 avatar
cn flag
I updated my question to include a screenshot of what we are trying to achieve.
cn flag
I'd be tempted to abandon the traditional form route altogether and use JS to submit the data to a route in your custom module which performs the save. Unless there's something obvious I'm missing, it looks to be technically possible, but probably not worth the effort to do it the normal way
Ex0r avatar
cn flag
I'm not sure what you mean by that. I haven't worked with submitting forms through Drupal using Javascript before. As I am mainly a backend developer, I don't have much experience with the front end happenings of Drupal.
Score:0
cn flag

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.

Ex0r avatar
cn flag
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
4uk4 avatar
cn flag
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.
Ex0r avatar
cn flag
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 avatar
cn flag
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.
mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.