Support dynamic forms using HTMX

Created on 10 July 2025, 29 days ago

Problem/Motivation

In general, \Drupal\Core\Form\FormBuilder is aware of ajax api requests and needs to be adjusted so that it is similarly aware of HTMX requests.

A thorny problem with dynamic elements was encountered in πŸ“Œ [POC] Implementing some components of the Ajax system using HTMX Active .
The form used as our discovery and development context for this POC, \Drupal\config\Form\ConfigSingleExportForm, has <select> elements with values that dynamically change based on user input.

With form builder as it is, if the form route is called with user input that is not a submit, the form rebuilds with a new build ID and fails to validate because the options get reset to their base case, and the user input no longer matches the available options.

Steps to reproduce

Once we have dependent issues committed, I'll update ConfigSingleExportForm without changing FormBuilder so others can see the issue and explore solutions.

Proposed resolution

In the POC I solved the problem like this.

if ($this->isHtmxRequest()) {
        // Restore the build id that was sent with the request. It will be used
        // after the rebuild to cache the rebuilt form.
        $form_state->addRebuildInfo('copy', ['#build_id' => TRUE]);
        $input = $form_state->getUserInput();
        $form['#build_id'] = $input['form_build_id'];
      }

Remaining tasks

Refactor FormBuilder
Add appropriate tests

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

πŸ“Œ Task
Status

Active

Version

11.0 πŸ”₯

Component

ajax system

Created by

πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @fathershawn
  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York
  • First commit to issue fork.
  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

    Posting some notes and code snippets from conversations in Slack. I'm not using the issue fork as we aren't really ready for that yet as there are still dependencies that need to be committed.

    I loaded up the 11.x branch locally and spent more time stepping through FormBuilder, FormAjaxResponseBuilder::buildResponse, and FormAjaxSubscriber::onException with xdebug.

    1. The form is definitely cached on an ajax interaction. This happens at FormBuilder.php:436
    2. I reversed the actual effect of FormAjaxSubscriber::onException. The Ajax classes copy the new build ID back to the form.

    So I removed the previous code to maintain build_id from my POC branch and instead added the following immediately after the $form['form_build_id'] part of the form is built in FormBuilder::prepareForm.

        // If a form is building from an HTMX request and the form id has changed
        // add htmx attributes to update the build ID in the client.
        // @see \Drupal\Core\Form\EventSubscriber\FormAjaxSubscriber::onException
        // @see Drupal.AjaxCommands.update_build_id
        if ($this->isHtmxRequest() && $form['#build_id'] !== $input['form_build_id']) {
          $existing_id = Html::getId($input['form_build_id']);
          $htmx_attributes = new HtmxAttribute();
          $htmx_attributes->swapOob('outerHTML:input[data-drupal-selector="' . $existing_id . '"]');
          $form['form_build_id']['#attributes'] = AttributeHelper::mergeCollections($form['form_build_id']['#attributes'], $htmx_attributes);
        }
    

    We don't have all the similar tools available yet, but this would be adding data-hx-swap-oob='outerHTML:input[data-drupal-selector="existing-form-build-id"]' to the build id input element on the response.

    And that works! Validation is working, form_build_id is changing.

  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York
  • First commit to issue fork.
  • Merge request !12918Resolve #3535173 "Support dynamic forms" β†’ (Open) created by nod_
  • πŸ‡«πŸ‡·France nod_ Lille

    we need πŸ“Œ Return htmx responses as SimplePageVariant Active before this works properly

  • Pipeline finished with Failed
    3 days ago
    Total: 134s
    #565323
  • Pipeline finished with Failed
    about 23 hours ago
    Total: 249s
    #566997
  • Pipeline finished with Failed
    about 23 hours ago
    #567014
  • Pipeline finished with Failed
    about 18 hours ago
    Total: 295s
    #567273
  • Merge request !12942Draft: Resolve #3535173 "Htmx aware forms" β†’ (Open) created by fathershawn
  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

    I pushed up a draft MR of how I imagine this implementation. I've commented out a condition that would depend on the upstream issue, but otherwise the single export form works here. My experience is that the form builder out of band swap needs to be deeper in the build process to prevent the validation error.

    Nice edge case catch on the nested case with a full form swap @nod_!

  • Pipeline finished with Failed
    about 12 hours ago
    #567586
  • Pipeline finished with Failed
    about 12 hours ago
    #567590
  • Pipeline finished with Failed
    about 11 hours ago
    #567606
Production build 0.71.5 2024