Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
W1M0R opened this issue Sep 2, 2024 · 7 comments
Open

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

W1M0R opened this issue Sep 2, 2024 · 7 comments

Comments

@W1M0R
Copy link

W1M0R commented Sep 2, 2024

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
Copy link
Contributor

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
Copy link
Author

W1M0R commented Sep 3, 2024

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
Copy link
Author

W1M0R commented Sep 3, 2024

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:

htmx/src/htmx.js

Lines 2593 to 2596 in 7fc1d61

} else if (triggerSpec.trigger === 'load') {
if (!maybeFilterEvent(triggerSpec, elt, makeEvent('load', { elt }))) {
loadImmediately(asElement(elt), handler, nodeData, triggerSpec.delay)
}

@W1M0R W1M0R changed the title Feature request: hx-init attribute Feature request: hx-init attribute or simpler equivalent Sep 3, 2024
@MichaelWest22
Copy link
Contributor

MichaelWest22 commented Sep 4, 2024

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
Copy link
Contributor

MichaelWest22 commented Sep 4, 2024

    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
Copy link
Author

W1M0R commented Sep 4, 2024

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

@maddalax
Copy link
Contributor

This is how I ended up solving it for https://github.com/maddalax/htmgo

htmx.defineExtension("htmgo", {
    onEvent: function (name, evt) {
       if(name === "htmx:load" && evt.target) {
          invokeOnLoad(evt.target as HTMLElement);
       }
    },
});

/**
 * Browser doesn't support onload for all elements, so we need to manually trigger it
 * this is useful for locality of behavior
 */
function invokeOnLoad(element : Element) {
    if(element == null || !(element instanceof HTMLElement)) {
        return
    }
    const ignored = ['SCRIPT', 'LINK', 'STYLE', 'META', 'BASE', 'TITLE', 'HEAD', 'HTML', 'BODY'];
    if(!ignored.includes(element.tagName)) {
        if(element.hasAttribute("onload")) {
            element.onload!(new Event("load"));
        }
    }
    // check its children
    element.querySelectorAll('[onload]').forEach(invokeOnLoad)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants