[POC] Implementing some components of the Ajax system using HTMX

Created on 11 May 2024, 7 months ago

Problem/Motivation

This task is a proof-of-concept for 🌱 [Plan] Gradually replace Drupal's AJAX system with HTMX Active as href=" https://www.drupal.org/project/drupal/issues/3404409#comment-15581769 🌱 [Plan] Gradually replace Drupal's AJAX system with HTMX Active ">requested
by @nod_.

Reference Links

Remaining tasks

  1. One or two examples (at least) of replacements for existing core ajax
  2. A list of the various ajax commands we have in core and their feasibility
    (with a very rough complexity estimate easy/hard) with htmx
  3. Test runs don't need to be green, although it' be nice if a couple of them
    were passing.
  4. can we use this to handle ajax form submit? if yes, how complex does it
    look? would be great to drop our jquery formsubmit fork
  5. A big thing here is to improve DX, so a couple of examples of how it could
    make it easier to introduce some ajax stuff in contrib/projects would be
    good.
  6. rough dependency evaluation
    https://www.drupal.org/about/core/policies/core-dependency-policies/depe β†’ ...
    (being a great meme poster is not enough :p)
  7. BC strategy (support existing calls, or a new codepath altogether, etc.)
    I'm not too keen on two separate mods. I feel it'll make adoption much, much
    slower than spending time on the BC layer even if it takes a long time. (we
    can always put restrictions on what the BC layer can do) if we have 2 code
    paths, we'd need a contrib module to bridge the two and let contrib
    maintainers test with htmx easily without changing their code

User interface changes

API changes

Data model changes

Release notes snippet

πŸ“Œ Task
Status

Active

Version

11.0 πŸ”₯

Component
AjaxΒ  β†’

Last updated about 1 hour ago

Created by

πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @fathershawn
  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York
  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York
  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

    Three initial thoughts:

    1. Content replacement is probably the simplest case and so one we should include in this POC
    2. I have a proof of concept for JS/CSS attachment in the HTMX contrib module.
    3. What if we plan to replace the #ajax render array key with an #htmx key? If we did that, then our BC strategy is writing a converter that rewrites the render array to equivalent values
  • πŸ‡¬πŸ‡§United Kingdom catch

    Content replacement with css/js would be a good proof of concept I think especially if that's already partially implement in the contrib module. If the new markup comes with CSS and JavaScript that's a fairly small but also complex use-case to cover.

  • πŸ‡«πŸ‡·France nod_ Lille

    Agreed with #5 about scope. For the poc a new #htmx key sounds fine but we'll need to make existing code work as-is as much as possible down the line

  • πŸ‡«πŸ‡·France fgm Paris, France

    I would think it saner (safer ?) to introduce such a #htmx key instead of overloading the meaning of #ajax.

    Then new code could opt in to the new mechanism, and the two mechanisms could coexist with the older one being deprecated and removed over time without having to maintain a compatibility layer. That seems more sustainable.

    We would probably just need to error when both keys are present on an element.

  • πŸ‡³πŸ‡ΏNew Zealand quietone

    Fix formatting in IS

  • πŸ‡ΊπŸ‡ΈUnited States cosmicdreams Minneapolis/St. Paul

    From reading through this issue it sounds to me that the Next steps here are:

    Next Steps

    1. Figure out how to add an '#html' property to fields
    2. Use that key to demonstrate how we could implement existing AJAX commands with HTMX
    3. Brainstorm a proof of concept scenario that would use those altered commands

    Do I have that right?

  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

    I think those steps are close to what I'm thinking about. I would adjust it like so:

    1. Determine where and how to process an #htmx render array property.
    2. Agree on a couple of use cases
    3. Design a processor that transforms #ajax render array key-value pairs into #htmx render array key-value pairs. This is our BC layer.

    For use cases I think we may have agreed on

    Content replacement using #wrapper

    I would suggest one of the commands that are similar, such as append, for the second.

  • πŸ‡«πŸ‡·France fgm Paris, France

    Makes sense, especially worrying about the BC layer only if the first steps succeed. Because it is a "nice to have", IMHO, not a "must".

  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

    I've been simmering ideas on my "back burner" and want to get some feedback before making something.

    HTMX expresses itself in HTML attributes. What if we extended \Drupal\Core\Template\Attribute to create an HtmxAttribute that has class methods for the various attributes used by HTMX? That would allow us to support something like this:

    $htmx = new HtmxAttributes();
    
    $htmx->get(Url::fromRoute(
        route_name: 'htmx.htmx_entity_view',
        route_parameters: ['entityType' => 'node', 'entity' => 27, 'viewMode' => 'teaser'])->toString()
      )
      ->select('article')
      ->target('main')
      ->swap('beforeEnd');
    
    $build = [
      '#htmx' => $htmx,
    ];
    

    Each class method would call \Drupal\Core\Template\Attribute::createAttributeValue and create the correct attribute. Our process function callback for the render process then just needs to merge the `HtmxAttribute` object with any existing Attribute object and set the result as the element's Attribute.

  • πŸ‡ͺπŸ‡¨Ecuador jwilson3

    I love the idea of interoperability with Attributes, your approach sounds like a reasonable architecture. But this makes me curious about the Twig template side, we have the create_attribute() function to build out attributes from scratch, or to completely override the {{ attributes }} variable passed in from the backend when Drupal provides too much extra stuff for a given template's needs (contextual links, data attributes, etc). It's not clear to me yet how this could be useful in Twig so maybe something to just note for future as needs arise, but thinking along the lines of helpers like attributes.addClass(), .removeClass(), etc.

    Would the Attributes integration be syntactic sugar for the PHP side only, or exposed to manipulation via attributes variable in Twig as well?

  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

    Good questions! I'm working backwards from where we need to end up. In the end, we need markup like this

      <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>
    

    So the yes, the Attributes object which arrives in Twig as attributes seems like the natural way to get that. And yes, if we merge HtmxAttribute or if we send it directly, it has all the methods of Attribute either through inheritance or because we've merged into one.

    I did a quick search into how we process and render the #id property and I didn't land on how that happens, but it arrives in Twig in the attribute variable so we end up in the same place right?

  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

    I've had an inquiry about the appropriate markup to use and have moved that to a related issue: 🌱 [policy, no patch] Choose a markup strategy for HTMX POC Fixed Please add your guidance there!

  • πŸ‡«πŸ‡·France fgm Paris, France

    If we go the Attribute route, I suggest we use composition instead of inheritance for future protection. With PHP not supporting multiple inheritance, should the Attribute class get child classes, we would be in a diamond problem situation if we inherit that class. Also, in recent years, Drupal core appears to have started to embrace the trend of final classes/methods/fields. Composing Attribute instead of inheriting from it offers us two advantages:

    • it protects us from that kind of changes
    • it saves us from the temptation of using non-public properties/methods

    I'd even suggest creating an AttributeInterface currently made of the public methods (no public fields) of the Attribute class, and composing that instead of the class itself, for even further protection.

  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

    Thank you @fgm for reminding me that we are working on core! I was surprised there wasn’t an interface but I’m so used to working in contrib, I thought inheritance was what we had. We totally need an interface here.

  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

    I decided to implement the inheritance version of this strategy in the contrib module so that people can easily try it out. The MR is pending with the extended class and a documentation page with some usage examples for that implementation.

    This issue's implementation would use composition, and have a dedicated key, taking care of merging the attributes in processing. That said, feedback welcome over in contrib to refine any of this before we stand it up here in Core.

  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

    It looks to me like the Attribute magic happens in template_preprocess found in core/includes/theme.inc. If I add a #htmx key in a render array, and then add

    if (isset($variables[$key]['#htmx'])) {
      $variables['attributes'] = AttributeHelper::mergeCollections($variables['attributes'], $variables[$key]['#htmx']);
    }
    

    right after the existing #attributes merge, then a breakpoint at the assignment here trips.

  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

    As I look more deeply at implementing the idea from @fgm in #17, it seems to me that the interface should be much smaller than the public methods, otherwise we're just moving everything in Attribute to various traits or something as there is only one protected method and it has the logic for creating values before storage. Many of the other existing public methods implement other Interfaces for Attribute.

    I looked through all the places we currently test for instanceof Attribute and I propose these methods on the interface:

    • ::hasAttribute
    • ::toArray
    • ::merge

    Because in templates we want the full suite of Attribute methods, I'd leave these instanceof conditionals as they are in AttributeHelper

        // If at least one collections is an Attribute object, merge through
        // Attribute::merge.
        $merge_a = $a instanceof Attribute ? $a : new Attribute($a);
        $merge_b = $b instanceof Attribute ? $b : new Attribute($b);
        $merge_a->merge($merge_b);
       
        return $a instanceof Attribute ? $merge_a : $merge_a->toArray();
    
  • πŸ‡«πŸ‡·France fgm Paris, France

    Makes sense. Smallest interfaces are the most useful ones, since you can always extend interfaces but not reduce them.

  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

    Well, #20 πŸ“Œ [POC] Implementing some components of the Ajax system using HTMX Active is not the right solution for where
    to merge in '#htmx' because of the nature of template_preprocess_HOOK such as this in form.inc. Probably why existing ajax is doing it's work at the element pre-process level.

    /**
     * Prepares variables for select element templates.
     *
     * Default template: select.html.twig.
     *
     * It is possible to group options together; to do this, change the format of
     * the #options property to an associative array in which the keys are group
     * labels, and the values are associative arrays in the normal #options format.
     *
     * @param $variables
     *   An associative array containing:
     *   - element: An associative array containing the properties of the element.
     *     Properties used: #title, #value, #options, #description, #extra,
     *     #multiple, #required, #name, #attributes, #size, #sort_options,
     *     #sort_start.
     */
    function template_preprocess_select(&$variables) {
      $element = $variables['element'];
      Element::setAttributes($element, ['id', 'name', 'size']);
      RenderElementBase::setAttributes($element, ['form-select']);
    
      $variables['attributes'] = $element['#attributes'];
      $variables['options'] = form_select_options($element);
    }
    
  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

    Some additional adjustment to FormBuilder is needed, and I could use guidance as to where. The POC is on admin/config/development/configuration/single/export

    Here's the problem sequence:

    1. Load the form
    2. Change the type
    3. Select a name
    4. Change the type

    At this point the form class is called to build the form, but this but doesn't run:
    FormBuilder.php:1294

          // Detect if the element triggered the submission via Ajax.
          if ($this->elementTriggeredScriptedSubmission($element, $form_state)) {
            $form_state->setTriggeringElement($element);
          }
    

    so the name is not reset.
    Then the code above is executed but the form is not rebuilt.

    I feel like we therefore either need to set a rebuild if our request is htmx but I'm not sure the correct place to do that in form builder.

    Here is a diff with 11.x of the current state of the work.

  • πŸ‡¬πŸ‡§United Kingdom longwave UK

    A blog post noting some rough edges in HTMX: https://chrisdone.com/posts/htmx-critique/ - none of these may be that problematic for us but something to bear in mind.

  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

    Critical thinking is a valuable means of understanding something new. I recognize that the author of the critique works regularly with HTMX and I'll watch for some of the edge cases called out, but I wonder if there are some a priori assumptions from working in single-page-applications when I read

    An attractive future direction might be to re-implement Htmx in React

  • πŸ‡¬πŸ‡§United Kingdom AndyF

    Just for additional context, I think the author of htmx responded at https://news.ycombinator.com/item?id=41782080.

  • πŸ‡¨πŸ‡¦Canada ambient.impact Toronto

    Not to dunk on the author of that even more, but I'm in agreement with @fathershawn, and would also highlight:

    An attractive future direction might be to re-implement Htmx in React:

    • The server sends a JSON blob that React converts into virtual DOM
      components.

    ...which baffles me because it sounds like this person doesn't understand the core philosophy behind htmx and similar approaches. Sending a JSON blob for React to convert into a virtual DOM has nothing to do with htmx, and it's in fact entirely antithetical to htmx as I understand it.

  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

    I have a working POC on the Single Config Export form: https://git.drupalcode.org/issue/drupal-3446642/-/compare/11.x...3446642...

    Feedback invited!

  • πŸ‡ΊπŸ‡ΈUnited States nicxvan

    This looks really interesting!
    I'm looking at this from the perspective of someone unfamiliar with htmx, this will not be a technical review.
    I was mostly interested in seeing how it looked on the front end.

    It seems like it's more attributes on html. I don't know if it's clearer or not, but what I'm reading is that it's well documented and adopted outside of Drupal.

    It worked for a bit even though I see console errors for Uncaught ReferenceError: oobSwapEvent is not defined.
    Eventually the htmx events stopped firing, but this is just a POC so that is likely not super helpful feedback.

    Another thing I noticed, is it is verbose. I see an hx-target attribute in the docs, but it's data-hx-target.
    It's not a huge thing, but an already verbose element is even more verbose: is data required to be on all of them.

    <select
      data-drupal-selector="edit-config-name"
      data-hx-post="/admin/config/development/configuration/single/export"
      data-hx-select="textarea[data-drupal-selector=&quot;edit-export&quot;]"
      data-hx-target="textarea[data-drupal-selector=&quot;edit-export&quot;]"
      data-hx-swap="outerHTML  ignoreTitle:true"
      id="edit-config-name"
      name="config_name"
      class="form-select form-element form-element--type-select"
    >
    
  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

    Thanks for the extra eyes @nicxvan! I missed a variable change moving some JS over from the HTMX module β†’ . Updated the branch.

    The markup format is discussed and decided in 🌱 [policy, no patch] Choose a markup strategy for HTMX POC Fixed

  • πŸ‡ΊπŸ‡ΈUnited States fathershawn New York

    Added support for HTMX Response Headers which allowed me to implement a missing feature in ConfigSingleExportForm. The route and the form are built to take parameters for configuration type and name. However the implementation via core ajax does not update the url.

    This htmx implementation now does so that the result can be captured via a link.

Production build 0.71.5 2024