Skip to content

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>
\Drupal\htmx\Entity\HtmxBlockListBuilder::render

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">
\Drupal\htmx\Controller\HtmxBlockAdminController::listBlocksDialog

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>
\Drupal\htmx\Controller\HtmxBlockAdminController::listBlocksDialog

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">
\Drupal\htmx\Form\HtmxBlockForm::actions

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>
\Drupal\htmx\Entity\HtmxBlockListBuilder::buildOperations \Drupal\htmx\Entity\HtmxBlockListBuilder::getDefaultOperations

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.