Htmx.php

Namespace

Drupal\Core\Htmx

File

core/lib/Drupal/Core/Htmx/Htmx.php

View source
<?php

declare (strict_types=1);
namespace Drupal\Core\Htmx;

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Template\AttributeBoolean;
use Drupal\Core\Template\AttributeHelper;
use Drupal\Core\Template\AttributeString;
use Drupal\Core\Url;
use Symfony\Component\HttpFoundation\HeaderBag;
use function Symfony\Component\String\u;

/**
 * Presents the HTMX controls for developers to use with render arrays.
 *
 * HTMX is designed as an extension of HTML. It is therefore a declarative
 * markup system that uses attributes.
 *
 * @code
 * <button hx-get="/contacts/1" hx-target="#contact-ui"> <1>
 *   Fetch Contact
 * </button>
 * @endcode
 *
 * HTMX is just as happy with `data-hx-get` and so we will
 * maintain standard markup in our implementation.
 *
 * HTMX uses over 30 such attributes. The other control surface for HTMX is a
 * set of response headers. HTMX supports 11 custom response headers.
 *
 * For example, to make a select element interactive so that it will:
 *  - Send a POST request to the form URL.
 *  - Select the wrapper element of the new <select> element from the response.
 *  - Target the wrapper element of the current <select> in the rendered form
 *    for replacement.
 *  - Use the outerHTML strategy, which is to replace the whole tag.
 *
 * @code
 * use Drupal\Core\Htmx\Htmx;
 * use Drupal\Core\Url;
 *
 * $form['config_type'] = [
 *   '#title' => $this->t('Configuration type'),
 *   '#type' => 'select',
 *   '#options' => $config_types,
 *   '#default_value' => $config_type,
 * ];
 *
 * $htmx = new Htmx();
 *
 * $htmx->post()
 *   ->select('*:has(>select[name="config_name"])')
 *   ->target('*:has(>select[name="config_name"])')
 *   ->swap('outerHTML');
 * $htmx->applyTo($form['config_type']);
 * }
 * @endcode
 *
 * To dynamically update the url in the browser using a response header when
 * the config_name selector is returned:
 *
 * @code
 *  if (!empty($default_type) && !empty($default_name)) {
 *    $push = Url::fromRoute('config.export_single', [
 *      'config_type' => $default_type,
 *      'config_name' => $default_name,
 *    ]);
 *    $htmx = new Htmx();
 *    $htmx->pushUrlHeader($push);
 *    $htmx->applyTo($form['config_name']);
 *  }
 * @endcode
 *
 * Whenever a method calls for a Url object, the cacheable metadata emitted by
 * rendering the object to string is also collected and merged to the render
 * array by the `::applyTo` method.
 *
 * A static method `Htmx::createFromRenderArray` is provided which
 * takes a render array as input and builds an new instance of Htmx with all
 * the HTMX specific attributes and headers loaded from the array.
 *
 * @see https://htmx.org/reference/
 * @see https://hypermedia.systems/book/contents/
 */
class Htmx {
  
  /**
   * All HTMX attributes begin with this string.
   */
  protected const ATTRIBUTE_PREFIX = 'data-';
  
  /**
   * Initialize empty storage.
   *
   * Allows for passing a populated HeaderBag to support merging.
   */
  public function __construct(protected Attribute $attributes = new Attribute(), protected HeaderBag $headers = new HeaderBag(), protected CacheableMetadata $cacheableMetadata = new CacheableMetadata()) {
  }
  
  /**
   * Utility method to transform camelCase strings to kebab-case strings.
   *
   * Passes kebab-case strings through without any transformation.
   *
   * @param string $identifier
   *   The string to verify or transform.
   *
   * @return string
   *   The original or transformed string.
   */
  protected function ensureKebabCase(string $identifier) : string {
    // Check for existing kebab case.
    $kebabParts = explode('-', $identifier);
    // If the number of lower case parts matches the number of parts, then
    // all the parts are lower case.
    $isKebab = count($kebabParts) === count(array_filter($kebabParts, function ($part) {
      return ctype_lower($part);
    }));
    if ($isKebab) {
      return $identifier;
    }
    return (string) u($identifier)->snake()
      ->replaceMatches('#[_:]#', '-');
  }
  
  /**
   * Helper method to get the url string and store cache metadata.
   *
   * @param \Drupal\Core\Url $url
   *   The URL to generate.
   *
   * @return string
   *   The url string.
   */
  protected function urlValue(Url $url) : string {
    $generatedUrl = $url->toString(TRUE);
    $this->cacheableMetadata
      ->addCacheableDependency($generatedUrl);
    return $generatedUrl->getGeneratedUrl();
  }
  
  /**
   * Utility method to create and store a string value as an attribute.
   *
   * @param string $id
   *   The HTMX attribute id.
   * @param string $value
   *   The attribute value.
   */
  protected function createStringAttribute(string $id, string $value) : void {
    $key = self::ATTRIBUTE_PREFIX . $id;
    $this->attributes[$key] = new AttributeString($key, $value);
  }
  
  /**
   * Utility method to create and store a boolean value as an attribute.
   *
   * @param string $id
   *   The HTMX attribute id.
   * @param bool $value
   *   The attribute value.
   */
  protected function createBooleanAttribute(string $id, bool $value) : void {
    $key = self::ATTRIBUTE_PREFIX . $id;
    $this->attributes[$key] = new AttributeBoolean($key, $value);
  }
  
  /**
   * Utility method to create and store an array as an attribute.
   *
   * @param string $id
   *   The HTMX attribute id.
   * @param array{string, string} $value
   *   The attribute values.
   */
  protected function createJsonAttribute(string $id, array $value) : void {
    $key = self::ATTRIBUTE_PREFIX . $id;
    // Ensure the object format HTMX shows in documentation.
    // Ensure numeric strings are encoded as numbers.
    $json = json_encode($value, JSON_FORCE_OBJECT | JSON_NUMERIC_CHECK);
    $this->attributes[$key] = new AttributeString($key, $json);
  }
  
  /**
   * Utility function for the request attributes.
   *
   * Provides the logic for the request attribute methods.  Separate public
   * methods are maintained for clear correspondence with the attributes of
   * HTMX.
   *
   * @param string $method
   *   The request method.
   * @param \Drupal\Core\Url|null $url
   *   The URL for the request.
   *
   * @return static
   *   returns self so that attribute methods may be chained.
   */
  protected function buildRequestAttribute(string $method, ?Url $url = NULL) : static {
    if (is_null($url)) {
      $value = '';
    }
    else {
      $value = $this->urlValue($url);
    }
    $this->createStringAttribute($method, $value);
    return $this;
  }
  
  /**
   * Apply the header values to the render array.
   */
  protected function applyHeaders() : array {
    $drupalHeaders = [];
    foreach ($this->headers as $name => $values) {
      foreach ($values as $value) {
        // Set replace to true.
        $drupalHeaders[] = [
          $name,
          $value,
          TRUE,
        ];
      }
    }
    return $drupalHeaders;
  }
  
  /**
   * Checks if a header is set.
   *
   * @param string $name
   *   The name of the header.
   *
   * @return bool
   *   True if header is stored.
   */
  public function hasHeader(string $name) : bool {
    return $this->headers
      ->has($name);
  }
  
  /**
   * Checks if an attribute is set.
   *
   * @param string $name
   *   The name of the attribute.
   *
   * @return bool
   *   True if attribute is stored.
   */
  public function hasAttribute(string $name) : bool {
    return $this->attributes
      ->hasAttribute($name);
  }
  
  /**
   * Removes a header from the header store.
   *
   * @param string $name
   *   The header name to remove.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   */
  public function removeHeader(string $name) : static {
    $this->headers
      ->remove($name);
    return $this;
  }
  
  /**
   * Removes an attribute from the attribute store.
   *
   * @param string $name
   *   The attribute name to remove.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   */
  public function removeAttribute(string $name) : static {
    $this->attributes
      ->removeAttribute($name);
    return $this;
  }
  
  /**
   * Get the attribute storage.
   *
   * @return \Drupal\Core\Template\Attribute
   *   The attribute storage.
   */
  public function getAttributes() : Attribute {
    return $this->attributes;
  }
  
  /**
   * Get the header storage.
   *
   * @return \Symfony\Component\HttpFoundation\HeaderBag
   *   The header storage.
   */
  public function getHeaders() : HeaderBag {
    return $this->headers;
  }
  
  /**
   * Set HX-Location header.
   *
   * @param \Drupal\Core\Url|\Drupal\Core\Htmx\HtmxLocationResponseData $data
   *   Use Url if only a path is needed.
   *
   * @return static
   *   Self for chaining.
   *
   * @see https://htmx.org/headers/hx-location/
   */
  public function locationHeader(Url|HtmxLocationResponseData $data) : static {
    if ($data instanceof HtmxLocationResponseData) {
      $value = (string) $data;
      $this->cacheableMetadata
        ->addCacheableDependency($data->getCacheableMetadata());
    }
    else {
      $value = $this->urlValue($data);
    }
    $this->headers
      ->set('HX-Location', $value);
    return $this;
  }
  
