HTMX Block Admin
Add a block button
The interactive elements of the admin interface for HTMX blocks are built using HTMX. Let's examine the markup.
<button
class="button button--action button--primary button--htmx button--htmx-refresh"
aria-live="polite"
name="open-plugin-dialog"
data-hx-get="/htmx/blocks/system-dialog"
data-hx-select="#drupal-off-canvas-wrapper"
data-hx-target="main" data-hx-swap="afterbegin"
data-hx-on--after-on-load="setListWithDialogOpen(this)"
data-hx-on-plugin-dialog-removed="setListWithoutDialog(this)">
<span class="default-text">Add a block</span>
<span class="htmx-indicator">Loading...</span>
</button>
This element reacts to its normal event, click
, to issue a get request to
/htmx/blocks/system-dialog
. From that request HTMX selects the tag with id
drupal-off-canvas-wrapper
. The CSS selector main
is the destination, or
target, for this selected content. HTMX calls this insertion a swap. The
swap strategy is afterbegin
which means that the content
selected from the request will be appended to the beginning of the content
within the main
tag and the title of the page will not be updated with the
title of from the incoming request. There are two HTMX event reactions stated
using data-hx-on-<event>
. The double dash in data-hx-on--after-on-load
is
shorthand for htmx:
and the full event name is htmx:afterOnLoad.
A custom function setListWithDialogOpen
is called and passed the button
element as a parameter. The second use, data-hx-on-plugin-dialog-removed
reacts to a custom event, pluginDialogRemoved
. The two span
tags leverage
dynamic CSS added and removed during HTMX request processing.
The inserted dialog contains a table of plugin names with a button like this on each row:
Add button in the plugin list
<input
class="button js-form-submit form-submit"
data-hx-get="/htmx/blocks/add/announce_block"
data-hx-target="#drupal-off-canvas-wrapper"
data-hx-select="#drupal-off-canvas-wrapper"
data-hx-swap="outerHTML"
type="submit"
name="op"
value="Add">
This input element has no form enclosing form, but even so it works because HTMX is capturing its natural event and issues a GET request. HTMX gets the appropriate block form prepared as an off canvas dialog and replaces the existing off canvas dialog entirely.
The off canvas dialog itself is enhanced with HTMX (other elements removed for clarity):
Dialog header bar
<dialog id="drupal-off-canvas-wrapper"
data-hx-on--load="revealOffCanvasWithInput(this)"
class="htmx-dialog-off-canvas" open="">
<div class="ui-dialog-titlebar"><span class="ui-dialog-title">Add an HTMX block</span>
<button
class="button--htmx button--htmx-refresh"
aria-live="polite"
data-hx-get="/htmx/blocks/system-dialog"
data-hx-select="table.block-add-table"
data-hx-target="dialog > table.block-add-table"
data-hx-swap="outerHTML">
<span class="default-text">Refresh</span>
<span class="htmx-indicator">Loading...</span>
</button>
<button
class="button button--action button--primary ui-button-icon-only
ui-dialog-titlebar-close button--htmx"
data-hx-on-click="removeOffCanvasDialog(this)">
<span class="default-text">Close</span>
</button>
</div>
</dialog>
The dialog
tag uses the load event to trigger a custom snippet that displays
the dialog. The first button
refreshes the list of available blocks by
re-requesting the dialog and only selecting the table from the response.
The second button
calls a custom snippet that removes the dialog.
The HTMX block form has its action and method properties removed and its submit buttons altered to rely completely on HTMX:
Enhancing the form
<input data-hx-select="#htmx-block-list"
data-hx-target="#htmx-block-list"
data-hx-swap="outerHTML"
data-hx-on--after-on-load="removeOffCanvasDialog(this)"
data-hx-post="/htmx/blocks/add/system_branding_block"
data-drupal-selector="edit-actions-submit" type="submit"
id="edit-actions-submit" name="op" value="Save"
class="button button--primary js-form-submit form-submit">
<input data-hx-post="/htmx/blocks/add/system_branding_block"
data-hx-select="dialog"
data-hx-target="closest dialog"
data-hx-swap="outerHTML"
data-hx-select-oob="#htmx-block-list"
data-drupal-selector="edit-actions-submit-continue" type="submit"
id="edit-actions-submit-continue" name="op" value="Save and continue"
class="button button--secondary js-form-submit form-submit">
The first input
posts the form, gets the block list from the response and
fully replaces the block list in the page. The second input
selects the
dialog
and replaces the closest dialog
, with another blank form. It also
selects the block list for the main portion of the page, which now includes the
added block and replaces it.
Each row in the HTMX block list has an operations drop button that has been enhanced to work via HTMX.
Operations buttons
<div class="dropbutton-wrapper dropbutton-multiple"
data-drupal-ajax-container="" data-once="dropbutton">
<div class="dropbutton-widget">
<ul data-hx-boost="true"
class="dropbutton dropbutton--extrasmall dropbutton--multiple">
<li class="edit dropbutton__item dropbutton-action">
<a href="/htmx/blocks/edit/poweredbydrupal"
data-hx-select="#drupal-off-canvas-wrapper"
data-hx-target="main"
data-hx-swap="afterbegin"
data-hx-push-url="false">
Edit
</a>
</li>
<li class="dropbutton-toggle">
<button type="button" class="dropbutton__toggle">
<span class="visually-hidden">List additional actions</span>
</button>
</li>
<li class="delete dropbutton__item dropbutton-action secondary-action">
<a href="/htmx/blocks/delete/poweredbydrupal"
data-hx-select="#drupal-off-canvas-wrapper"
data-hx-target="main"
data-hx-swap="afterbegin"
data-hx-push-url="false">
Delete
</a>
</li>
</ul>
</div>
</div>
The hx-boost="true"
property is inherited by all child elements. This property
instructs HTMX to take over the behavior of a
and form
tags so that they
can be enhanced with other HTMX properties. The edit button href
then becomes
a get request from which the edit form wrapped in off canvas dialog markup is
selected and and revealed. A similar process displays the delete form in the
off canvas dialog.