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 hx-get="/htmx/blocks/system-dialog"
        hx-select="#drupal-off-canvas-wrapper"
        hx-target="main" hx-swap="afterbegin ignoreTitle:true"
        hx-on::after-on-load="setListWithDialogOpen(this)"
        hx-on:plugin-dialog-removed="setListWithoutDialog(this)"
        class="button button--action button--primary button--htmx"
        name="open-plugin-dialog" aria-live="polite">
    <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 ignoreTitle:true 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 hx-on:<event>. The double colon in 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, 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

<button hx-get="/htmx/blocks/add/announce_block"
        hx-target="#drupal-off-canvas-wrapper"
        hx-select="#drupal-off-canvas-wrapper"
        hx-swap="outerHTML ignoreTitle:true" class="button">Add</button>
\Drupal\htmx\Controller\HtmxBlockAdminController::listBlocksDialog

The button 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"
        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 hx-get="/htmx/blocks/system-dialog"
            hx-select="table.block-add-table"
            hx-target="dialog > table.block-add-table"
            hx-swap="outerHTML ignoreTitle:true" class="button--htmx button--htmx-refresh" aria-live="polite">
            <span class="default-text">Refresh</span> <span class="htmx-indicator">Loading...</span>
    </button>
    <button
      hx-on:click="removeOffCanvasDialog(this)" class="button button--action button--primary ui-button-icon-only ui-dialog-titlebar-close button--htmx"
      <span="">Close
    </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 hx-post="/htmx/blocks/add/page_title_block"
       hx-swap="outerHTML ignoreTitle:true"
       hx-target="#htmx-block-list"
       hx-select="#htmx-block-list"
       hx-on::after-on-load="removeOffCanvasDialog(this)"
       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 hx-post="/htmx/blocks/add/page_title_block"
       hx-swap="outerHTML ignoreTitle:true"
       hx-select="dialog"
       hx-target="closest dialog"
       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 hx-boost="true" class="dropbutton dropbutton--extrasmall dropbutton--multiple">
      <li class="edit dropbutton__item dropbutton-action">
        <a href="/htmx/blocks/edit/pagetitle"
           hx-select="#drupal-off-canvas-wrapper"
           hx-target="main"
           hx-swap="afterbegin ignoreTitle:true"
           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/pagetitle"
           hx-select="#drupal-off-canvas-wrapper"
           hx-target="main"
           hx-swap="afterbegin ignoreTitle:true"
           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.