  /**
   * Set HX-Push-Url header.
   *
   * @param \Drupal\Core\Url|false $value
   *   URL to push to the location bar or false to prevent a history update.
   *
   * @return static
   *   Self for chaining.
   *
   * @see https://htmx.org/headers/hx-push-url/
   */
  public function pushUrlHeader(Url|false $value) : static {
    $url = 'false';
    if ($value instanceof Url) {
      $url = $this->urlValue($value);
    }
    $this->headers
      ->set('HX-Push-Url', $url);
    return $this;
  }
  
  /**
   * Set HX-Replace-Url header.
   *
   * @param \Drupal\Core\Url|false $data
   *   URL for history replacement, false  prevents updates to the current URL.
   *
   * @return static
   *   Self for chaining.
   *
   * @see https://htmx.org/headers/hx-replace-url/
   */
  public function replaceUrlHeader(Url|false $data) : static {
    $value = 'false';
    if ($data instanceof Url) {
      $value = $this->urlValue($data);
    }
    $this->headers
      ->set('HX-Replace-Url', $value);
    return $this;
  }
  
  /**
   * Set HX-Redirect header.
   *
   * @param \Drupal\Core\Url $url
   *   Destination for a client side redirection.
   *
   * @return static
   *   Self for chaining.
   *
   * @see https://htmx.org/headers/hx-redirect/
   */
  public function redirectHeader(Url $url) : static {
    $this->headers
      ->set('HX-Redirect', $this->urlValue($url));
    return $this;
  }
  
  /**
   * Set HX-Refresh header.
   *
   * @param bool $refresh
   *   If set to “true” the client-side will do a full refresh of the page.
   *
   * @return static
   *   Self for chaining.
   *
   * @see https://htmx.org/reference/#response_headers
   */
  public function refreshHeader(bool $refresh) : static {
    $this->headers
      ->set('HX-Refresh', $refresh ? 'true' : 'false');
    return $this;
  }
  
  /**
   * Set HX-Reswap header.
   *
   * @param string $strategy
   *   Specify how the response will be swapped (see hx-swap).
   *
   * @return static
   *   Self for chaining.
   *
   * @see https://htmx.org/reference/#response_headers
   */
  public function reswapHeader(string $strategy) : static {
    $this->headers
      ->set('HX-Reswap', $strategy);
    return $this;
  }
  
  /**
   * Set HX-Retarget header.
   *
   * @param string $strategy
   *   CSS selector that replaces the target to a different element on the page.
   *
   * @return static
   *   Self for chaining.
   *
   * @see https://htmx.org/reference/#response_headers
   */
  public function retargetHeader(string $strategy) : static {
    $this->headers
      ->set('HX-Retarget', $strategy);
    return $this;
  }
  
  /**
   * Set HX-Reselect header.
   *
   * @param string $strategy
   *   CSS selector that changes the selection taken from the response.
   *
   * @return static
   *   Self for chaining.
   *
   * @see https://htmx.org/reference/#response_headers
   */
  public function reselectHeader(string $strategy) : static {
    $this->headers
      ->set('HX-Reselect', $strategy);
    return $this;
  }
  
  /**
   * Set HX-Trigger header.
   *
   * See the documentation for the structure of the array.
   *
   * @param string|array $data
   *   An event name or an array which will be JSON encoded.
   *
   * @return static
   *   Self for chaining.
   *
   * @see https://htmx.org/headers/hx-trigger/
   */
  public function triggerHeader(string|array $data) : static {
    if (is_array($data)) {
      $data = json_encode($data);
    }
    $this->headers
      ->set('HX-Trigger', $data);
    return $this;
  }
  
  /**
   * Set HX-Trigger-After-Settle header.
   *
   * See the documentation for the structure of the array.
   *
   * @param string|array $data
   *   An event name or an array which will be JSON encoded.
   *
   * @return static
   *   Self for chaining.
   *
   * @see https://htmx.org/headers/hx-trigger/
   */
  public function triggerAfterSettleHeader(string|array $data) : static {
    if (is_array($data)) {
      $data = json_encode($data);
    }
    $this->headers
      ->set('HX-Trigger-After-Settle', $data);
    return $this;
  }
  
  /**
   * Set HX-Trigger-After-Swap header.
   *
   * See the documentation for the structure of the array.
   *
   * @param string|array $data
   *   An event name or an array which will be JSON encoded.
   *
   * @return static
   *   Self for chaining.
   *
   * @see https://htmx.org/headers/hx-trigger/
   */
  public function triggerAfterSwapHeader(string|array $data) : static {
    if (is_array($data)) {
      $data = json_encode($data);
    }
    $this->headers
      ->set('HX-Trigger-After-Swap', $data);
    return $this;
  }
  
  /**
   * Creates a `data-hx-get` attribute.
   *
   * This attribute instructs HTMX to issue a GET request to the specified URL.
   *
   * This request method also accepts no parameters, which issues a get
   * request to the current url. If parameters are used, both are required.
   *
   * @param \Drupal\Core\Url|null $url
   *   The URL for the GET request. If NULL the current page is used.
   *
   * @return static
   *   returns self so that attribute methods may be chained.
   *
   * @see https://htmx.org/attributes/hx-get/
   */
  public function get(?Url $url = NULL) : static {
    return $this->buildRequestAttribute('hx-get', $url);
  }
  
  /**
   * Creates a `data-hx-post` attribute.
   *
   * This attribute instructs HTMX to issue a POST request to the specified URL.
   *
   *  This request method also accepts no parameters, which issues a post
   *  request to the current url. If parameters are used, both are required.
   *
   * @param \Drupal\Core\Url|null $url
   *   The URL for the POST request. If NULL the current page is used.
   *
   * @return static
   *   returns self so that attribute methods may be chained.
   *
   * @see https://htmx.org/attributes/hx-post/
   */
  public function post(?Url $url = NULL) : static {
    return $this->buildRequestAttribute('hx-post', $url);
  }
  
  /**
   * Creates a `data-hx-put` attribute.
   *
   * This attribute instructs HTMX to issue a PUT request to the specified URL.
   *
   *  This request method also accepts no parameters, which issues a put
   *  request to the current url. If parameters are used, both are required.
   *
   * @param \Drupal\Core\Url|null $url
   *   The URL for the PUT request. If NULL the current page is used.
   *
   * @return static
   *   returns self so that attribute methods may be chained.
   *
   * @see https://htmx.org/attributes/hx-put/
   */
  public function put(?Url $url = NULL) : static {
    return $this->buildRequestAttribute('hx-put', $url);
  }
  
  /**
   * Creates a `data-hx-patch` attribute.
   *
   * This attribute instructs HTMX to issue a PATCH request
   *
   *  This request method also accepts no parameters, which issues a patch
   *  request to the current url. If parameters are used, both are required.
   *
   * @param \Drupal\Core\Url|null $url
   *   Collects the cacheable metadata from the URL generation.
   *
   * @return static
   *   returns self so that attribute methods may be chained.
   *
   * @see https://htmx.org/attributes/hx-patch/
   */
  public function patch(?Url $url = NULL) : static {
    return $this->buildRequestAttribute('hx-patch', $url);
  }
  
  /**
   * Creates a `data-hx-delete` attribute.
   *
   * This attribute instructs HTMX to issue a DELETE request
   * to the specified URL.
   *
   *  This request method also accepts no parameters, which issues a delete
   *  request to the current url. If parameters are used, both are required.
   *
   * @param \Drupal\Core\Url|null $url
   *   The URL for the DELETE request. If NULL the current page is used.
   *
   * @return static
   *   returns self so that attribute methods may be chained.
   *
   * @see https://htmx.org/attributes/hx-delete/
   */
  public function delete(?Url $url = NULL) : static {
    return $this->buildRequestAttribute('hx-delete', $url);
  }
  
  /**
   * Creates a `data-hx-on` attribute.
   *
   * This attribute instructs HTMX to react to events with inline scripts
   * on elements.
   * - $event is the name of the JavaScript event.
   * - $action is the JavaScript statement to execute when the event occurs.
   *   This can be a short instruction or call a function in a script that
   *   has been loaded by the page.
   *
   * @param string $event
   *   An event in either camelCase or kebab-case.
   * @param string $action
   *   The JavaScript statement.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-on/
   */
  public function on(string $event, string $action) : static {
    // Special case: the `::EventName` shorthand for `htmx:EventName`.
    // Prepare a leading '-' so that our final attribute is
    // `data-hx--event-name` rather than `data-hx-event-name`.
    $extra = str_starts_with($event, '::') ? '-' : '';
    $formattedEvent = 'hx-on-' . $extra . $this->ensureKebabCase($event);
    $this->createStringAttribute($formattedEvent, $action);
    return $this;
  }
  
