bigskysoftware / htmx

</> htmx - high power tools for HTML
https://htmx.org
Other
37.31k stars 1.26k forks source link

Feature request: hx-init attribute or simpler equivalent #2863

Open W1M0R opened 2 weeks ago

W1M0R commented 2 weeks ago

Thank you for providing a refreshing way to create for the web.

I find myself hooking into hyperscript and javascript whenever I need to do some initialisation work on an htmx component.

Hyperscript:

init log 'init'

HTMX JS API:

htmx.onLoad(el, () => {
if (el == htmx.find('#my-comp')) {
  console.log('init', el)
}
})

I imagine hx-on could also be used, although I haven't tested it):

<div  hx-on:htmx:load="if (event.detail.elt == this) console.log('init it')"

It would be nice to have something similar for htmx, to avoid the need for hyperscript or javascript for that simple use case:

<div hx-init="console.log('init', this, event)" />
MichaelWest22 commented 1 week ago

Seems that hx-on::load already does what you want fine for the simple case. Note that :htmx: can be shortened to :: making it only a few characters more than "hx-init"

<div hx-on::load="console.log('hi')"></div>

There are already many ways supported to handle load events in htmx like the htmx.onLoad() and hx-on and you can also try adding a simple event listener to the body so you don't have to define the load event on every single element.

document.body.addEventListener('htmx:load', function(evt) {
  if(evt.detail.elt.tagName == 'TABLE') {
    // do custom table init code here
  }
});

One thing to be aware of with htmx:load events is they fire once on the body when you do the first full page load and also on every parent element swapped in via a htmx ajax request. But you do not get a load event for any children loaded in during initial page load or any children elements inside htmx partial responses. So sometimes when handling load events you may need to also process all the elt's children to see what needs to have custom init code run.

For example here is a global handler that could init all div's (swap in different logic for your use) as they are loaded:

function customInit(elt) {
  console.log('do custom init here')
}
document.body.addEventListener('htmx:load', function(evt) {
  if(evt.detail.elt.tagName == 'DIV') { 
    customInit(evt.detail.elt)  // check item itself need init
  }
  for(const el of evt.detail.elt.querySelectorAll("div")) {
    customInit(el)  // check if any children need init
  }
});

So if you were going to implement a proper "hx-init" attribute in htmx then it would be great if it was able to do the querySelectorAll part above for you and run reliably on all initial page loaded elements and all children of partial ajax responses. I think this could be done with a new htmx extension if you didn't want to have to write custom event listeners as above.

W1M0R commented 1 week ago

Thanks @MichaelWest22. Your example highlights some of my own observations very well. Maybe an extension could improve the situation. Looking at some of the htmx debug messages, maybe I can get something workable using: https://htmx.org/events/#htmx:afterProcessNode

W1M0R commented 1 week ago

I couldn't identify a simple enough htmx attribute or event that could satisfy this request.

For the time being, it looks like there is no equivalent capability in htmx (in terms of simplicity, safety, clarity, maintainability and LoB), when compared to the following initialisation methods offered by other supporting libraries:

hyperscript

  <span _="init log 'hyperscript'"></span>

alpine

<span x-data x-init="console.log('alpine')"></span>

surreal

<span>
  <script>
    console.log("surreal");
  </script>
</span>

htmx (non-init)

<span hx-on:htmx:load="console.log('htmx - triggers for all htmx content or not at all')"></span>

htmx (HX-Trigger)

<span
      hx-get="/init-my-component"
      hx-swap="none"
      hx-trigger="load"
      hx-on:my-component-init-event="console.log('htmx - server returns HX-Trigger my-component-init-event')"
></span>

The hx-trigger="load" seems to execute as one would expect for initialisation. It looks like this method is then not the same as hx-on:htmx:load (which executes sometimes or not at all according to its own rules).

For the hx-trigger method to work without requiring server-side co-op, it would be helpful if the hx-trigger could execute a custom event on load instead of executing the hx-get. For example:

<span
      hx-trigger="load trigger:my-init"
      hx-on:my-init="console.log('body init (htmx)"
></span>

Here is the code handling the hx-trigger version of load:

https://github.com/bigskysoftware/htmx/blob/7fc1d61b4fdbca486263eda79c3f31feb10af783/src/htmx.js#L2593-L2596

MichaelWest22 commented 1 week ago

Yeah I can see several ways to create a hx-init extension:

  1. Get the extension to call internalAPI.triggerEvent() so that it can emit a new htmx:init event on any elements found in the htmx:load event that have hx-init="true" attribute on them. You then can write simple event listener in JS as needed.
  2. Get the extension to find all elements with hx-init attribute like hx-init="console.log('init')" and use Function() eval to execute this attribute string with the elt variable passed in. (This will break on strict CSP's as EVAL is problematic)
  3. Create a callback function config item and get the extension to find all elements with hx-init attribute and call the named callback function passing in elt. You would then need to declare callback functions below the extension definition for each kind of init you need and assign them manually to the config item for them to execute.
MichaelWest22 commented 1 week ago
    let intAPI
    htmx.defineExtension('hx-init', {
      init: function(apiRef) {
        intAPI = apiRef
      },

      onEvent: function(name, evt) {
        if (name === 'htmx:load') {
          if(evt.detail.elt.getAttribute('hx-init') !== null) { 
            intAPI.triggerEvent(evt.detail.elt,'htmx:init')
          }
          for(const el of evt.detail.elt.querySelectorAll("[hx-init]")) {
            intAPI.triggerEvent(el,'htmx:init') 
          }
        }
      }
    })

For example here is option 1 which seems to work well. just set hx-ext="hx-init" attribute on the page body and add hx-init="true" to all elements to trigger init on and then write a htmx:init eventListener

W1M0R commented 1 week ago

@MichaelWest22 I like option 1. Thanks for the reference implementation.