Form button with HTMX triggers¶
Version v1
You can apply this model as a recipe (Drupal 10.3 or later) to your own Drupal site:
1 2 3 4 5 6 7 8 9 10 11 | |
Purpose¶
This model puts a "Show writing tips" panel into the Article node form. Selecting it fetches a fragment from an ECA endpoint over HTMX and swaps that fragment into the panel. The rest of the form is not touched: nothing is submitted, nothing is validated, and no field changes.
It completes a set of three samples that all attach a button to an entity form and differ in how the button talks to the server.
| Sample | Mechanism | What the button can change |
|---|---|---|
| Field widget actions with ECA | Field widget action | The one field the button belongs to |
| Form button with an Ajax handler | Form API plus Ajax | Any number of fields, by rebuilding the form |
| This model | HTMX | No fields at all: it swaps markup into a region |
What HTMX adds, and what it does not¶
HTMX is not a faster Ajax. It is a different thing in the same place, and the difference is worth being precise about, because picking the wrong one produces a lot of wasted work.
The Ajax button in the previous sample goes through the Form API. Selecting it submits the form, Drupal validates and rebuilds it, and the response is the rebuilt form. That is what makes filling several fields possible, and it is also what makes the interaction expensive: the whole form makes a round trip, and validation has to be scoped so that an incomplete form does not block the button.
An HTMX element does not go through the Form API at all. It issues an ordinary HTTP request to a URL and swaps the response markup into a target. Drupal never sees a form submission, so there is no validation, no rebuild, and no risk of disturbing what the author has typed. In exchange it has no form state, and that cuts both ways:
- It cannot write into form fields. Doing so would mean returning raw input markup and replacing widgets by hand, which throws away the Form API and is not a pattern worth teaching.
- It cannot read unsaved input either. The "Values" option on the element is resolved with tokens on the server when the form is built, so it carries what the server knew at render time, not what the author has typed since. There is no option for HTMX's
hx-include.
What it gains is everything that does not need form state: triggers other than a click, such as load, revealed, every 30s or keyup changed delay:500ms; swapping into any element on the page rather than only the form; and a response that is a plain fragment from a URL you control.
So the dividing line is: use Ajax to change the form, use HTMX to show something beside it.
How it works¶
The "Build form" event fires for an Article node form. "HTMX: render element" places a div into the form carrying the HTMX attributes, with a data-hx-get pointing at /eca/writing-tips and a click trigger. This works because the build form event exposes the form array itself as its render array, so render actions can write into a form the same way they write into a block.
Selecting the panel makes the browser request the endpoint. Two more events serve it: an "ECA Endpoint access" event allows the request, and an "ECA Endpoint response" event sets the fragment content. The fragment carries a timestamp, so selecting the panel again visibly refetches while every field keeps its value.
Points worth copying¶
- The element is a
div, not abutton. "HTMX: render element" renders anhtml_tagwith no attributes beyond the HTMX ones, so abuttonwrapper inside a form would have notypeand would default totype="submit". The initial content holds a real<button type="button">instead, which can never submit, and the click bubbles up to thedivthat carries the trigger. - Give the element a machine name. With the build mode set to "Set and clear any previous value" and no machine name, the action assigns the built element to the whole render array, which for a build form event is the entire form.
- The panel keeps its HTMX attributes after the swap, because the swap replaces its inner HTML rather than the element itself. Selecting it again refetches, which is why the fragment ends with a plain refresh button that needs no attributes of its own.
- The endpoint response is already a bare fragment, so the "only main content" option stays off. That option exists for URLs that would otherwise render a full themed page.
- The endpoint needs its own access event. Without one the request is refused and the panel silently shows nothing.
- The timestamp uses
[date:...], not[current-date:...]. Only[date:...]is provided by Drupal core, where it falls back to the time of the current request when no date is handed to it. Thecurrent-datetype comes from the contributed Token module, so on a site without that module it has no provider at all and resolves to an empty string rather than to an error. [date:short],[date:medium]and[date:long]render the site's own configured date formats and are the better choice in most cases, because a site can change them centrally. None of them include seconds, though, and this sample needs a value that visibly changes on every click, so it formats the time itself with[date:custom:H:i:s]. Anything beyond those four, such as[date:html_time], again needs the Token module.
Requirements¶
This recipe installs the eca, eca_access, eca_endpoint, eca_form, eca_htmx and modeler_api modules. HTMX itself comes from Drupal core, so nothing else has to be downloaded.
It also applies core's article_content_type recipe, which provides the Article content type. From Drupal 11.4 the Standard profile no longer creates it, and the build form event is restricted to the article bundle, so without this step the panel would never appear.
Applying a recipe from within another one is not a pure no-op on an existing site. Configuration that is already present is never overwritten, but the included recipe also runs its own config actions, and those reset the components of the Article form and view displays to core's defaults. On a site whose Article displays have been customized, apply this recipe to a copy first.
To use the panel on another content type, change the bundle on the build form event. To place it on a page rather than a form, use the same action under a block or entity display event instead.
Dependencies¶
- module
- eca_access
- eca_endpoint
- eca_form
- eca_htmx
- modeler_api