  /**
   * Creates a `data-hx-push-url` attribute.
   *
   * This attribute instructs HTMX to control URLs in the browser history.
   *
   * Use a boolean when this attribute is added along with ::get
   * - true: pushes the fetched URL into history.
   * - false: disables pushing the fetched URL if it would otherwise be pushed
   *   due to inheritance or hx-boost.
   *
   * Use a URL to cause a push into the location bar. This may be relative or
   * absolute, as per history.pushState()
   *
   * @param bool|\Drupal\Core\Url $value
   *   Use a Url object or a boolean, depending on the use case.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-push-url/
   */
  public function pushUrl(bool|Url $value) : static {
    $url = $value === FALSE ? 'false' : 'true';
    if ($value instanceof Url) {
      $url = $this->urlValue($value);
    }
    $this->createStringAttribute('hx-push-url', $url);
    return $this;
  }
  
  /**
   * Creates a `data-hx-select` attribute.
   *
   * This attribute instructs HTMX which content to swap in from a response.
   * HTMX uses the given selector to select elements from the response.
   * For example, passing 'data-drupal-selector="edit-theme-settings"' will
   * instruct HTMX to select the element with this data attribute and value.
   *
   * @param string $selector
   *   A CSS selector string.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-select/
   */
  public function select(string $selector) : static {
    $this->createStringAttribute('hx-select', $selector);
    return $this;
  }
  
  /**
   * Creates a `data-hx-select-oob` attribute.
   *
   * This attribute instructs HTMX to select content for an out-of-band swap
   * from a response. Each value can specify any valid hx-swap strategy by
   * separating the selector and the swap strategy with a colon,
   * such as #alert:afterbegin.
   *
   * @param string|string[] $selectors
   *   A value or array of values.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-select-oob/
   */
  public function selectOob(string|array $selectors) : static {
    if (is_array($selectors)) {
      $selectors = implode(',', $selectors);
    }
    $this->createStringAttribute('hx-select-oob', $selectors);
    return $this;
  }
  
  /**
   * Creates a `data-hx-swap` attribute.
   *
   * This attribute allows you to specify how the response will be
   * swapped into the DOM relative to the target of an AJAX request.
   *
   * @param string $strategy
   *   The swap strategy.
   * @param string $modifiers
   *   Optional modifiers for changing the behavior of the swap.
   * @param bool $ignoreTitle
   *   Instruct HTMX not to swap in the page title from the request.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-swap/
   */
  public function swap(string $strategy, string $modifiers = '', bool $ignoreTitle = TRUE) : static {
    if ($modifiers !== '') {
      $strategy .= ' ' . $modifiers;
    }
    // HTMX defaults this behavior to FALSE, that is it replaces the page title.
    // We believe our most common use case is to not change the title.
    if ($ignoreTitle) {
      $strategy .= ' ignoreTitle:true';
    }
    $this->createStringAttribute('hx-swap', $strategy);
    return $this;
  }
  
  /**
   * Creates a `data-hx-swap-oob` attribute.
   *
   * This attribute is used in the markup of the returned response. It
   * specifies that some content in a response should be swapped into the DOM
   * somewhere other than the target, that is “Out of Band”. This allows you to
   * piggyback updates to other elements on a response.
   *
   * @param true|string $value
   *   Either true, a swap strategy, or strategy:CSS-selector.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-swap-oob/
   */
  public function swapOob(true|string $value) : static {
    if ($value === TRUE) {
      $value = 'true';
    }
    $this->createStringAttribute('hx-swap-oob', $value);
    return $this;
  }
  
  /**
   * Creates a `data-hx-target` attribute.
   *
   * This attribute allows you to target a different element for
   * swapping than the one issuing the AJAX request. There are a variety
   * of target string syntaxes.  See the URL below for details.
   *
   * @param string $target
   *   The target descriptor.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-target/
   */
  public function target(string $target) : static {
    $this->createStringAttribute('hx-target', $target);
    return $this;
  }
  
