- Issue created by @fathershawn
- πΊπΈUnited States fathershawn New York
Three initial thoughts:
- Content replacement is probably the simplest case and so one we should include in this POC
- I have a proof of concept for JS/CSS attachment in the HTMX contrib module.
- 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.
- πΊπΈUnited States cosmicdreams Minneapolis/St. Paul
From reading through this issue it sounds to me that the Next steps here are:
Next Steps
- Figure out how to add an '#html' property to fields
- Use that key to demonstrate how we could implement existing AJAX commands with HTMX
- 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:
- Determine where and how to process an
#htmx
render array property. - Agree on a couple of use cases
- 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.
- Determine where and how to process an
- π«π·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 anHtmxAttribute
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 likeattributes.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 asattributes
seems like the natural way to get that. And yes, if we mergeHtmxAttribute
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 theAttribute
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 offinal
classes/methods/fields. ComposingAttribute
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 thepublic
methods (no public fields) of theAttribute
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 incore/includes/theme.inc
. If I add a#htmx
key in a render array, and then addif (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 forAttribute
.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 theseinstanceof
conditionals as they are inAttributeHelper
// 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
Here's an initial draft of the attribute changes: https://git.drupalcode.org/issue/drupal-3446642/-/compare/11.x...3446642-poc-implementing-htmx
- πΊπΈ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 oftemplate_preprocess_HOOK
such as this inform.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 onadmin/config/development/configuration/single/export
Here's the problem sequence:
- Load the form
- Change the type
- Select a name
- 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.
- π¬π§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.
- The server sends a JSON blob that React converts into virtual DOM
- πΊπΈ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="edit-export"]" data-hx-target="textarea[data-drupal-selector="edit-export"]" 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.