From 0807782cc7fee42b93a092b04c8c31f50fb8bef2 Mon Sep 17 00:00:00 2001 From: MichaelWest22 Date: Sun, 9 Aug 2026 02:12:24 +1200 Subject: [PATCH] expose initialize function for async and streaming use cases --- src/htmx.d.ts | 7 ++++ src/htmx.js | 38 +++++++++---------- test/tests/unit/bootstrap.js | 1 + test/tests/unit/process.js | 33 ++++++++++++++++ .../05-methods/10-htmx-initialize.md | 38 +++++++++++++++++++ 5 files changed, 97 insertions(+), 20 deletions(-) create mode 100644 www/src/content/reference/05-methods/10-htmx-initialize.md diff --git a/src/htmx.d.ts b/src/htmx.d.ts index b2edd342d..5399f9f6d 100644 --- a/src/htmx.d.ts +++ b/src/htmx.d.ts @@ -612,6 +612,13 @@ export interface Htmx { * Equivalent to listening for `htmx:after:process`. */ onLoad(callback: (elt: Element) => void): void; + /** + * Sets up history handling and processes `document.body`. + * Called automatically on `DOMContentLoaded` (or next tick if the document is already loaded). + * Safe to call multiple times — history listeners are only registered once. + * Call manually when loading htmx asynchronously or after a streaming response delivers the full page. + */ + initialize(): void; /** * Initialize htmx attributes on `root` and all its descendants. * When `force` is `true`, tears down and re-initializes already-processed elements — diff --git a/src/htmx.js b/src/htmx.js index ce6f83bfa..6c299c67c 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -157,6 +157,7 @@ var htmx = (() => { #hxOnQuery #transitionQueue #historyAbort + #historyInitialized #processingTransition constructor() { @@ -185,14 +186,10 @@ var htmx = (() => { triggerHtmxEvent: this.__trigger.bind(this), executeJavaScript: this.__executeJavaScript.bind(this) }; - let init = () => { - this.__initHistoryHandling() - this.process(document.body) - }; + let init = () => this.initialize(); if (document.readyState === 'loading') { document.addEventListener("DOMContentLoaded", init) } else { - // wait a tick so extensions can register setTimeout(init) } } @@ -1256,6 +1253,22 @@ var htmx = (() => { // Public JS API //============================================================================================ + initialize() { + if (this.config.history && !this.#historyInitialized) { + this.#historyInitialized = true; + if (!history.state) history.replaceState({htmx: true}, '', location.href); + if (window.navigation && !/firefox/i.test(navigator.userAgent)) { + navigation.addEventListener('navigate', (event) => { + if (event.navigationType === 'traverse' && event.canIntercept && !event.hashChange) + event.intercept({handler: () => this.__restoreHistory()}); + }); + } else { + window.addEventListener('popstate', (event) => this.__restoreHistory(event.state)); + } + } + this.process(document.body); + } + async swap(ctx) { try { this.__handleHistoryUpdate(ctx); @@ -1617,21 +1630,6 @@ var htmx = (() => { // History Support //============================================================================================ - __initHistoryHandling() { - if (!this.config.history) return; - if (!history.state) { - history.replaceState({htmx: true}, '', location.href); - } - if (window.navigation && !/firefox/i.test(navigator.userAgent)) { - navigation.addEventListener('navigate', (event) => { - if (event.navigationType === 'traverse' && event.canIntercept && !event.hashChange) - event.intercept({handler: () => this.__restoreHistory()}); - }); - } else { - window.addEventListener('popstate', (event) => this.__restoreHistory(event.state)); - } - } - __pushUrlIntoHistory(path) { if (!this.config.history) return; if (!history.state) history.replaceState({htmx: true}, '', location.href); diff --git a/test/tests/unit/bootstrap.js b/test/tests/unit/bootstrap.js index e9ec5e892..6d5dce8ee 100644 --- a/test/tests/unit/bootstrap.js +++ b/test/tests/unit/bootstrap.js @@ -192,6 +192,7 @@ describe('bootstrap unit tests', function() { 'ajax', 'find', 'findAll', + 'initialize', 'on', 'onLoad', 'parseInterval', diff --git a/test/tests/unit/process.js b/test/tests/unit/process.js index 287425a85..6eddb50be 100644 --- a/test/tests/unit/process.js +++ b/test/tests/unit/process.js @@ -189,4 +189,37 @@ describe('process() unit tests', function() { assert.equal(counts['c'], 1, 'child cleaned once') }) +}); + +describe('initialize() unit tests', function() { + + beforeEach(function() { + setupTest(); + }); + + afterEach(function() { + cleanupTest(); + }); + + it('initialize() processes document.body', function() { + let btn = createProcessedHTML('') + btn.removeAttribute('data-htmx-powered') + delete btn._htmx + htmx.initialize() + assert.isTrue(btn.hasAttribute('data-htmx-powered')) + }) + + it('initialize() is idempotent - calling twice does not double-register history listeners', function() { + let count = 0 + let orig = window.addEventListener + window.addEventListener = function(type, ...args) { + if (type === 'popstate') count++ + return orig.call(this, type, ...args) + } + htmx.initialize() + htmx.initialize() + window.addEventListener = orig + assert.isAtMost(count, 1, 'popstate listener registered more than once') + }) + }); \ No newline at end of file diff --git a/www/src/content/reference/05-methods/10-htmx-initialize.md b/www/src/content/reference/05-methods/10-htmx-initialize.md new file mode 100644 index 000000000..a0127f76f --- /dev/null +++ b/www/src/content/reference/05-methods/10-htmx-initialize.md @@ -0,0 +1,38 @@ +--- +title: "htmx.initialize()" +description: "Manually initializes htmx" +--- + +The `htmx.initialize()` function sets up htmx history handling and processes `document.body`. It is called automatically on `DOMContentLoaded` (or on the next tick if the document is already loaded), but can be called manually when htmx is loaded asynchronously or after a streaming response has delivered the full page. + +## Syntax + +```javascript +htmx.initialize() +``` + +## Parameters + +None. + +## Usage + +Call after loading htmx asynchronously to ensure history handling and element initialization run at the right time: + +```javascript +import('https://cdn.jsdelivr.net/npm/htmx.org/dist/htmx.min.js').then(() => { + htmx.initialize(); +}); +``` + +Call from a streaming extension after the full page has arrived: + +```javascript +// inside an hx-streaming extension handler +htmx.initialize(); +``` + +## Notes + +* Safe to call multiple times — history listeners are only registered once +* Equivalent to setting up history handling then calling [`htmx.process(document.body)`](/reference/methods/htmx-process)