  /**
   * Creates a `data-hx-trigger` attribute.
   *
   * This attribute instructs HTMX when to trigger a request.
   *
   * Used with an HTMX request attribute. Allows:
   * - An event name (e.g. “click” or “myCustomEvent”) followed by an event
   *   filter and a set of event modifiers
   * - A polling definition of the form every <timing declaration>
   * - A comma-separated list of such events.
   *
   * @param string|string[] $triggerDefinition
   *   The trigger definition.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-trigger/
   */
  public function trigger(string|array $triggerDefinition) : static {
    if (is_array($triggerDefinition)) {
      $triggerDefinition = implode(',', $triggerDefinition);
    }
    $this->createStringAttribute('hx-trigger', $triggerDefinition);
    return $this;
  }
  
  /**
   * Creates a `data-hx-vals` attribute.
   *
   * This attribute instructs HTMX to add values to the parameters that will be
   * submitted with an HTMX request.
   *
   * The value of this attribute is a list of name-expression values
   * which will be converted to JSON (JavaScript Object Notation) format.
   *
   * @param array<string, string> $values
   *   The values in an array of 'name' => 'value' pairs.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-vals/
   */
  public function vals(array $values) : static {
    $this->createJsonAttribute('hx-vals', $values);
    return $this;
  }
  
  /**
   * Creates a `data-hx-boost` attribute.
   *
   * This attribute instructs HTMX to add progressive enhancement
   * to links or forms. The attribute allows you to “boost” normal anchors and
   * form tags to use AJAX instead. This has the nice fallback that, if the
   * user does not have javascript enabled, the site will continue to work.
   *
   * @param bool $value
   *   Should the element and its descendants be "boosted"?
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-boost/
   */
  public function boost(bool $value) : static {
    $this->createStringAttribute('hx-boost', $value ? 'true' : 'false');
    return $this;
  }
  
  /**
   * Creates a `data-hx-confirm` attribute.
   *
   * This attribute instructs HTMX to shows a confirm() dialog before issuing
   * a request.
   *
   * @param string $message
   *   The user facing message.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-confirm/
   */
  public function confirm(string $message) : static {
    $this->createStringAttribute('hx-confirm', $message);
    return $this;
  }
  
  /**
   * Creates a `data-hx-disable` attribute.
   *
   * This attribute instructs HTMX to disable HTMX processing for the given
   * node and any descendants.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-disable/
   */
  public function disable() : static {
    $this->createBooleanAttribute('hx-disable', TRUE);
    return $this;
  }
  
  /**
   * Creates a `data-hx-disabled-elt` attribute.
   *
   * This attribute instructs HTMX to add the disabled attribute to the
   * specified elements during a request.
   *
   * The descriptor syntax is the same as hx-target. See the documentation
   * link below for more details.
   *
   * @param string $descriptor
   *   The attribute value.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-disabled-elt/
   */
  public function disabledElt(string $descriptor) : static {
    $this->createStringAttribute('hx-disabled-elt', $descriptor);
    return $this;
  }
  
  /**
   * Creates a `data-hx-disinherit` attribute.
   *
   * This attribute instructs HTMX to control and disable automatic HTMX
   * attribute inheritance for child nodes.
   *
   * @param string $names
   *   The attribute names to disinherit or * for all.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-disinherit/
   */
  public function disinherit(string $names) : static {
    $this->createStringAttribute('hx-disinherit', $names);
    return $this;
  }
  
  /**
   * Creates a `data-hx-encoding` attribute.
   *
   * This attribute instructs HTMX to change the request encoding type.
   *
   * @param string $method
   *   The encoding method.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-encoding/
   */
  public function encoding(string $method = 'multipart/form-data') : static {
    $this->createStringAttribute('hx-encoding', $method);
    return $this;
  }
  
  /**
   * Creates a `data-hx-ext` attribute.
   *
   * This attribute instructs HTMX to enable HTMX extensions for an element
   * and descendants.
   *
   * @param string $names
   *   An extension name, or a comma separated list of names.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-ext/
   */
  public function ext(string $names) : static {
    $this->createStringAttribute('hx-ext', $names);
    return $this;
  }
  
  /**
   * Creates a `data-hx-headers` attribute.
   *
   * This attribute instructs HTMX to add to the headers that will be submitted
   * with an HTMX request.
   *
   * @param array<string, string> $headerValues
   *   The header values as name => value.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-headers/
   */
  public function headers(array $headerValues) : static {
    $this->createJsonAttribute('hx-headers', $headerValues);
    return $this;
  }
  
  /**
   * Creates a `data-hx-history` attribute.
   *
   * The attribute value is set to false. This attribute prevents sensitive
   * data from being saved to the localStorage cache when htmx takes a snapshot
   * of the page state. This attribute is effective when set on any element in
   * the current document, or any html fragment loaded into the current document
   * by htmx
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-history/
   */
  public function history() : static {
    $this->createStringAttribute('hx-history', 'false');
    return $this;
  }
  
  /**
   * Creates a `data-hx-history-elt` attribute.
   *
   * This attribute instructs HTMX which element to snapshot and restore
   * during history navigation.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-history-elt/
   */
  public function historyElt() : static {
    $this->createBooleanAttribute('hx-history-elt', TRUE);
    return $this;
  }
  
  /**
   * Creates a `data-hx-include` attribute.
   *
   * This attribute instructs HTMX to include additional element values
   * in HTMX requests.
   *
   * The descriptor syntax is the same as hx-target. See the documentation
   * link below for more details.
   *
   * @param string $descriptors
   *   The element descriptors.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-include/
   */
  public function include(string $descriptors) : static {
    $this->createStringAttribute('hx-include', $descriptors);
    return $this;
  }
  
  /**
   * Creates a `data-hx-indicator` attribute.
   *
   * This attribute instructs HTMX which element should receive the
   * htmx-request class on during the request.
   *
   * @param string $selector
   *   The element CSS selector value. Selector may be prefixed with `closest`.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-indicator/
   */
  public function indicator(string $selector) : static {
    $this->createStringAttribute('hx-indicator', $selector);
    return $this;
  }
  
  /**
   * Creates a `data-hx-inherit` attribute.
   *
   * This attribute instructs HTMX how to control automatic attribute
   * inheritance for child nodes.
   *
   * HTMX evaluates attribute inheritance with hx-inherit in two ways when
   * hx-inherit is set on a parent node:
   *  - data-hx-inherit="*"
   *    All attribute inheritance for this element will be enabled.
   * - data-hx-hx-inherit="hx-select hx-get hx-target"
   *   Enable inheritance for only one or multiple specified attributes.
   *
   * @param string $attributes
   *   The attributes to inherit.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-inherit/
   */
  public function inherit(string $attributes) : static {
    $this->createStringAttribute('hx-inherit', $attributes);
    return $this;
  }
  
  /**
   * Creates a `data-hx-params` attribute.
   *
   * This attribute instructs HTMX to filter the parameters that will
   * be submitted with a request.
   *
   * Verify the current filter syntax at the link below.
   * - To include all parameters, use the string '*'.
   * - To pass no parameters use 'none'.
   * - To exclude some parameters use an array of parameters names, prefixing
   *   the first name with `not`, as in ['not param1', 'param2', 'param3'].
   * - To only submit some parameters, use an array of parameter names as in
   *   ['param1', 'param2', 'param3'].
   * parameters use an array, pre
   *
   * @param string|string[] $filter
   *   The filter string or strings.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-params/
   */
  public function params(string|array $filter) : static {
    if (is_array($filter)) {
      $filter = implode(',', $filter);
    }
    $this->createStringAttribute('hx-params', $filter);
    return $this;
  }
  
  /**
   * Creates a `data-hx-preserve` attribute.
   *
   * This attribute instructs HTMX that matching elements should be kept
   * unchanged between requests. Depends on an unchanging id property on the
   * element.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-preserve/
   */
  public function preserve() : static {
    $this->createBooleanAttribute('hx-preserve', TRUE);
    return $this;
  }
  
  /**
   * Creates a `data-hx-prompt` attribute.
   *
   * This attribute instructs HTMX to show a prompt() before
   * submitting a request.
   *
   * @param string $message
   *   The message to display in the prompt.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-prompt/
   */
  public function prompt(string $message) : static {
    $this->createStringAttribute('hx-prompt', $message);
    return $this;
  }
  
  /**
   * Creates a `data-hx-replace-url` attribute.
   *
   * This attribute instructs HTMX to control URLs in the browser location bar.
   *
   * Use a boolean when this attribute is added along with a request:
   * - true: replaces the fetched URL in the browser navigation bar.
   * - false: disables replacing the fetched URL if it would otherwise be
   *   replaced due to inheritance.
   *
   * Use a URL to replace the value in the location bar. This may be relative or
   * absolute, as per history.replaceState().
   *
   * @param bool|\Drupal\Core\Url $value
   *   A Url object, or a boolean, depending on the use case. See details above.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-replace-url/
   */
  public function replaceUrl(bool|Url $value) : static {
    $url = $value ? 'true' : 'false';
    if ($value instanceof Url) {
      $url = $this->urlValue($value);
    }
    $this->createStringAttribute('hx-replace-url', $url);
    return $this;
  }
  
  /**
   * Creates a `data-hx-request` attribute.
   *
   *  This attribute instructs HTMX to configure various aspects of the request.
   *
   * The hx-request attribute supports the following configuration values:
   * - timeout: (integer) the timeout for the request, in milliseconds.
   * - credentials: (boolean) if the request will send credentials.
   * - noHeaders: (boolean) strips all headers from the request.
   *
   * Dynamic javascript values are not supported for security and for
   * simplicity.  If you need calculated values you should do determine them
   * here on the server-side
   *
   * @param array<string, int|bool> $configValues
   *   The configuration values as name => value.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-request/
   */
  public function request(array $configValues) : static {
    $this->createJsonAttribute('hx-request', $configValues);
    return $this;
  }
  
  /**
   * Creates a `data-hx-sync` attribute.
   *
   * This attribute instructs HTMX to synchronize AJAX requests
   * between multiple elements.
   *
   * @param string $selector
   *   A CSS selector followed by a strategy.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-sync/
   */
  public function sync(string $selector) : static {
    $this->createStringAttribute('hx-sync', $selector);
    return $this;
  }
  
  /**
   * Creates a `data-hx-validate` attribute.
   *
   * This attribute instructs HTMX to cause an element to validate itself
   * before it submits a request.
   *
   * @param bool $value
   *   Should the element validate before the request.
   *
   * @return static
   *   Returns this object to allow chaining methods.
   *
   * @see https://htmx.org/attributes/hx-validate/
   */
  public function validate(bool $value = TRUE) : static {
    $this->createStringAttribute('hx-validate', $value ? 'true' : 'false');
    return $this;
  }
  
  /**
   * Exports data from internal storage to a render array.
   *
   * @param mixed[] $element
   *   The render array for the element.
   * @param string $attributeKey
   *   Optional target key for attribute output: defaults to '#attributes'.
   */
  public function applyTo(array &$element, string $attributeKey = '#attributes') : void {
    // Attach HTMX and Drupal integration javascript.
    if (!in_array('core/drupal.htmx', $element['#attached']['library'] ?? [])) {
      $element['#attached']['library'][] = 'core/drupal.htmx';
    }
    // Consolidate headers.
    if ($this->headers
      ->count() !== 0) {
      $element['#attached']['http_header'] = $element['#attached']['http_header'] ?? [];
      $element['#attached']['http_header'] = NestedArray::mergeDeep($element['#attached']['http_header'], $this->applyHeaders());
    }
    if (count($this->attributes
      ->storage()) !== 0) {
      // Consolidate attributes.
      $element[$attributeKey] = $element[$attributeKey] ?? [];
      $element[$attributeKey] = AttributeHelper::mergeCollections($element[$attributeKey], $this->attributes);
    }
    $this->cacheableMetadata
      ->applyTo($element);
  }
  
  /**
   * Creates an Htmx object with values taken from a render array.
   *
   * @param array $element
   *   A render array.
   * @param string $attributeKey
   *   Optional target key for attribute output: defaults to '#attributes'.
   *
   * @return static
   *   A new instance of this class.
   */
  public static function createFromRenderArray(array $element, string $attributeKey = '#attributes') : static {
    $incomingAttributes = $element[$attributeKey] ?? [];
    $incomingHeaders = $element['#attached']['http_header'] ?? [];
    // Filter for HTMX values.
    $incomingAttributes = array_filter($incomingAttributes, function (string $key) {
      return str_starts_with($key, 'data-hx-');
    }, ARRAY_FILTER_USE_KEY);
    $preparedHeaders = [];
    foreach ($incomingHeaders as $value) {
      if (is_array($value) && str_starts_with($value[0], 'hx-')) {
        // Header value array may have 3 values, we want the first two.
        $preparedHeaders[$value[0]] = $value[1];
      }
    }
    $attributes = new Attribute($incomingAttributes);
    $headers = new HeaderBag($preparedHeaders);
    $cacheableMetadata = CacheableMetadata::createFromRenderArray($element);
    return new static($attributes, $headers, $cacheableMetadata);
  }

}

Classes

Title Deprecated Summary
Htmx Presents the HTMX controls for developers to use with render arrays.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.