Compare commits

...

670 Commits

Author SHA1 Message Date
Carson Gross
580549355a fix date 2026-01-20 10:20:30 -07:00
Carson Gross
709512c1ac formatting & a bit of editorial work 2026-01-20 10:19:48 -07:00
Carson Gross
5a374d546b formatting 2026-01-20 10:13:01 -07:00
Carson Gross
1a30b9130e formatting 2026-01-20 10:12:15 -07:00
Carson Gross
ad65bc77ce formatting 2026-01-20 10:11:05 -07:00
Carson Gross
381449089d remove double title 2026-01-20 10:08:00 -07:00
Carson Gross
2e229462e3 add 2024 olympics to the essays page 2026-01-20 10:06:48 -07:00
Rodolphe Trujillo
6b214f11e7 Add essay: Building Critical Infrastructure with htmx for Paris 2024 Olympics (#3627)
* Add essay: Building Critical Infrastructure with htmx for Paris 2024 Olympics

* Refine essay: clarify wording, add note on Tour de France 2025 reuse

* add comma and "the"

---------

Co-authored-by: Rodolphe Trujillo <rodolphe.trujillo@arolo-solutions.com>
2026-01-20 10:04:51 -07:00
Carson Gross
58dc1e247d add sponsor 2026-01-19 15:20:13 -07:00
Alexander Petros
749d5f2f4c Fix REST links (#3611) 2025-12-31 16:53:41 -07:00
Carson Gross
fcfca903af Merge remote-tracking branch 'origin/master' 2025-12-24 12:46:27 -07:00
Carson Gross
563fff67db add sponsor 2025-12-24 12:46:19 -07:00
Alexander van Saase
9c1297c5f3 website: add Askama to the list of template engines that support template fragments (#3576)
Some checks failed
Node CI / test_suite (push) Has been cancelled
Add Askama to the list of template engines that support fragments
2025-12-11 11:04:26 -07:00
Loren Stewart
e495b68dc3 Add optimistic extension to extensions index (#3474) 2025-11-16 07:37:32 -07:00
Carson Gross
3abaf7eb3f Merge remote-tracking branch 'origin/master' 2025-11-10 12:26:21 -07:00
Carson Gross
e9f2ee94e3 update the-fetchening.md 2025-11-08 20:01:08 -07:00
raven.so.900
bced397c28 Add Nomini to htmx alternatives (#3497)
Add Nomini to alternatives.md
2025-11-08 16:19:33 -07:00
Carson Gross
7a0086fceb improve 2025-11-03 12:49:22 -07:00
Carson Gross
2a1339287e typo 2025-11-03 12:48:52 -07:00
Carson Gross
a275707f4a typo 2025-11-03 12:48:27 -07:00
Carson Gross
f1e0b926d8 typo 2025-11-03 12:42:58 -07:00
Carson Gross
e71f746bad typo 2025-11-03 12:21:19 -07:00
Carson Gross
8b249b1544 correct tag chars 2025-11-03 12:09:23 -07:00
Carson Gross
b7f833b6d5 add article on the fetch()ening 2025-11-03 11:50:52 -07:00
Carson Gross
20f97a68ae fix the title fix 2025-10-24 21:07:28 -06:00
Carson Gross
5bfeb977d4 fix title 2025-10-24 21:07:04 -06:00
Carson Gross
cf6609d454 prep 2.0.8 release 2025-10-24 21:01:07 -06:00
Carson Gross
1e80780963 prep 2.0.8 release
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-10-24 20:52:58 -06:00
Carson Gross
022e53c0a1 prep 2.0.8 release 2025-10-24 20:45:52 -06:00
Carson Gross
41c55c941e prep 2.0.8 release 2025-10-24 20:43:15 -06:00
Carson Gross
49d6aa3752 prep 2.0.8 release 2025-10-24 20:42:29 -06:00
Carson Gross
a6c3323e61 Merge branch 'master' into dev 2025-10-24 20:38:00 -06:00
Simon Hartley
915a240de5 Docs: update extension versions so correct file is served by jsDelivr (#3478)
Docs: update extension versions so the correct file is served by jsDelivr

Co-authored-by: scrhartley <scrhartley@github.com>
2025-10-18 21:48:42 -06:00
Luke Warlow
b9336a96fb Update parseHTML to use Document.parseHTMLUnsafe where supported (#3185)
Fixes #2682
2025-10-17 13:16:28 -06:00
MichaelWest22
83a1449a89 Add pushUrl option to ajax api and improve hx-location push url handling (#3404)
* add pushUrl option

* Remove duplicate save to history

* Improve pushUrl and hx-location url handling

* Add replace option to api as well

* minor wording change

* push headers support true

* roll back anchor support for header base paths except for true case

* add selectOOB and simplify ajax helper

* Remove refactor

* reverse order of push/replace

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-10-17 13:15:43 -06:00
Deniz Akşimşek
deecda151c Add alt text to images in content and examples (#3423) 2025-10-17 13:11:02 -06:00
MichaelWest22
cd045c3e0e fix issue with hx-sync and htmx:abort with shadow DOM (#3424) 2025-10-17 12:50:31 -06:00
MichaelWest22
69ecb9f85d minor spelling fix in htmx.js and api.md (#3476)
minor spelling fix

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-10-17 12:46:53 -06:00
MichaelWest22
04d6c7249b fix stale currentPathForHistory issue (#3451)
* fix stale currentPathForHistory issue

* Update Node Versions and types to fix latest tsc upgrade issues (#3455)

* Update Node Versions and types to fix latest tsc upgrade issues

* fix playwright only test filtering that has broken from playwright browser updates

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>

* fix stale currentPathForHistory issue

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-10-17 12:41:07 -06:00
Jean Raby
148fc95cbc typo: appreach -> approach (#3475) 2025-10-17 16:57:53 +13:00
Mihai
e2faeaf7a9 Fix typo in reference.md (#3469) 2025-10-17 00:26:44 +13:00
Blake Deckard
887524c734 Fix typo in vendoring.md (#3458) 2025-10-17 00:20:34 +13:00
Mike Dalrymple
4e183e6da4 Fix typo: idiomorph.md (#3460)
typo: changes 'being' to 'begin'
2025-10-16 11:03:44 +13:00
Carson Gross
014cd4ee69 clean up sponsorships 2025-10-14 05:29:17 -06:00
MichaelWest22
7de98197f3 Update Node Versions and types to fix latest tsc upgrade issues (#3455)
* Update Node Versions and types to fix latest tsc upgrade issues

* fix playwright only test filtering that has broken from playwright browser updates

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
(cherry picked from commit 56299971ce)
2025-10-14 05:22:05 -06:00
MichaelWest22
56299971ce Update Node Versions and types to fix latest tsc upgrade issues (#3455)
* Update Node Versions and types to fix latest tsc upgrade issues

* fix playwright only test filtering that has broken from playwright browser updates

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-10-14 05:19:20 -06:00
MrPowerGamerBR
0da136f4a5 Add Primer.js to the Hypermedia Research section (#3464)
* Add Primer.js to the Hypermedia Research section

* Include date, move the names to within the parenthesis to make it consistent with the other links
2025-10-08 08:26:15 -06:00
Carson Gross
2d0c0f1b56 add essay to "On The Other Hand..."
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-09-23 07:21:39 -06:00
Carson Gross
81a6e25fb9 update htmx sha
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-09-09 15:34:48 -06:00
Carson Gross
2b83121f27 update package lock
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-09-08 11:23:43 -06:00
Carson Gross
3b85139c61 changelog
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-09-08 10:30:07 -06:00
Carson Gross
96d361d440 Merge branch 'master' into dev 2025-09-07 12:15:00 -06:00
Carson Gross
ccdce87ec3 Merge branch 'master' into dev
# Conflicts:
#	www/content/docs.md
2025-09-07 12:14:25 -06:00
Carson Gross
449c8e9531 prep next release
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-09-07 12:13:49 -06:00
MichaelWest22
cee310e4d5 Handle not preventing link when inside htmx enabled element (#3396)
* Handle not preventing link when inside htmx enabled element

* Simplify shouldCancel and pass in eltToListenOn to solve from: issue without regressions

* move regex to local variable format
2025-09-07 12:09:05 -06:00
letianpailove
d5de7d4a03 Fix minor typos and grammar in documentation (#3400)
Some checks failed
Node CI / test_suite (push) Has been cancelled
This PR fixes two minor issues in the documentation:

Double negation typo

Original: hx-boost does not not update the <html> or <body> tags...

Fixed: hx-boost does not update the <html> or <body> tags...

Explanation: Removed a duplicated "not".

Grammar and consistency in terminology

Original: The MDN Article provide a good jumping off point...

Fixed: The MDN Article provides a good jumping-off point...

Explanation:

Corrected subject-verb agreement (article → provides).

Added hyphens in "jumping-off point" as a compound adjective.
2025-09-02 17:52:57 -06:00
Yami Odymel
458ae04d17 Update hx-swap-oob.md typo: excapsulate -> encapsulate, Fixed #3406 (#3407) 2025-09-02 17:52:29 -06:00
Carson Gross
e4f8fe9a77 Merge remote-tracking branch 'origin/master'
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-08-02 08:24:21 -06:00
Carson Gross
d7507ddf2c add trackity logo 2025-08-02 07:48:41 -06:00
MichaelWest22
448db781ef add back null check for no form in getRelatedFormData (#3394)
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-07-27 09:34:50 -06:00
Yawar Amin
d818268c4c Update CSRF recommendation (#3383)
Some checks failed
Node CI / test_suite (push) Has been cancelled
Adjust CSRF recommendation
2025-07-21 22:43:36 -04:00
Alexander Petros
2289e3176e Add note about the loading mechanism to quirks page (#3387) 2025-07-21 22:42:49 -04:00
MichaelWest22
081adf8eeb Fix ajax api body test (#3073)
Some checks failed
Node CI / test_suite (push) Has been cancelled
fix api body test to not replace body and just check target
2025-07-21 21:42:30 -04:00
Adrian Hesketh
7ae66f9b33 docs(essays): add templ to list of template languages that support fragments (#3382)
Some checks failed
Node CI / test_suite (push) Has been cancelled
docs: add templ to template languages that support fragments
2025-07-20 15:16:32 -04:00
gastendonk
29363deb1a Update click-to-edit.md (#3172)
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-07-18 11:32:48 -06:00
Baraa Al-Masri
7619551349 Add DankMuzikk and DankLyrics to webring. (#3378)
Add DankMuzikk and DankLyrics to webring
2025-07-18 11:31:45 -06:00
Baraa Al-Masri
90f94753ec Add DankTodo to webring. (#3375)
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-07-17 14:37:13 -05:00
MichaelWest22
9d598f8d6e implement reportValidity for reporting proper form validation errors behind config flag (#3372)
Some checks failed
Node CI / test_suite (push) Has been cancelled
* implement reportvalidity behind feature flag

* add tests
2025-07-14 19:04:17 -06:00
Carson Gross
dc804932a7 fix formatting 2025-07-14 18:21:16 -06:00
Vincent
03c3af724c Fix google search syntax from search bar (#3209)
Some checks failed
Node CI / test_suite (push) Has been cancelled
Co-authored-by: 1cg <469183+1cg@users.noreply.github.com>
2025-07-14 18:18:39 -06:00
Jackie Li
93cafa50f8 docs: add module import instructions for idiomorph extension (#3350)
* docs: add module import instructions for idiomorph extension

Fixes #3349

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* use the right import path

* Restore example to make it consistent in the examples

* fix manual bundle instructions too

* remove unnecessary script tag

* remove unminified version as well since it's not needed for htmx integration

* clear instruction on using min version with htmx

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-14 18:17:16 -06:00
Thibaud Colas
ec886c81c1 Fix invisible button text for hx-indicator in dark mode (#3353)
Without this, in dark mode the [hx-indicator](https://htmx.org/attributes/hx-indicator/) demo button has white text on white background. Switching to `Canvas` means the button background will be black in dark mode.

This is a similar fix to #2719, though we can’t use `primary` here as it would make the indicator "bars.svg" image almost invisible.
2025-07-14 18:16:37 -06:00
Simon Hartley
9c5a646395 Remove redundant script tags (#3358)
* Remove redundant script tags

* Remove redundant script tags

---------

Co-authored-by: scrhartley <scrhartley@github.com>
2025-07-14 18:14:36 -06:00
Dave Lewis
85e499d174 Update Rust server examples with actix-htmx (#3359)
Update Rust server examples

Add link to actix-htmx
2025-07-14 18:13:52 -06:00
Marius Gundersen
bb4d877330 Added extension (#3364)
Added link to htmx-json extension

https://github.com/mariusGundersen/htmx-json
2025-07-14 18:13:39 -06:00
flamendless
91f4472548 Fix typo (#3371) 2025-07-14 18:13:12 -06:00
Thomas Ricci
9a686ea2de Add Thomas Ricci's website to webring (#2231)
Add a HTMX CEO's website

It seems like personal sites aren't allowed as per #2186, however it was closed because it was "Not exactly an htmx showcase." so I'm not exactly sure if it's that personal sites aren't allowed, or it's that the site didn't showcase HTMX.

I'd argue my site is webring-worthy because it relies nearly completely on HTMX and hyperscript, shows that HTMX and hyperscript can create very [fast](https://pagespeed.web.dev/analysis/https-thomasricci-dev/6tey5ptzsx?form_factor=desktop) websites, and has had (some) popularity w/ [HTMX on twitter](https://twitter.com/RudRecciah/status/1749007532734235134).

I'd love for it to be added, but if that's not an option that's okay.

Co-authored-by: 1cg <469183+1cg@users.noreply.github.com>
2025-07-14 18:12:10 -06:00
MichaelWest22
28fae544c2 Cancel button with inner content form submit properly (#3368)
Co-authored-by: 1cg <469183+1cg@users.noreply.github.com>
2025-07-14 17:41:26 -06:00
MichaelWest22
032972be35 update indicator style to have visibility:hidden for screen readers (#3361)
* update indicator style to have visibility:hidden for screen readers

* spelling
2025-07-14 17:40:09 -06:00
Viktor Szépe
9fb3c0e492 Fix variable names (#3344)
* Fix variable names

* Fix one more variable name
2025-07-14 17:38:39 -06:00
Matteo Smaila
b0c87bf363 Bugfix: swap="outerHTML" on <div> with style attribute leaves htmx-swapping class behind (#3341)
* Added regression test for swap=outerHTML unexpected behavior, checked it failes, implemented initial fix in htmx.js that makes (all) test(s) run and pass.

* Renamed variable in my regression test to be more clear.

* I noticed I wasn't using the copies of the attributes I introduced.Tests were passing and I know why, though. This means I miss one more regression test for the bug in cloneAttributes.

* Added one more regression test for the fix in cloneAttributes.

* Made preservation of htmx- prefixed classes more robust in cloneAttributes after I noted they could as well be removed by mergeTo.setAttribute in the second forEach loop.

* Started as a typo-fix, ended up renaming regression tests to be more explicit.

* Started as a typo-fix, ended up renaming regression tests to be more explicit.

* Removed space that I accidentally added before.

* Applied changes as requested by MichaelWest22.

---------

Co-authored-by: Matteo Smaila <matteo.smaila@314softwaresolutions.com>
Co-authored-by: 1cg <469183+1cg@users.noreply.github.com>
2025-07-14 17:37:40 -06:00
Daniel J. Summers
d91c8820f9 Remove extra "2" from unminified v2.0.6 SRI hash (#3362)
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-07-03 12:58:44 +12:00
Carson Gross
7529444e86 fix shas
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-06-27 07:46:10 -06:00
Carson Gross
a440c6d4f4 Merge branch 'master' into dev
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-06-27 07:43:31 -06:00
Carson Gross
1b3e78c331 fix package-lock.json 2025-06-27 07:41:58 -06:00
Carson Gross
599d152c48 prep 2.0.6 release 2025-06-27 07:40:21 -06:00
Carson Gross
fe7f103eab prep 2.0.6 release 2025-06-27 07:39:35 -06:00
MichaelWest22
8e489ef6ee fix click events on elements wrapped by link don't cancel link navigation (#3357)
fix click events on elements wrapped by link doesn't cancel link navigation
2025-06-27 07:33:28 -06:00
Carson Gross
17a7dd1fc4 Merge remote-tracking branch 'origin/master'
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-06-25 15:15:58 -06:00
Carson Gross
17f417c923 remove 2.0 announcement 2025-06-25 15:15:48 -06:00
surfskidude
5df061fb65 Adding Lua Server Pages server example (#3347) 2025-06-25 10:17:24 -06:00
Carson Gross
c838cfb7a6 update docs
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-06-20 15:46:42 -06:00
Carson Gross
726292af1d fix SHAs
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-06-20 15:31:36 -06:00
Carson Gross
683c0e8ae2 prep 2.0.5 release 2025-06-20 15:29:01 -06:00
Carson Gross
4f95de2e58 prep 2.0.5 release 2025-06-20 15:21:29 -06:00
Simon Hartley
e2353e26bc Allow use of "this" when evaluating hx-vals (#3332)
Some checks failed
Node CI / test_suite (push) Has been cancelled
Evaluate hx-vals with "this" referring to the element it is defined on

Co-authored-by: scrhartley <scrhartley@github.com>
2025-06-20 14:52:17 -06:00
MichaelWest22
dc71b317d0 throw targetError correctly when target invalid during retarget (#3335)
* throw targetError correctly when target invalid during retarget

* fix missing jsdoc return
2025-06-20 14:51:12 -06:00
MichaelWest22
84306ccf3d Fix non chrome view transtions tests (#3338)
Some checks failed
Node CI / test_suite (push) Has been cancelled
fix non chrome view transitions tests
2025-06-19 15:02:11 -06:00
Carson Gross
3f49db3936 document inherit keyword
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-06-19 11:21:49 -06:00
Carson Gross
bb4eb0f813 changelog for 2.0.5 release 2025-06-19 10:40:06 -06:00
Carson Gross
e38d6a7147 update sha 2025-06-19 10:24:57 -06:00
Carson Gross
1d01b94b90 prep v2.0.5 2025-06-19 10:24:09 -06:00
MichaelWest22
c091b95fa3 Move currentPathForHistory to session storage (#3330)
Some checks failed
Node CI / test_suite (push) Has been cancelled
* Move currentPathForHistory to session storage

* update test

* fix more sessionStorage from localStorage
2025-06-16 18:24:39 -05:00
MichaelWest22
c9e2bea954 Fix Modified click trigger on form elements prevent default behaviour (#3336)
Fix Modified click trigger on form elements prevent default behaviour of clicked element
2025-06-16 18:09:32 -05:00
MichaelWest22
7388d0c057 Move History storage to sessionStorage (#3305)
* Move History storage to sessionStorage and history path to window

* Fix type warnings

* Revert currentPathForHistory to move it to its own PR

* fix test
2025-06-16 18:08:34 -05:00
MichaelWest22
508e332544 Standardize history restore functions to use proper htmx swap functions (#3306)
* Improve history support and events

* Improve history event overrides

* Improve history support and events

* Improve history event overrides

* Update Documentation of new event changes

* Add event testing for updated events

* update event doco and rename to historyElt to be consistent

* Improve history support and events

* Improve history event overrides

* Update Documentation of new event changes

* Add event testing for updated events

* update event doco and rename to historyElt to be consistent

* Fix loc coverage test coverage

* Standardize history restore functions to use proper htmx swap functions

* Add test for hx-history-elt attribute

* Fix broken merge conflict resolution
2025-06-16 16:53:57 -06:00
Simon Hartley
3c1ac71573 Follow up to #3234 (#3334)
* Follow up to #3234

Add missing part of migration from UNPKG to jsDelivr

* Restore UNPKG support in package.json

---------

Co-authored-by: scrhartley <scrhartley@github.com>
2025-06-16 16:37:26 -06:00
MichaelWest22
859708c379 move delay and view transitions to inside swap function for api and history use (#3328)
* move delay and view transitions to inside swap function

* Fix indenting and add tests

* move delay and view transitions to inside swap function

* Fix indenting and add tests

* revert rollback of feat: handle 'unset'for HX-Reselect in swap function
2025-06-16 17:31:07 -05:00
Sukka
7df5969664 Replace jsDelivr w/ UNPKG (#3234)
Some checks failed
Node CI / test_suite (push) Has been cancelled
Co-authored-by: 1cg <469183+1cg@users.noreply.github.com>
2025-06-03 07:14:03 -06:00
Ryan Kilpadi
5b4d77da6b Attach hx-on handlers before processing nodes (#3131) 2025-06-02 15:50:19 -05:00
Carson Gross
e7bb245ef4 fix bad strings, merge master, update package-lock.json 2025-06-02 11:54:49 -06:00
Carson Gross
0aaab5a3c9 Merge branch 'master' into dev
# Conflicts:
#	package-lock.json
#	package.json
2025-06-02 11:52:35 -06:00
Carson Gross
d1d0cd916b Merge remote-tracking branch 'origin/master'
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-06-02 11:51:28 -06:00
Carson Gross
dd1914d503 update https://tacohiro.systems/ link 2025-06-02 11:51:21 -06:00
Carson Gross
e783c88670 move repeat logic on HX-Retarget header out to its own function. 2025-06-02 11:48:22 -06:00
Carson Gross
8ec48d9d29 Merge remote-tracking branch 'origin/dev' into dev 2025-06-02 11:47:55 -06:00
Piotr Tomiak
11ff1940f0 Automatically generate Web Types for the package based on current documentation (#3071) 2025-06-02 11:25:23 -06:00
ludicrousdisplay
28b31f23db adding javalin-htmx examples to server integration examples (#3104)
* adding javalin-htmx examples to server integration examples

Adding two examples of using htmx with the javalin server library

* Update www/content/server-examples.md

Co-authored-by: Vincent <vichenzo-thebaud@hotmail.com>

---------

Co-authored-by: Vincent <vichenzo-thebaud@hotmail.com>
2025-06-02 11:21:38 -06:00
Vincent
53c5cf6df7 Remove obsolete npm command + fix links to ws/sse extensions (#3208)
Remove obsolete npm command + fix links to ws/SSE
2025-06-02 11:21:11 -06:00
Vincent
82546dbd47 Inherit list-keyword in hx-include / hx-indicator (#1766)
Inherit keyword in hx-include / hx-indicator
2025-06-02 11:20:24 -06:00
Jeremiah Johnson
4184d1fd0c fix(swap): apply swap delay in swap function instead of handleAjaxResponse (#2845)
* fix(swap): apply swap delay in swap function instead of handleAjaxResponse

* add swap delay test
2025-06-02 11:19:11 -06:00
Oliver Haas
6d238f3d61 Feat/hx reselect support unset (#3153)
feat: handle 'unset'for HX-Reselect in swap function (+ test)
2025-06-02 11:01:20 -06:00
Simon Hartley
075ed73799 Fix target value in htmx:targetError when inheritance has been used (#3178)
Co-authored-by: scrhartley <scrhartley@github.com>
2025-06-02 10:58:31 -06:00
Carson Gross
70f41e0c6c Merge remote-tracking branch 'origin/dev' into dev 2025-06-02 10:57:57 -06:00
JacobMonticello
e4ecc55586 Typo in addEventListener - use evt instead of event (#3188)
Use evt instead of event

Co-authored-by: Jacob monticello <Jacob Monticello>
2025-06-02 10:57:46 -06:00
Gustavo Guzmán
11a8e9c6c2 Feature: add extensionsToIgnore to withExtensions (#3195)
feat: add extensionsToIgnore to withExtensions
2025-06-02 10:56:51 -06:00
Carson Gross
646c8583be Merge remote-tracking branch 'origin/dev' into dev 2025-06-02 10:56:11 -06:00
David Martiník
730bd8224d Fixes issue 1537 - OOB does not escape query selector - updated version (#3304)
* Fixes issue 1537 - OOB does not escape query selector

* Adds test cases for oob swaps where the id contains special characters

* Updated oob multiple elements with the same ID test

* fix(issue-1537): resolved conflicts with master

* fix(issue-1537): fixed codestyle issues

---------

Co-authored-by: Fraser Chapman <fraser.chapman@gmail.com>
Co-authored-by: David Martiník <david.martinik@powerflow.cz>
2025-06-02 10:52:29 -06:00
Simon Hartley
8409ebca3b Fix missing TypeScript property (#3315)
Add hidden elt property to HtmxRequestConfig

Co-authored-by: scrhartley <scrhartley@github.com>
2025-06-02 10:50:51 -06:00
MichaelWest22
083dbcdd6f update web test-runner and remove temp summaryReporter workaournd (#3320)
* update web test-runner and remove temp summaryReporter workaournd

* Update error reporting to show in context of the test file and as a summary at the bottom
2025-05-22 07:17:16 +02:00
Simon Hartley
d2e39716fb Fix type for event parameter (#3317)
Fix type of event for HtmxExtension.onEvent

Co-authored-by: scrhartley <scrhartley@github.com>
2025-05-19 07:40:36 +02:00
QBH3
407408b947 Update sse.md (#3303)
version of sse extension does not match the sha384 hash, i believe it to be the newer 2.2.3 version of htmx-ext-sse
2025-05-08 08:22:55 +02:00
Devin Muzzy
0404d0137b documentation clarification for htmx:load (#3299)
htmx:load doc clarification
2025-05-01 08:33:09 +02:00
Simon Hartley
f4cc8382a4 Correction for confirm example (#3296) 2025-04-30 08:10:40 +02:00
Simon Hartley
ff190eef26 Add substr lint (#3295)
Co-authored-by: scrhartley <scrhartley@github.com>
2025-04-28 07:15:03 +02:00
Simon Hartley
53496ff428 Fix typo (#3294)
Co-authored-by: scrhartley <scrhartley@github.com>
2025-04-28 07:12:33 +02:00
Carson Gross
a9b673c93c Merge branch 'dev' of github.com:bigskysoftware/htmx into dev 2025-04-24 14:24:45 -06:00
MichaelWest22
5520566fc3 Add historyRestoreAsHxRequest config to optionally disable hx-request header from history restore requests (#3278)
* Added Config to optionally disable the breaking HX-Request change made recently

* fix broken resolved conflict
2025-04-24 14:21:23 -06:00
Simon Hartley
db8e5e03cb Fix event not being available in hx-vals/hx-vars when hx-trigger has delay (#3196)
Fix event not being available in hx-vals/hx-vars when hx-trigger uses delay

Co-authored-by: scrhartley <scrhartley@github.com>
2025-04-24 13:58:31 -06:00
Emil Hemdal
e01027b938 Write title as innerText instead of innerHTML (#3173)
* Change innerHTML to innerText for title element

Some security checks are grumpy when using innerHTML. Using innerText
instead calms them.

Signed-off-by: Emil Hemdal <emil@hemdal.se>

* Add Type Hints/Cast

* Change to textContent instead of innerText

Also remove type hinting/casting since it is no longer needed

Signed-off-by: Emil Hemdal <emil@hemdal.se>

---------

Signed-off-by: Emil Hemdal <emil@hemdal.se>
2025-04-24 13:57:34 -06:00
Simon Hartley
6a585f9f3c Get rid of latest usages of substr (#3074)
* Replace latest usages of substr

* Replace usage of substr in tests

---------

Co-authored-by: scrhartley <scrhartley@github.com>
2025-04-24 13:56:55 -06:00
MichaelWest22
05d37e6ea6 Remove old IE support (#3277)
* Remove old IE support

* don't need regex in normalizePath

* fix verifyPath to handle about: situations like some iframes now that there is no fallback

* improve diff

* fix logic mistake in last diff improvment

* Update url normlization test post testing upgrade

* remove un-needed document
2025-04-24 13:56:05 -06:00
MichaelWest22
d3bcd787ba proxy window.location for testing and extension overrides (#3283)
* proxy window.location for testing and extension overrides

* when not whe in test name
2025-04-24 13:55:27 -06:00
MichaelWest22
408850a08e Additional Code Coverage (#3282)
* Improve loc coverage by removing dead paths caused by bad type checks and add some tests for other paths

* removed exception for https://github.com/microsoft/playwright/issues/5894 that was fixed in 2022 with webkit 16.0
2025-04-24 13:54:43 -06:00
MichaelWest22
21dc121fce handle removing request lock on errors (#3284)
* handle removing request lock on errors

* Fixed non function verifyUrl tests and added tests for the double requests this PR fixes
2025-04-24 13:53:14 -06:00
Shawn Duncan
0da03839ce Create SECURITY.md (#3288)
* Create SECURITY.md

* Update SECURITY.md

Remove boiler plate text
2025-04-24 13:41:47 -06:00
Ajani Bilby
86893ebf4c Docs: Update Link (#3281)
use a permalink
2025-04-21 09:11:55 +02:00
Yawar Amin
4d16626cf4 Adjust examples (#3280)
Autofocus when editing, and update text to match example code.
2025-04-20 10:52:08 +02:00
Simon Hartley
bc7a6b8c55 Documentation update for events (#3279) 2025-04-20 10:49:26 +02:00
Carson Gross
63016891a6 Merge branch 'master' into dev 2025-04-17 17:58:26 -06:00
Carson Gross
f8b71843da Merge remote-tracking branch 'origin/dev' into dev 2025-04-17 17:58:04 -06:00
MichaelWest22
24a0106f76 Update testing framework to web-test-runner and improve code coverage (#3273)
* Fix old npm dependencies

* implement web-test-runner tests for headless alongside Mocha browser tests

* Increase test and code coverage

* update to 100% coverage and impove eslint

* Update testing Doco

* revert all htmx changes and updates/disable tests needed

* fix browser mocha test

* Default testing to use playwrite only instead of puppeter

* playwright install fix

* Imporve test summary reporting

* flatten false looks closer to original
2025-04-17 17:55:43 -06:00
Johannes Neumeier
e8f5990c1b Mention hx-include does not match disabled fields. Fixes #3053 (#3274) 2025-04-17 07:55:11 +02:00
Simon Hartley
6836e87a6c Docs - add sections and search filtering for community extensions (#3260)
Docs - add sections and search filtering for community extensions

Co-authored-by: scrhartley <scrhartley@github.com>
2025-04-07 22:24:39 -05:00
Pierre Chapuis
3830fa7b2c fix customized confirmation example (#3262) 2025-04-05 10:25:47 +02:00
FumingPower
0578564a25 docs: Add dynamic-url to community extensions list (#3259)
* docs: Add dynamic-url to community extensions list

* docs: applied minor corrections
2025-04-03 07:56:39 +02:00
Simon Hartley
b1c1a1ba23 Documentation update for hx-swap (#3261) 2025-04-02 10:29:14 -06:00
Simon Hartley
aa3dbf0c50 Documentation update for debug extension (#3255) 2025-03-28 08:03:47 +01:00
librasteve
838f49d977 Adding Raku / Cro Templates to Fragments list (#3253)
17:31]librasteve: hello, we recently added fragment to the Raku Cro Templates (as inspired by HTMX LOB) and I thought you may like to add to the list of fragments at https://htmx.org/essays/template-fragments/, something like Raku / Cro Template (https://github.com/croservices/cro-website/blob/main/docs/reference/cro-webapp-template-syntax.md#fragments) ~librasteve
[18:15]1cg: Hi there
[18:16]1cg: That looks great, yes, can you create a PR here w/ a link to that page: https://github.com/bigskysoftware/htmx/blob/master/www/content/essays/template-fragments.md
[18:16]1cg: and then send me a link to the PR and I'll integrate it
[19:17]librasteve: cool - will do
2025-03-28 08:00:03 +01:00
Carson Gross
a2681e3ee2 remove deco sponsorship 2025-03-27 10:43:26 -06:00
Carson Gross
f42117628c fix all the sponsor images 2025-03-25 21:43:48 -06:00
Carson Gross
15085704f2 Merge remote-tracking branch 'origin/master' 2025-03-25 21:33:32 -06:00
Carson Gross
0d7434f998 fix jetbrains logo 2025-03-25 21:33:23 -06:00
Joundill
d1aa89192f Documentation update for hx-get (#3251)
Update hx-get.md

Clarified notes on hx-get params
2025-03-25 08:44:22 +01:00
Simon Hartley
a3ac341994 Documentation update for hx-select-oob (#3250)
Co-authored-by: scrhartley <scrhartley@github.com>
2025-03-25 08:42:51 +01:00
Simon Hartley
abaf3de237 Documentation update for include-vals extension (#3247)
Documentation update for outdated extension
2025-03-25 08:38:19 +01:00
Simon Hartley
1b4e778ba6 Documentation improvements for idiomorph extension (#3249) 2025-03-25 08:36:33 +01:00
Simon Hartley
fec926d354 Documentation update for hx-trigger (#3246) 2025-03-24 08:08:41 +01:00
Simon Hartley
f8f7466d5c Fix broken link in SSE extension docs (#3241) 2025-03-19 07:34:20 +01:00
Sebastian Davids
1d1a3ceeee Fix JSDoc of getRespCodeTarget (#3235)
Signed-off-by: Sebastian Davids <sdavids@gmx.de>
2025-03-17 08:06:06 +01:00
James Cole
fcf7457766 Add attribute-tools Community Extension to doc (#3229)
added attribute-tools Community Extension to doc
2025-03-14 09:05:06 +01:00
Kai Schlamp
b76b1f6b9b Add django-block-fragments to template-fragments essay (#3227) 2025-03-12 10:34:28 -05:00
Carson Gross
b8a29903dc Merge remote-tracking branch 'origin/master' 2025-03-06 15:59:04 -07:00
Carson Gross
7b34baed84 remove ex-sponsors 2025-03-06 15:58:52 -07:00
su21
07d35186fb Fix wording in changed modifier description in hx-trigger.md (#3219)
Update the description of the `changed` event modifier from "the event will only change" to "the event will only fire" to clarify that the modifier controls when the `change` event triggers, improving accuracy and readability. Addresses a potential documentation error.

Fixes #3218
2025-03-06 08:16:19 +01:00
William Jackson
efcc6b2211 Add descriptions for /headers/* and /api (#3221)
* Add descriptions for /headers/* and /api

* Add a missing space in a description
2025-03-05 20:58:38 -07:00
jdocksey
103c72ed74 Documentation link added (#3211)
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-02-25 09:08:02 +01:00
Cristian Molina
d8dc1ee93d Small fix on alternatives.md (#3204)
Some checks failed
Node CI / test_suite (push) Has been cancelled
Update alternatives.md

Fix "and" -> "an"
2025-02-22 08:29:58 +01:00
Carson Gross
61ad9549c3 format
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-02-19 09:49:23 -07:00
Carson Gross
14f3c5efd8 format 2025-02-19 09:46:59 -07:00
Carson Gross
70736c4c8f Merge remote-tracking branch 'origin/master' 2025-02-19 09:26:08 -07:00
Carson Gross
163d226988 publish leonard richardson's interview 2025-02-19 09:26:02 -07:00
ollin
0a135f95ce Update hx-swap.md (#3194)
Some checks failed
Node CI / test_suite (push) Has been cancelled
typo
2025-02-18 09:30:55 +01:00
Mariss Tubelis
90a91a60e0 Extension docs: npm, bundler, min/unmin and SRI hash instructions (#3127)
Some checks failed
Node CI / test_suite (push) Has been cancelled
* Extensions docs: add npm/bundler installation guide and up versions numbers for links

* Revert extensions._index.md table change

* Update docs.md extension installation and integration instruction

* Move extension installation and enabling to new sections in docs.md

* Update extension installation guidelines

* Update idiomorph installation guidelines

* Minor consistency edits

* Make the need for hx-ext clearer

* Fix typos and note for community repos not hosted outside this repo
2025-02-14 22:37:21 +01:00
TGJ Gilmore
0f9c4202ba Documentation typo correction (#3182)
Some checks failed
Node CI / test_suite (push) Has been cancelled
* Documentation update to include the use of hx-headers to prevent CSRF

* Update hx-headers.md

Revised follow review.

* Update docs.md

Typo correction
2025-02-12 09:41:55 +01:00
TGJ Gilmore
72b425f5fb Documentation update to include the use of hx-headers to prevent CSRF (#3176)
Some checks failed
Node CI / test_suite (push) Has been cancelled
* Documentation update to include the use of hx-headers to prevent CSRF

* Update hx-headers.md

Revised follow review.
2025-02-10 20:35:31 +01:00
William Jackson
46badfe0b1 Add descriptions for attribute pages (#3158)
Some checks failed
Node CI / test_suite (push) Has been cancelled
Descriptions for attribute pages
2025-02-07 17:07:13 -07:00
Zendrael Ziul
6dbf554e49 Fixed repo url for pascal htmx project reference (#3169)
Some checks failed
Node CI / test_suite (push) Has been cancelled
* Add FreePascal example with Pas2JS

* hotfix pascal htmx repo

---------

Co-authored-by: 1cg <469183+1cg@users.noreply.github.com>
2025-02-05 09:36:56 +01:00
Zendrael Ziul
d8b12de2fb Add FreePascal server example with Pas2JS (#3168)
Some checks are pending
Node CI / test_suite (push) Waiting to run
Add FreePascal example with Pas2JS
2025-02-04 09:30:47 -06:00
Alexander Petros
a60cdf1854 Add "less htmx" link (#3167)
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-02-02 14:36:28 -07:00
Carson Gross
10e8656af5 Merge remote-tracking branch 'origin/master'
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-01-31 07:16:25 -07:00
Carson Gross
4328259749 publish mike amundsen's interview 2025-01-31 07:16:16 -07:00
Mehrad
6ea1029960 Fix some external links (#3157) (#3160)
Fix some external links (#3157)
2025-01-31 09:58:23 +01:00
William Jackson
a9b11187ba Mark the interviews folder as a section (#3161)
Some checks are pending
Node CI / test_suite (push) Waiting to run
Adding `_index.md` to the `interviews` folder will mark this folder as a "section". Pages in this section will now inherit section settings from the parent `essays` section, including rendering using the `essay.html` template instead of the default `page.html` template.

Adding `render = false` to the interview section metadata will suppress generating a page at `/essays/interviews/index.html`
2025-01-30 18:46:17 -07:00
Carson Gross
1c3556c30e Merge remote-tracking branch 'origin/master'
Some checks are pending
Node CI / test_suite (push) Waiting to run
2025-01-30 10:24:00 -07:00
Carson Gross
ac03dc540f publish defunkt interview 2025-01-30 10:23:52 -07:00
William Jackson
b0797be8cd Fix some external links (#3159)
Some checks are pending
Node CI / test_suite (push) Waiting to run
2025-01-30 12:42:00 +01:00
William Jackson
6d39919b99 Essay descriptions (#3154)
Some checks are pending
Node CI / test_suite (push) Waiting to run
* Add description to architectural-sympathy

And fix some typos

* Add description to codin-dirty

* Add description to complexity-budget

* Add description to does-hypermedia-scale

* Add description to future and fix date

* Add description to hateoas

* Add description to how-did-rest-come-to-mean-...

* Add description to htmx-sucks

* Add description to hypermedia-apis-vs-data-apis

* Add description to hypermedia-clients

* Add description to hypermedia-driven-applications

* Update hypermedia-friendly-scripting.md

* Update hypermedia-on-whatever-youd-like.md

* Update is-htmx-another-javascript-framework.md

* Update locality-of-behaviour.md

* Update lore.md

* Update mvc.md

* Update no-build-step.md

* Update prefer-if-statements.md

* Update rest-copypasta.md

* Update right-click-view-source.md

* Update spa-alternative.md

* Update splitting-your-apis.md

* Update template-fragments.md

* Update rest-explained.md

* Update two-approaches-to-decoupling.md

* Update vendoring.md

* Remove double-quote characters from descriptions

* Add description block to demo.html

* Update view-transitions.md

* Update web-security-basics-with-htmx.md

* Update webcomponents-work-great.md

* Update when-to-use-hypermedia.md

* Update why-gumroad-didnt-choose-htmx.md

* Update why-tend-not-to-use-content-negotiation.md

* Update you-cant.md

* Fix description for htmx-sucks

* Use `authors` built-in variable

Instead of the custom `author` taxonomy

* Descriptions and typos in interviews

* That double word is actually correct
2025-01-29 10:53:42 -07:00
Carson Gross
f760d3f27d typo 2025-01-29 10:42:52 -07:00
Carson Gross
bdbbeee284 remove AI slop 2025-01-29 10:42:22 -07:00
Carson Gross
f206885422 get rid of double headers
Some checks are pending
Node CI / test_suite (push) Waiting to run
2025-01-29 07:25:43 -07:00
Carson Gross
d941a204fc Merge remote-tracking branch 'origin/master' 2025-01-29 07:23:21 -07:00
Carson Gross
7b8e26e193 publish makinde adeagbo interview 2025-01-29 07:23:14 -07:00
William Jackson
0e1794f0fd Allow page description with default if missing (#3150)
Some checks failed
Node CI / test_suite (push) Has been cancelled
* Allow page description with default if missing

If an essay has a description in the frontmatter, use that description in the `<meta name="description">` tag.

* Add description to 10-tips-for-ssr-hda-apps

* Add description to a-real-world-nextjs-to-htmx-port

* Add description to a-real-world-react-to-htmx-port

* Add description to a-real-world-wasm-to-htmx-port

* Add description to a-response-to-rich-harris

* Add description to alternatives

* Add description to another-real-world-react-to-htmx-port
2025-01-27 16:36:45 -07:00
Carson Gross
8ef8fe369b typo
Some checks are pending
Node CI / test_suite (push) Waiting to run
2025-01-27 11:24:33 -07:00
Carson Gross
fd9af68ed8 improvements 2025-01-27 10:54:35 -07:00
Carson Gross
d1e693a15a update dates 2025-01-27 10:50:26 -07:00
Carson Gross
63975c72fe vendoring essay 2025-01-27 10:48:02 -07:00
Carson Gross
8ed85cdb22 Merge remote-tracking branch 'origin/master'
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-01-25 08:28:35 -07:00
Carson Gross
94b903ebab add htmeggs to lore 2025-01-25 08:28:23 -07:00
Ajani Bilby
27f0b31076 Extension Proposal: hx-drag for dragstart & drop triggered htmx requests. (#3147)
Some checks are pending
Node CI / test_suite (push) Waiting to run
Update _index.md
2025-01-25 13:05:40 +01:00
Carson Gross
4177071ee4 Merge remote-tracking branch 'origin/master'
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-01-21 09:47:49 -07:00
Carson Gross
1ac87b9f9c add triptych & fixi 2025-01-21 09:47:40 -07:00
Zixian
d890547012 Adding example to hx-ext documentation (#3109)
Some checks failed
Node CI / test_suite (push) Has been cancelled
* Update hx-ext.md

* Update hx-ext.md

* Update www/content/attributes/hx-ext.md

Co-authored-by: Vincent <vichenzo-thebaud@hotmail.com>

* Update hx-ext.md

---------

Co-authored-by: Vincent <vichenzo-thebaud@hotmail.com>
2025-01-16 08:27:51 +01:00
Kylie Smith
5fd60a5745 Fixed TwinSpark link in alternatives (#3124)
Some checks failed
Node CI / test_suite (push) Has been cancelled
Fixed TwinSpark link
2025-01-13 09:32:24 +01:00
Tapani Honkanen
05c88d80ed Fix htmx:beforeHistorySave docs (#3123) 2025-01-13 09:31:54 +01:00
Carson Gross
aeeb2165a9 alternatives
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-01-11 17:43:40 -07:00
Carson Gross
29ace831cb alternatives 2025-01-11 17:30:31 -07:00
Carson Gross
02bc415735 fix link
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-01-10 12:38:47 -07:00
Joe Fioti
c802494f7e added wasm to htmx port essay (#3119)
* added wasm to htmx port essay

* changed wasm-to-htmx essay

* added image and changed author
2025-01-10 12:17:40 -07:00
Carson Gross
65145db5bf Merge remote-tracking branch 'origin/master'
Some checks are pending
Node CI / test_suite (push) Waiting to run
2025-01-09 16:58:01 -07:00
Carson Gross
1a38467f9c sponsor update 2025-01-09 16:57:54 -07:00
Geoffrey B. Eisenbarth
9fcb5c5c32 Enforce no-op on submit buttons with formmethod=dialog. (#3075)
Some checks failed
Node CI / test_suite (push) Has been cancelled
* Enforce no-op on submit buttons with formmethod=dialog.

* Properly resolve referenced forms. (#3094)

* Properly resolve referenced forms.

* Clarify variable.

* Cast elt to avoid TS exceptions.

* Refactor for JSDoc.

* Clarify shouldCancel.

* Remove complicated JSDoc in favor of ts-ignore.

* More coverage for button scenarios.

* Use properties instead of matching.

* Mention reset button change.

* Mention formmethod=dialog change.
2025-01-09 15:22:36 -07:00
Geoffrey B. Eisenbarth
f46989b24a Properly resolve referenced forms. (#3094)
Some checks are pending
Node CI / test_suite (push) Waiting to run
* Properly resolve referenced forms.

* Clarify variable.

* Cast elt to avoid TS exceptions.

* Refactor for JSDoc.

* Clarify shouldCancel.

* Remove complicated JSDoc in favor of ts-ignore.

* More coverage for button scenarios.

* Use properties instead of matching.

* Mention reset button change.
2025-01-09 11:23:32 -07:00
Adam Zapletal
e0905ff94a Fix broken docs links on quirks page (#3111)
Some checks are pending
Node CI / test_suite (push) Waiting to run
2025-01-09 09:12:13 +01:00
Carson Gross
bc95b40004 Merge remote-tracking branch 'origin/master'
Some checks are pending
Node CI / test_suite (push) Waiting to run
2025-01-08 12:33:45 -07:00
Carson Gross
69edc99956 add ACM Newsletter paper 2025-01-08 12:33:37 -07:00
Simon Hartley
9062996025 [Documentation] Make htmx:beforeSwap more complete (#3110)
Some checks are pending
Node CI / test_suite (push) Waiting to run
Co-authored-by: scrhartley <scrhartley@github.com>
2025-01-08 09:13:31 +01:00
Yawar Amin
271a3869c5 Render toast as aria-live region on page load (#3112)
Some checks are pending
Node CI / test_suite (push) Waiting to run
* Render toast as aria-live region on page load

This tells the screen reader to announce updates that happen inside the
`span#toast` element.

* Update code sample in example text

* Use <output> element to announce form action
2025-01-07 23:33:03 -05:00
Mark Bundschuh
e64dd0f8ba Fix error in quirks documentation for htmx config (#3098)
Some checks are pending
Node CI / test_suite (push) Waiting to run
2025-01-07 10:15:50 +01:00
Ryan Kilpadi
9f0de8199f Add 404 page (#3105) 2025-01-07 10:15:02 +01:00
Ryan Kilpadi
5f3cb583ab Add example for form reset pattern (#3100) 2025-01-07 09:49:40 +01:00
Carson Gross
d3996e1eec Merge remote-tracking branch 'origin/master'
Some checks are pending
Node CI / test_suite (push) Waiting to run
2025-01-06 09:50:59 -07:00
Carson Gross
b7a3221c07 retrospective-essay 2025-01-06 09:50:50 -07:00
Eduardo Canellas
3455ebe8b3 fix QUIRKS.md typo (#3095)
Some checks are pending
Node CI / test_suite (push) Waiting to run
2025-01-06 07:48:25 +01:00
Asif Imran
c6679b18eb Update docs.md to fix dead link (#3090)
Update docs.md

dead link
2025-01-06 07:27:32 +01:00
Carson Gross
667e07f432 Merge remote-tracking branch 'origin/master'
Some checks are pending
Node CI / test_suite (push) Waiting to run
2025-01-05 09:44:00 -07:00
Carson Gross
9a4483ea83 add copy & paste components as a Con for htmx 2025-01-05 09:43:52 -07:00
Maciej Trybilo
1e844f67c3 Add an example of evaluated values passed to hx-headers. (#2939)
Some checks are pending
Node CI / test_suite (push) Waiting to run
* Update hx-headers.md

Add an example of evaluated values passed to hx-headers.

* Follow the example in hx-vals.
2025-01-05 08:48:36 +01:00
Carson Gross
3080b41a4e fix quirks
Some checks are pending
Node CI / test_suite (push) Waiting to run
2025-01-04 17:53:59 -07:00
Carson Gross
38f135fa42 update sponsors
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-01-02 16:53:09 -07:00
Carson Gross
30880c3af7 until next christmas...
Some checks failed
Node CI / test_suite (push) Has been cancelled
2025-01-01 07:31:24 -07:00
Carson Gross
02ef84fca3 fix link
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-12-29 13:50:56 -07:00
Carson Gross
762604f6af update QUIRKS.md 2024-12-29 13:49:40 -07:00
Carson Gross
5cd6402065 update QUIRKS.md
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-12-25 12:51:12 -07:00
Carson Gross
726a64f853 update QUIRKS.md 2024-12-25 12:46:42 -07:00
Carson Gross
da049e2a4c update QUIRKS.md 2024-12-25 07:59:30 -07:00
Carson Gross
3f230d6c73 Merge remote-tracking branch 'origin/master'
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-12-24 13:19:35 -07:00
Carson Gross
b9b5dfa80d formatting 2024-12-24 13:19:22 -07:00
Alexander Petros
4c0b2e1b4a Fix h2 color (#3091) 2024-12-24 11:57:47 -07:00
Carson Gross
1ee743332f fix double frontmatter issue 2024-12-24 11:05:02 -07:00
Carson Gross
a8b01016b2 Merge remote-tracking branch 'origin/master' 2024-12-24 11:00:57 -07:00
Carson Gross
b15d185aec QUIRKS.md 2024-12-24 11:00:47 -07:00
Jon-Michael
3523732baa Added a favicon, merry christmas!!! (#3089)
Some checks are pending
Node CI / test_suite (push) Waiting to run
Co-authored-by: Jon-Michael Hartway <jhartway@citytelecoin.com>
2024-12-23 09:43:12 -07:00
Yawar Amin
fb3b6a13a7 Docs anchors (#3047)
* Fix anchor id for 'Configuring Response Handling Examples' header

* Move heading anchor links to right of heading

To make sure they don't get cut off by weird browsers.

Also make the anchor link text the 'link' emoji which is more fun than
the hashtag.

* Keep anchor links on the left

* Make entire header the anchor link and show link emoji on hover

* Fix anchor link colour in dark mode

* Revert anchor link icon to hash character

---------

Co-authored-by: 1cg <469183+1cg@users.noreply.github.com>
2024-12-23 09:41:47 -07:00
Carson Gross
16933aa7cc update lore 2024-12-23 09:28:51 -07:00
Carson Gross
7acc9db6da update lore and fix github star count alignment in mobile
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-12-22 21:07:56 -07:00
Carson Gross
e1cf65abb4 get rid of the nav shift
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-12-21 09:34:13 -07:00
Carson Gross
2d3fbbf09f lore update
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-12-20 14:32:22 -07:00
Carson Gross
87de42f0c7 Merge remote-tracking branch 'origin/master'
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-12-20 09:11:37 -07:00
Carson Gross
51c603ef9d christmas gift request 2024-12-20 09:11:25 -07:00
Alexander Petros
eab4f77af2 Fix escaped quotes in titles (#3086)
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-12-19 22:37:26 -07:00
Alexander Petros
69857dfdbb Add /essays/all (#3085)
Some checks are pending
Node CI / test_suite (push) Waiting to run
I set up zola to aggregate all these and create an RSS feed for them. I
also removed a couple of the essays from the /essays page in an attempt
to highlight the best one for each argument (now that they can all be
found on /essays/all).
2024-12-18 19:39:25 -07:00
Carson Gross
db71a58af0 Merge remote-tracking branch 'origin/master'
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-12-18 18:02:04 -07:00
Carson Gross
a76b68d722 update lore 2024-12-18 18:01:54 -07:00
Matt544
1814f99023 Add reference to hx-preserve to the example at "Preserving File Inputs after Form Errors" (#2474)
* Add reference to hx-preserve to file-upload-input.md

* Address review feedback: add more description behaviour when using .
2024-12-18 21:09:45 +01:00
Carson Gross
7cd560571d update lore 2024-12-18 12:49:35 -07:00
Carson Gross
68cff62b56 firefox fix 2024-12-18 12:45:40 -07:00
Carson Gross
1625f92eaf lore update
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-12-17 20:43:08 -07:00
Carson Gross
bd67c53dd6 lore update 2024-12-17 20:42:24 -07:00
Carson Gross
b6467ae035 gotta be responsive, c'mon 2024-12-17 20:19:52 -07:00
Carson Gross
876beb67b4 lore update 2024-12-17 20:17:01 -07:00
Carson Gross
35b2ef31a0 lore update 2024-12-17 19:53:36 -07:00
Carson Gross
5744603751 hold my beer 2024-12-17 19:45:52 -07:00
Carson Gross
c2579039f2 update lore.md 2024-12-17 18:30:37 -07:00
Carson Gross
240ceb6cb7 update lore.md 2024-12-17 18:17:31 -07:00
Carson Gross
e362655f20 update lore.md 2024-12-17 18:02:09 -07:00
Carson Gross
790bbf8aaf update lore.md 2024-12-17 17:53:54 -07:00
Carson Gross
22086c534b update lore.md 2024-12-17 17:53:32 -07:00
Carson Gross
646bb02662 update lore.md 2024-12-17 17:48:46 -07:00
Carson Gross
d0922265d5 update lore.md 2024-12-17 17:48:09 -07:00
Carson Gross
03f606a2c0 update lore.md 2024-12-17 17:45:36 -07:00
Carson Gross
616c0c4e87 update lore.md 2024-12-17 17:39:53 -07:00
Carson Gross
6efba5e5ef update lore.md 2024-12-17 17:35:22 -07:00
Carson Gross
87e3dc53bc add lore.md 2024-12-17 17:20:51 -07:00
Carson Gross
a399672a4a Merge remote-tracking branch 'origin/master' 2024-12-17 17:10:42 -07:00
Carson Gross
333a37349b add lore.md 2024-12-17 17:10:32 -07:00
MikeMoolenaar
eff9a0ba25 Improve active-search example by not using chrome-only event "search" (#2229)
* Improve active-search documenation by not using chrome-only type 'search' input

* Fix typo

* Restore input type to search
2024-12-16 10:25:02 +01:00
Mariss Tubelis
0c6bf35f22 Update preload extension documentation: form preloading and preload="always" (#3001)
* Update documentation for preload extension - include form element preloading and preload always attribute description

* Adjust version number

* Document HX-Preload header

* Rename preloaded header from HX-Preload to HX-Preloaded

* Up version

* Update www/content/extensions/preload.md

Co-authored-by: Vincent <vichenzo-thebaud@hotmail.com>

---------

Co-authored-by: Vincent <vichenzo-thebaud@hotmail.com>
2024-12-16 09:07:54 +01:00
Zixian
b3d3569719 Update hx-trigger.md (#3076)
* Update hx-trigger.md

- Added a clarification in notes to suggest adding a delay when adding reset to hx-trigger.

- Also thought it's useful to add a reference to MDN's list of web API  events, as it was one of the omissions in the hx-trigger doc which made me Google a little when I started out.

* Update hx-trigger.md

My bad, you're absolutely right. I've rewritten the proposed changes with your explanation in mind.
2024-12-15 09:25:58 +01:00
Carson Gross
44af27f400 fix url 2024-12-13 20:49:15 -07:00
Carson Gross
b0b08fa9d2 Merge remote-tracking branch 'origin/master' 2024-12-13 11:08:46 -07:00
Carson Gross
b82cf843e4 update sha 2024-12-13 11:06:48 -07:00
Carson Gross
db42b46218 2.0.4 release notes
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-12-12 14:34:08 -07:00
Carson Gross
233bf01a4b 2.0.4 release notes 2024-12-12 14:33:31 -07:00
Carson Gross
1a80c283f5 2.0.4 release notes 2024-12-12 13:25:36 -07:00
Carson Gross
52db21955e prep next release 2024-12-12 13:13:37 -07:00
Carson Gross
fb78106dc6 prep next release 2024-12-12 13:12:49 -07:00
Geoffrey B. Eisenbarth
cc2466b1f8 Cancel vanilla submits from <button[form]/>. (#3072) 2024-12-12 13:09:26 -07:00
Vincent
a331244923 Support multiple extended selectors for hx-include, hx-trigger from, and hx-disabled-elt (#2902)
* Initial suggestion (squashed)

Support multiple extended selectors for hx-include

Additional test for nested standard selector

Add @MichaelWest22 hx-disabled-elt multiple selector test

Add hx-trigger `from` test with multiple extended selectors

Simplify

Include #2915 fix

Update htmx.js

Split for readability

Don't apply global to previous selectors

Rewrite loop, restore global recursive call, minimize diff

Use break for better readability

Co-Authored-By: MichaelWest22 <12867972+MichaelWest22@users.noreply.github.com>

* Keep global as a first-position-only keyword

* Wrapped selector syntax

* Replace substring check by individual chars check

* Fix format

---------

Co-authored-by: MichaelWest22 <12867972+MichaelWest22@users.noreply.github.com>
2024-12-12 11:12:01 -07:00
Mikael Ståldal
06d42778fa Add http4k server example (#3012)
Adding https://github.com/mikaelstaldal/htmx-http4k-thymeleaf
2024-12-11 16:47:45 -07:00
Carson Gross
232667d2c6 fix https://github.com/bigskysoftware/htmx/issues/1788
boosted forms that issue a `GET` (and only a `GET`) and have no `action` attribute or an empty `action` attribute should clear the existing parameters of the current path when submitting lmao
2024-12-11 16:42:20 -07:00
Carson Gross
815c117088 comment out test that is breaking suite in browser 2024-12-11 15:06:03 -07:00
Carson Gross
e171bca9e7 Merge branch 'master' into dev 2024-12-11 14:59:27 -07:00
Simon Hartley
704dac7a7f Replace deprecated String.prototype.substr usage (#2951)
Co-authored-by: shartley <scrhartley@github.com>
2024-12-11 14:28:09 -07:00
basvk
34dda10f9e Do not execute hx-trigger="load" on re-initialization of an existing node (#2976)
* Do not execute hx-trigger="load" on re-initialization of an existing node

* simplify initNode firstInit logic
2024-12-11 14:27:34 -07:00
Alexander Petros
bd35f64cf7 Add missing htmx:trigger event on load triggers (#3033) 2024-12-11 14:20:12 -07:00
Alexander Petros
5ab508f652 Make bodyContains return true for nested shadow roots (#3034)
This fixes an issues in which `bodyContains()` would incorrectly return
false for nested shadow roots.
2024-12-11 14:17:32 -07:00
MichaelWest22
c24fb71a10 Handle Invalid template content (#3064) 2024-12-11 14:16:20 -07:00
Carson Gross
2e59a14213 follower24 sponsorship
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-12-07 20:25:46 -07:00
Carson Gross
45f9c7aa4a prefer if statements to polymorphism 2024-12-07 20:18:47 -07:00
Carson Gross
f946dbd876 new sponsors
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-12-05 13:18:27 -07:00
Carson Gross
2b88d967c1 design update
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-12-03 07:25:05 -07:00
Carson Gross
68ef5c51e2 Merge remote-tracking branch 'origin/master'
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-12-02 11:06:21 -07:00
Carson Gross
c9b8f0a211 add a modest critique 2024-12-02 11:06:12 -07:00
Ryan Kilpadi
445fddeb28 [Documentation] Update location of ws/sse demo server (#3052)
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-11-30 10:30:53 +01:00
Ryan Kilpadi
ffbcd9291c [Documentation] Fix outdated attribute links (#3051)
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-11-30 10:27:47 +01:00
Carson Gross
24fb2fe522 essay fix
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-11-25 05:00:59 -07:00
Carson Gross
bd2150aaaf Merge remote-tracking branch 'origin/master' 2024-11-25 04:48:51 -07:00
Carson Gross
8ff3b3d186 essay fix 2024-11-25 04:48:30 -07:00
Felipe Gené
1a23a5a6e9 feat: add amz-content-sha256 extension to docs (#3036) 2024-11-25 11:25:53 +01:00
Carson Gross
cef216bc55 essay fix
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-11-24 18:04:35 -07:00
Carson Gross
3e1a3934c9 Merge remote-tracking branch 'origin/master' 2024-11-24 18:00:51 -07:00
Carson Gross
f600eb8550 essay fix 2024-11-24 18:00:42 -07:00
Alec Gargett
6067f4d5d7 fixed minor typo "and" --> "an".md (#3035)
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-11-24 17:19:35 -05:00
Carson Gross
aa9434e002 Merge remote-tracking branch 'origin/master' 2024-11-24 13:01:09 -07:00
Carson Gross
b9cafa3ffa Merge branch 'master' into codin-dirty-essay 2024-11-24 13:00:33 -07:00
Carson Gross
4ba6852aea finish codin' dirty essay 2024-11-24 13:00:26 -07:00
MichaelWest22
5373966e7d Improve detail.elt event documentation (#3007)
Some checks failed
Node CI / test_suite (push) Has been cancelled
improve detail.elt event documentation
2024-11-24 12:24:42 -05:00
Paul Garner
bc7ea4a8d9 typing for defineExtension should allow partials (#3030)
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-11-22 11:03:42 +01:00
Simon Hartley
7415f395b2 [Documentation] Fix link in web socket extension docs (#3026)
Some checks failed
Node CI / test_suite (push) Has been cancelled
Co-authored-by: scrhartley <scrhartley@github.com>
2024-11-19 09:47:29 +01:00
Simon Hartley
7879d2e3bc [Documentation] Add missing htmx:sendAbort event (#3024)
Some checks failed
Node CI / test_suite (push) Has been cancelled
Co-authored-by: scrhartley <scrhartley@github.com>
2024-11-17 10:24:16 +01:00
Simon Hartley
001f9e024a [Documentation] Fix incorrect link (#3022)
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-11-15 09:57:00 +01:00
Carson Gross
dba0fcf7e6 update video
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-11-14 15:32:00 -07:00
Alexander Petros
b4ebb527a1 WC essay typo fixes (#3021)
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-11-14 08:54:21 +01:00
Markus Unterwaditzer
8d07de9708 fix vertical alignment of github stars button in all states (#3006)
Without uBlock, the button's alt-text would be 1px lower than the rest
of the nav items, and inconsistently capitalized.

With uBlock, the button would be slightly too high up.

This is on Firefox.

The solution is to apply styles on the `span` that GitHub's JS inserts.
However, using margin will make the entire nav jump during pageload, so
position: relative seems more robust there.
2024-11-13 22:29:30 -05:00
Alexander Petros
82eb2a635a Fix name on essay (#3019)
Some checks are pending
Node CI / test_suite (push) Waiting to run
Fix name on essay
2024-11-13 17:24:46 -07:00
Alexander Petros
ccbc101dd8 Add web components essay (#3018) 2024-11-13 15:20:56 -07:00
Sascha Woo
df73ff2a7a Add missing disableInheritance documentation (#3017)
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-11-13 16:05:04 +01:00
Markus Unterwaditzer
79b6504d63 Fix wrong cursor style in webring (#3015)
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-11-12 07:53:34 +01:00
Simon Hartley
d39dd0e576 [Documentation] Add missing default extension points (#3014) 2024-11-12 07:52:13 +01:00
Carson Gross
ec955496ed reorganize essays 2024-11-07 15:55:58 -07:00
Carson Gross
a2b868833c Merge branch 'master' into codin-dirty-essay 2024-11-07 15:19:48 -07:00
Carson Gross
3dc73f6d04 reorganize essays
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-11-07 10:19:19 -07:00
Pouria Ezzati
5de1c76f6c Add Next.js to htmx essay (#3002)
add next.js to htmx essay
2024-11-07 10:16:25 -07:00
Simon Hartley
62969122f1 Fix illegal invocation for FormData proxy (#2997)
Some checks failed
Node CI / test_suite (push) Has been cancelled
Fix illegal invocation for a FormData proxy returning a function looked-up via a symbol

Co-authored-by: shartley <scrhartley@github.com>
2024-11-07 10:15:07 -07:00
Carson Gross
e5e8d9c38d Merge remote-tracking branch 'origin/master'
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-11-05 12:28:28 -07:00
Carson Gross
b37b438c4f remove single line closing angle brackets wft 2024-11-05 12:28:17 -07:00
Lee Phillips
e88bc9b35b Update server-examples.md (#2996)
Some checks failed
Node CI / test_suite (push) Has been cancelled
Add links to two Julia projects by Lee Phillips.
2024-11-03 22:20:37 -06:00
Maik Jablonski
5d27ee7856 Added Jeasx example for template fragements (#2991)
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-11-02 09:25:24 -06:00
maddalax
b9c25acf64 Add htmgo to server examples (#2992)
Some checks failed
Node CI / test_suite (push) Has been cancelled
* add htmgo

* move
2024-10-31 09:31:20 -06:00
Rob Zimmerman
d39a598e0b Fixing a typo in confirm.md (#2988)
Some checks are pending
Node CI / test_suite (push) Waiting to run
`evt` is not the correct variable name here - it should be `e`.
2024-10-31 10:18:01 +01:00
Carson Gross
841df9b6e6 fix link
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-10-28 10:45:51 -06:00
Carson Gross
0a62d025cf Merge remote-tracking branch 'origin/master'
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-10-27 16:49:52 -06:00
Carson Gross
3517c734e5 add sponsor 2024-10-27 16:49:43 -06:00
Simon Hartley
1a1b4a1613 docs: Fix typo (#2984)
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-10-27 08:00:33 +01:00
Simon Hartley
70da8e43a6 Fix typo on extensions page (#2983)
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-10-26 09:17:22 +02:00
Simon Hartley
43e703bda1 Fix link to no-cache extension readme (#2981)
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-10-25 08:55:40 +02:00
Spencer Brown
2617bbded6 "Download a Copy" in the doc points to old release 2.0.2; this PR changes it to 2.0.3 as it should be (#2980)
Fix "Download a copy" link to point to current 2.0.3
2024-10-25 08:54:28 +02:00
Paweł Korzeniewski
ff6964b6a5 Add missing word in previous CSS selector documentation (#2977)
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-10-23 08:34:32 +02:00
MichaelWest22
816fe6d161 ajax helper with no source or target defaults to body (#2948)
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-10-20 16:38:41 -06:00
youssame
3e265ea263 Fix TypeError on null path variable (#2967)
* Fix the error

* add tests
2024-10-20 16:37:58 -06:00
Carson Gross
3a105a900b Merge remote-tracking branch 'origin/master'
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-10-17 07:35:17 -06:00
Carson Gross
10658a049b remove 2024-10-17 07:35:09 -06:00
grugBraid
b6af863e52 Add link to contact-app built in Blazor (#2970)
Some checks are pending
Node CI / test_suite (push) Waiting to run
* Added htmx contact app using Blazor SSR

* add back whitespace
2024-10-17 09:46:09 +02:00
Carson Gross
c5e82ba49f Merge remote-tracking branch 'origin/master'
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-10-14 06:16:56 -06:00
Carson Gross
0d217e9b09 add photoquest to webring.md 2024-10-14 06:16:47 -06:00
Carson Gross
49d3fab984 more work on essay 2024-10-14 06:15:10 -06:00
Kian Yang Lee
f0964d2d08 docs: migrate realtime server implementation (#2955)
Some checks failed
Node CI / test_suite (push) Has been cancelled
Removes the realtime server for testing WebSockets and Server Sent
Events (SSE) in htmx to the extension repository, as mentioned in issue #2944.
2024-10-13 10:09:32 +02:00
Paweł Korzeniewski
0ce391e924 Use correct extended CSS selector name in hx-trigger notes section (#2964)
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-10-13 10:08:10 +02:00
Carson Gross
6cb5050994 Merge branch 'master' into codin-dirty-essay 2024-10-12 12:02:44 -06:00
youssame
56ca3eeef2 docs: Fix typo in URL to prevent broken link redirect (#2962)
Some checks failed
Node CI / test_suite (push) Has been cancelled
Doc: Fix typo in URL to prevent broken link redirect
2024-10-11 18:21:42 +02:00
Simon Hartley
5a7899dca0 Correct lint command in contribution guidelines (#2950)
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-10-08 08:51:36 +02:00
MichaelWest22
1242977d11 improve hx-preserve documentation (#2949)
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-10-07 18:58:49 +02:00
eikek
8a60c695bf Add htmx4s to scala server-examples (#2722)
Some checks failed
Node CI / test_suite (push) Has been cancelled
Add htmx4s to scala examples
2024-10-05 09:40:57 +02:00
Viktor Szépe
e64ca1ff38 Fix typos in docs (#2943) 2024-10-05 09:38:40 +02:00
Carson Gross
defcf160d8 add moveBefore() demo
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-10-04 13:15:07 -06:00
Carson Gross
b19e2f7dab add moveBefore() demo 2024-10-04 13:04:58 -06:00
Carson Gross
52f8076dcf Merge remote-tracking branch 'origin/master' 2024-10-04 11:53:48 -06:00
Carson Gross
d6e17abb13 final doc fix
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-10-04 11:53:03 -06:00
Bracken
08369730b6 Clarify event filter uses in hx-trigger (#2914)
Some checks are pending
Node CI / test_suite (push) Waiting to run
* Clarify event filter uses in hx-trigger

* Change hx-trigger event filter example to avoid ambiguity

input is both a tag and an event.

* Document the scope difference between standard selector and event filter

from:input listens to the page and click[event.target.matches('input')] listens to the element.

* Document that event filters as an alternative to CSS selectors require eval

* Move note about eval to standard event filters section.

* Simplify the referenced to standard event filters in the standard event modifiers section, and link to the standard event filters section instead.

* End quote in the correct place in standard event modifier doc.

* Correct language on how event filters from:body receive events.

* hx-trigger filter example catches click events specifically.
2024-10-04 09:26:09 +02:00
Carson Gross
5b2fe2c19c move general extension docs back to htmx.org/extensions and include core extension documentation on the site
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-10-03 18:52:59 -06:00
Carson Gross
22e5dfdc05 release prep 2024-10-03 15:06:53 -06:00
Carson Gross
f4e67863d9 release prep
Some checks are pending
Node CI / test_suite (push) Waiting to run
2024-10-02 22:06:56 -06:00
MichaelWest22
033d295ef9 fix es6 tsc checks complaining on newer s regex flag (#2938) 2024-10-02 22:05:40 -06:00
Carson Gross
56f801f69d release prep 2024-10-02 21:09:37 -06:00
Carson Gross
2fc32b368c release prep 2024-10-02 20:22:11 -06:00
Carson Gross
9b9bb7b5fa release prep 2024-10-02 20:20:57 -06:00
Carson Gross
d9b4ada06b release prep 2024-10-02 20:19:43 -06:00
Carson Gross
1c4d378d03 fix formatting 2024-10-02 20:11:29 -06:00
Carson Gross
c7278c448e fix test (firefox, safari) 2024-10-02 20:09:11 -06:00
Carson Gross
a440bcdb41 Merge branch 'master' into dev 2024-10-02 19:58:39 -06:00
Carson Gross
1537833ae0 Merge remote-tracking branch 'origin/dev' into dev 2024-10-02 19:58:30 -06:00
MichaelWest22
958fef20d9 Add shadowRoot host selector (#2866) 2024-10-02 19:46:11 -06:00
Jonathan
99285cd5c3 fix for hx-swab-oob within web components (#2846)
* Failing test for oob-swap within web components

* hx-swap-oob respects shadow roots

* Lint and type fixes

* fix jsdoc types for rootNode parameter

* Fix for linter issue I was confused about before

* oob swaps handle global correctly

* swap uses contextElement if available, document if not

Previous a commit made swapOptions.contextElement a required field. This
could have harmful ramifications for extensions and users, so instead,
the old behavior of assuming document as a root will be used if the
contextElement is not provided.

* rootNode parameter is optional in oobSwap

If not provided, it will fall back to using document as rootNode. jsdocs
have been updated for oobSwap and findAndSwapElements accordingly.
2024-10-02 19:44:41 -06:00
Nathan
8c6582679b Better graceful degradation of boosted form element (#2802)
* better graceful degradation of form elt

* smaller

* move fix and add tests
2024-10-02 19:44:13 -06:00
aeccue
c24adef38f [FIX] Properly remove request indicators (#2860)
Deduct request count before removing request indicators

Co-authored-by: 1cg <469183+1cg@users.noreply.github.com>
2024-10-02 19:43:19 -06:00
MichaelWest22
0e1eeec8b4 remove extra hx-swap-oob attribute that is not used in the page (#2823)
remove extra hx-swap-oob tag that is not used in the page
2024-10-02 19:40:54 -06:00
Eric Kwoka
b23b2f034e 🐛 Prevents erroring on null vals (#2799)
* 🐛 Prevents erroring on null vals

* 🚧 Applies same fix in FormProxy

* 🧪 Adds Test for null in FormDataProxy
2024-10-02 19:21:02 -06:00
MichaelWest22
5b550e5c49 Optimize Head regex (#2781)
* remove shared tag regex utility function that is no longer really needed

* fix head-support manual test to point to externally hosted extension

* minimize regex
2024-10-02 19:20:23 -06:00
MichaelWest22
b98e4f2b12 fix htmx.ajax defaulting to swap body when target not found (#2878)
* ajax helper handle no target

* allow source only targeting

* Add tests

* Handle source set but invalid target set

* Improve source logic

* missed #

* improve readiblity and add inline comment
2024-10-02 19:18:22 -06:00
Joerg Sonnenberger
df92b295d6 Change hx-trigger's changed modifier to work for independent trigger specifications (#2891)
* Adjust hx-trigger's changed modifier for multiple sources

The `changed` trigger modifier can see different event targets, either due
to the `from` modifier or event bubbling. The existing behavior trigger
only for one node (`from` modifier) or inconsistently (bubbling).

Use a nested weak map to keep track of the last value per distinguished
(trigger specification, event target node) pair. The weak map ensures
that Garbage Collection can still recycle the nodes.

If a event target was not seen via `from`, it is assumed changed for the
first time the trigger is hit.

* Add test case for separate triggers with changed modifier
2024-10-02 19:17:25 -06:00
Jackie Li
4916ce4d02 fix #2932: check parent is null for swap delete (#2933)
* fix #2932: check parent is null for swap

* fix test in swap when parent elt deleted
2024-10-02 19:08:39 -06:00
MichaelWest22
4a8172325e enable hx-preserve handing for oob swaps (#2934)
* Add support for oob swaps with hx-preserve

* Add tests

* Documentation

* Impove fix to handle when oob swaps shouldSwap set false
2024-10-02 19:02:46 -06:00
Carson Gross
c069f208b2 clean up the formatting for the gumroad essay
Some checks failed
Node CI / test_suite (push) Has been cancelled
2024-10-02 19:01:16 -06:00
Sahil Lavingia
42e51a191e gumroad essay (#2936)
* gumroad essay

* cp

* cp

* cp

* updates based on feedback

* cp

* cp

* cp
2024-10-02 18:44:36 -06:00
Carson Gross
d0a84a451c sub in tracebit logo 2024-10-02 15:59:24 -06:00
Carson Gross
eeeba484cf add cased to sponsors replacing codacy 2024-10-01 12:15:50 -06:00
MichaelWest22
d528c9d94d Handle Space before comma in Trigger Spec (#2903)
* strip space after trigger spec

* Add test

* handle addiional case
2024-09-25 13:12:37 -06:00
MichaelWest22
3d1a2e5202 [bug] load trigger stops hx-disabled-elt getting re-enabled (#2925)
* allow disable-elt on load to function

* Update requestCount fallback
2024-09-25 12:13:25 -06:00
Carson Gross
b8c92b8071 Merge branch 'master' into dev 2024-09-25 12:07:51 -06:00
Simon Hartley
aad0fbc7ed Add a link to the extensions site on the docs page (#2930)
Add a link to the extensions site in the docs page
2024-09-25 09:40:55 +02:00
Luis Eduardo
27b5bcc438 Improved documentation for htmx:confirm event and examples for implementing sweetalert (#2926)
* Add detail.question and detail.issueRequest(skipConfirmation=false) documentation to htmx:confirm event

* Update htmx:confirm event documentation

* Update htmx:confirm event documentation

* Update htmx:confirm event documentation

* Modify htmx:confirm event documentation
2024-09-25 09:39:28 +02:00
Brooke Kuhlmann
44c4de41cc Added htmx before transition reference link (#2929)
This is missing from the reference page but properly documented on the events page. This simply links the two together so anyone can quickly jump between the two.

Milestone: patch
2024-09-25 09:29:56 +02:00
Carson Gross
1ef814b0be Merge remote-tracking branch 'origin/master' 2024-09-22 20:39:18 -06:00
Carson Gross
9fd8aa80b5 lmao 2024-09-22 20:39:08 -06:00
gparmigiani
b4aff2340d add F# server-examples (#2886)
add F# server-examples.md
2024-09-22 12:07:17 -04:00
Jon Sterling
653794436b fix typo in rest-explained.md (#2924) 2024-09-22 12:02:40 -04:00
Carson Gross
b71af75f1a fix 2024-09-20 11:11:05 -06:00
Carson Gross
c2a29ce574 Merge remote-tracking branch 'origin/master' 2024-09-20 10:48:36 -06:00
Carson Gross
d61b000d73 add Tony's essay 2024-09-20 10:48:26 -06:00
Ben Croker
02aa8fd718 Fix docs for htmx.config.scrollBehavior (#2918)
* Update docs.md

* Update reference.md

* Update api.md

* Fix links

---------

Co-authored-by: Vincent <vichenzo-thebaud@hotmail.com>
2024-09-19 09:33:37 +02:00
Keeper-of-the-Keys
7f8105a905 [Documentation] Add more information about other swap strategies (#2889)
* Add more information about other swap strategies

* Change suggested by @MichaelWest22

* Grammar error as per comments from @Telroshan

* Clarification requested by @Telroshan
2024-09-18 09:22:43 +02:00
MichaelWest22
0023cd85f0 [Documentation] Config default update for methodsThatUseUrlParams (#2911)
Config default update
2024-09-16 10:09:38 +02:00
MichaelWest22
6ce6a1a77b Documentation 3xx redirects can't send headers (#2904)
* 3xx redirects can't send headers

* fix response-headers links
2024-09-15 09:29:39 +02:00
shouya
e6a2ea15a2 Documentation for dynamic hx-vals (#2898)
* add test for spread operator in hx-vals

* update documentation

Closes #2885
2024-09-13 10:19:21 +02:00
Carson Gross
2b8acfa2ca Merge remote-tracking branch 'origin/master' 2024-09-10 07:10:10 -06:00
Carson Gross
25f26127b9 link ACM paper 2024-09-10 07:10:01 -06:00
MichaelWest22
230262cf93 Documentation of svg oob swaps (#2882)
* Slipery SVGs

* remove a

* Fix typo and punctuation

* Also add code syntax for tags

---------

Co-authored-by: Vincent <vichenzo-thebaud@hotmail.com>
2024-09-08 10:21:56 +02:00
Jared Foy
73f529e3c3 Update hx-select.md (#2881) 2024-09-08 10:10:07 +02:00
Jared Foy
73388624ca Update CONTRIBUTING.md (#2875) 2024-09-06 10:29:44 +02:00
Carson Gross
7fc1d61b4f update sponsors 2024-09-02 14:59:20 -06:00
Carson Gross
1ef205da28 Merge remote-tracking branch 'origin/master' 2024-09-02 14:52:40 -06:00
Carson Gross
b25b911ba1 update sponsors 2024-09-02 14:52:32 -06:00
Ben Hoyt
be2e6b4970 Trivial grammar fixes (#2862) 2024-09-02 08:03:43 +02:00
Vincent
326ff3b296 Fix focusin-based tests (#2861) 2024-09-01 14:22:04 -04:00
Alexander Kulikov
324ee19377 Listen to resize events and check revealed (#2780) 2024-08-29 12:01:13 -06:00
Carson Gross
2855c2c24e merge https://github.com/bigskysoftware/htmx/pull/2723 2024-08-29 12:00:00 -06:00
Ben Croker
cd6cdb275e Ability to add options argument to event listener (#2836)
* Update htmx.js

* Update events.js

* Add fallback value

* Use JSDoc syntax

* Document parameter

* Only accept an object

* Revert change

* Add useCapture

* Update htmx.js

* Add `useCapture` test

* Clean up

* Revert addition of test
2024-08-29 10:36:24 -06:00
MichaelWest22
bc4468ddcd fix restoreHistory title replacment (#2841)
Co-authored-by: 1cg <469183+1cg@users.noreply.github.com>
2024-08-29 10:32:49 -06:00
MichaelWest22
2ba7fd280e Upgrade Typescript to move configuration from const to let (#2853)
* typescript upgrade v5.5.4

* fix dist
2024-08-29 10:30:51 -06:00
Carson Gross
0fd854758c fix formatting 2024-08-27 07:47:01 -06:00
Carson Gross
b4048ebb59 add moveBefore support 2024-08-23 15:20:01 -06:00
Vincent
d152a3c75a Redirect /extensions to extensions website (#2842) 2024-08-22 15:06:00 -06:00
Alexander Petros
4dfedf4f71 Fix duplicate typo (#2838)
Thanks!
2024-08-21 22:32:39 +02:00
Adam Johnson
bd2dc6564d Link to htmx:sendError from error description (#2792) 2024-08-20 23:25:23 -04:00
SimunKaracic
4d35ef51e1 Update server-examples.md (#2827)
Add scalatags + zio-http example
2024-08-20 23:21:19 -04:00
Aaron Cunningham
2472bcab98 Update lazy-load.md (#2826)
fixed double-spacing issue in example
2024-08-19 22:29:06 +02:00
ehenighan
27fc37ce50 Issue #2676 - Tests for v2 to prevent regression of issue from v1 (#2829)
* Tests for v2 to prevent regression of issue from v1

* Linting

---------

Co-authored-by: Ed Henighan <ed.henighan@adi-uk.com>
2024-08-19 21:09:42 +02:00
MichaelWest22
91901e38b8 fix responseHandling meta example (#2821) 2024-08-16 10:19:31 +02:00
Karol Skolasiński
e47c2f8c25 docs: add missing semicolons (#2820)
Co-authored-by: Karol Skolasiński <karol.skolasinski@7willows.com>
2024-08-16 10:02:50 +02:00
Carson Gross
bee498792c Merge remote-tracking branch 'origin/master' 2024-08-14 12:56:51 -06:00
Carson Gross
bb0ebf642b fix version 2024-08-14 12:56:44 -06:00
MichaelWest22
667636d098 fix cdn typo (#2814) 2024-08-14 10:29:05 +02:00
Carson Gross
eeaad206e8 update sha 2024-08-12 15:06:59 -06:00
Carson Gross
c0f80e65f9 update release date 2024-08-12 14:58:37 -06:00
Carson Gross
06d9f72f97 Merge remote-tracking branch 'origin/master' into dev 2024-08-12 14:57:04 -06:00
Saman Shahbazi
de98c40271 Fix typo in update-other-content.md (#2809)
fix typo
2024-08-11 10:27:50 +02:00
Adam Johnson
9bae00b698 Update installing download link to v2 (#2791) 2024-08-09 12:07:00 -04:00
ekzyis
f925e83b95 Add HTML comment and catch-all to responseHandling config via <meta> tag (#2793)
* Add HTML comment that was removed

* Add catch-all

---------

Co-authored-by: ekzyis <ekzyis@ekzy.is>
2024-08-09 11:49:24 -04:00
Serge Bornow
bb733a88f1 Update CONTRIBUTING.md - fixing a link (#2795)
* Update CONTRIBUTING.md  - fixing a link, wording

Excited to see this unframework framework as a long time (since 1998 coder) HTML appreciator. 

I wanted to learn about extensions but noticed a broken link that others might stumble upon. 

Consider rephrasing a question to invite a person to contribute their issue. (this can be ignored if you like).

* Update CONTRIBUTING.md

Removed change to grammar/style of writing back to original.
Providing a more updated reference to extensions with greater detail.
2024-08-09 17:23:45 +02:00
Carson Gross
b61077697d changelog for release 2024-08-05 14:07:57 -06:00
Carson Gross
0322b8782e release prep 2024-08-05 13:54:49 -06:00
Carson Gross
82355a741d release prep 2024-08-05 13:53:33 -06:00
Carson Gross
97b8c68dd3 release prep 2024-08-05 13:53:08 -06:00
Carson Gross
b1d6135dca add htmx:trigger for throttled events too 2024-08-05 13:48:37 -06:00
Kyungmin Bae
27412551a5 fix: Fire htmx:trigger event on delayed triggers (#2411)
* Add test on htmx:trigger for delayed triggers

* Fire htmx:trigger event on delayed triggers
2024-08-05 13:47:58 -06:00
Carson Gross
df3fd8fe28 Merge remote-tracking branch 'origin/dev' into dev 2024-08-05 13:46:28 -06:00
Carson Gross
42343d9194 Merge branch 'master' into dev 2024-08-05 13:41:37 -06:00
Carson Gross
eb32358873 add a catchall to the docs 2024-08-05 13:41:28 -06:00
ekzyis
e2b671d2f5 Fix example for responseHandling config via <meta> tag (#2715)
The current example throws this error:

  htmx.js:2914 SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
    at JSON.parse (<anonymous>)
    at parseJSON (htmx.js:816:19)
    at getMetaConfig (htmx.js:4902:14)
    at mergeMetaConfig (htmx.js:4909:24)
    at HTMLDocument.<anonymous> (htmx.js:4917:5)

Co-authored-by: ekzyis <ekzyis@ekzy.is>
2024-08-05 13:40:16 -06:00
pokonski
ee9b0e0390 Do not boost forms with method="dialog" (#2752)
* Do not boost forms with method="dialog"

* Clean up
2024-08-05 13:38:51 -06:00
Vincent
941e94fb98 Fix file upload through htmx.ajax (#2778)
* Fix File values handling in formDataFromObject

Fixes #2630

* Test file input upload + htmx.ajax file upload
2024-08-05 13:38:21 -06:00
Carson Gross
084df38c31 only removed templates explicitly used for encapsulating oob swaps
fixes https://github.com/bigskysoftware/htmx/issues/2776
2024-08-05 13:37:31 -06:00
Carson Gross
ce46e436fd handle situation where body has been deleted
fixes https://github.com/bigskysoftware/htmx/issues/2710
2024-08-05 13:18:03 -06:00
Carson Gross
89dc9bea2e Merge remote-tracking branch 'origin/dev' into dev 2024-08-05 13:12:22 -06:00
Carson Gross
0ace4a731c scan through all siblings (not just until the first non-element) when doing an outerHTML swap to add things to settle
fixes https://github.com/bigskysoftware/htmx/issues/2787
2024-08-05 13:12:13 -06:00
Carson Gross
df16ed8e96 manual test for keeping indicators visible 2024-08-05 12:29:33 -06:00
Carson Gross
e99d8976f5 Manual test to ensure disabled elements are scrubbed when saving history 2024-08-05 12:22:51 -06:00
M Somerville
13b44b7897 Fix inlineStyleNonce typo in reference (#2785) 2024-08-03 19:45:37 +02:00
Carson Gross
a575ad20f0 use attributes rather than request count since we are working on a clone of the original DOM 2024-08-01 13:44:31 -06:00
Carson Gross
115f2cf210 remove disabled attributes from anything disabled due to an htmx request when snapshotting for history 2024-08-01 12:59:42 -06:00
Carson Gross
116c8619d5 allow response actions (such as full page refreshes) to retain htmx indicators during the response handling 2024-08-01 12:56:24 -06:00
Jonas Högman
45d4bec43f Fix typo in update-other-content.md (#2783)
Co-authored-by: Jonas Hogman <jonas.hogman@seb.se>
2024-08-01 11:27:06 +02:00
Alexandre Spaeth
a30e96b553 Fix package.json configuration for types (#2769)
* Generate .d.ts file from the esm module (#2733)

* Fix types annotation filename in package.json (#2734)

---------

Co-authored-by: Alexandre Spaeth <alex_erson@users.noreply.github.com>
2024-07-29 17:54:06 -04:00
Ben Croker
a44a1b3c34 Add ability to trigger an event on another element using HX-Trigger response header (#2768)
* Update htmx.js

* Update htmx.js

* Update headers.js

* Update htmx.js

* Update htmx.js

* Update htmx.js

* Update hx-trigger.md
2024-07-29 10:33:24 -06:00
Jan-Niklas W.
0f70de7c0f docs: update jetbrains sponsor logo (#2551)
* docs: update jetbrains sponsor logo

* docs: support light and dark theme for jetbrains logo

---------

Co-authored-by: Jan-Niklas Wortmann <jan-niklas.wortmann@jetbrains.com>
2024-07-26 00:32:59 -04:00
Igor Berlenko
49b5dae073 fixed typo in hx-trigger.js (#2728)
Update hx-trigger.js
2024-07-26 00:27:22 -04:00
Alexandre Spaeth
6a800e8085 Generate .d.ts file from the esm module (#2733) (#2734)
Co-authored-by: Alexandre Spaeth <alex_erson@users.noreply.github.com>
2024-07-26 00:26:59 -04:00
Vincent
81b401a83d Fix: stringify objects when appending them to FormData (#2748) 2024-07-26 00:24:24 -04:00
Vincent
bec3657a81 Fix: values order with non-GET requests (#2749)
Fix values order with non-GET requests #2703
2024-07-26 00:22:59 -04:00
Daniel J. Summers
45566e126e Bump Zola version to 0.19.1 (#2739) 2024-07-17 22:07:07 -06:00
Bobae Kang
bc0eebfb3e Stop center aligning docs nav on small screen (#2740) 2024-07-17 20:09:04 -06:00
ASMag
4a2289d1ca Update README.md - fix url to htmx extensions (#2732)
Update README.md

fix url to htmx extensions
2024-07-17 08:58:39 +02:00
Ben Croker
1c1e4faab9 Fix default scrollBehavior value in docs (#2729)
* Update reference.md

* Update htmx.js

* Update reference.md
2024-07-16 08:18:11 +02:00
dependabot[bot]
7ce7b4878b Bump ws from 8.14.2 to 8.17.1 (#2718)
Bumps [ws](https://github.com/websockets/ws) from 8.14.2 to 8.17.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.14.2...8.17.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-13 14:17:06 -04:00
Johannes Przymusinski
cbb2b46de2 fix: type definitions for HtmxExtension (#2721) 2024-07-13 19:44:08 +02:00
Ronald Cantillo
1136d26353 fix: example buttons in darkmode (#2719) 2024-07-13 09:17:59 +02:00
Kyah Rindlisbacher
c8418332de Correct hx-delete doc-url to https://htmx.org/attributes/hx-delete/ (#2716) 2024-07-13 09:07:04 +02:00
Carson Gross
79e084542c update CHANGELOG.md 2024-07-12 11:21:54 -06:00
Carson Gross
2f38ac7c55 add sha as an npm command and update for release 2024-07-12 11:16:04 -06:00
Carson Gross
2925d2c30e update changelog 2024-07-12 11:14:29 -06:00
Carson Gross
b2791a7b62 update release 2024-07-12 11:13:32 -06:00
Carson Gross
c3ecbcc8d8 update release 2024-07-12 11:13:29 -06:00
Carson Gross
f751aba86f remove only top level files to keep the legacy extensions 2024-07-12 11:12:56 -06:00
Carson Gross
ee13ae744a Merge remote-tracking branch 'origin/dev' into dev 2024-07-12 11:12:20 -06:00
Kuberdenis
0f3b2b4a92 Let templates have plaintext as contents (fixes 2702) (#2708)
let templates have plaintext as contents (fixes 2702)
2024-07-12 11:11:42 -06:00
Carson Gross
f38e057397 Merge branch 'master' into dev 2024-07-11 18:50:16 -06:00
Peter Volf
89b9e2afa5 Added a FastAPI + TailwindCSS + DaisyUI example (#2695)
added a FastAPI + TailwindCSS + DaisyUI example
2024-07-08 11:50:42 -06:00
Christopher Jefferson
10f45c321e Clarify that hx-trigger:from doesn't dynamically update (#2696)
* Fix typo

* Clarity that 'from' doesn't update as the page changes
2024-07-08 11:50:17 -06:00
Bruno Alves
eb1b723ea7 #41540 Update Quarkus with recent examples (#2700)
Make sure Quarkus is referenced with recent examples in the htmx ecosystem #41540
2024-07-08 11:49:32 -06:00
Paul Garner
d9eb6d74f8 HtmxAjaxHelperContext typedef should have all optional fields (#2701)
* all fields of HtmxAjaxHelperContext should be optional

* update docs for htmx.ajax to describe purpose of source field in context
2024-07-08 11:49:10 -06:00
Sameer Dudeja
6c2f9ea939 Update beforeHistorySave documentation (#2692)
Issue : #2688
Update beforeHistorySave docs
2024-07-03 23:09:55 -04:00
Michael Lynch
5f7e2354b5 Document software requirements for developing htmx (#2664)
* Document software requirements for developing htmx

* Expand explanations
2024-06-27 13:51:50 -06:00
srcerer
f4f448db0a Update podcasts.csv (#2667)
Add
JavaScript Jabber htmx 2.0
Spiro Floropoulos
Software Unscripted
ThePrimeTime
TKYT
2024-06-27 13:50:40 -06:00
Carson Gross
52b357089d bump version and make the esm version "main" 2024-06-27 13:40:02 -06:00
Carson Gross
20e41c85eb Merge remote-tracking branch 'origin/dev' into dev
# Conflicts:
#	package-lock.json
2024-06-27 13:37:39 -06:00
Vincent
edac767fd9 Fix selfRequestsOnly doc (defaults to true in htmx 2) #2546 #2669 (#2674) 2024-06-27 13:36:31 -06:00
Sameer Dudeja
6f83885de3 Update in Documentation (#2655) 2024-06-20 11:19:10 -06:00
Alexander Petros
e1143de850 Generate .d.ts file in build script (#2653)
* Generate .d.ts file in build script

Resolves: #2629

* Streamline configuration options

Delete the JSConfig so we don't have to un-specify options when checking
vs generating (and also to remove a config file). Steamline the options
by re-using the NPM commands.

* Remove type generating from dev script
2024-06-20 11:18:47 -06:00
Adrian Wannenmacher
fa2dc6c02e fix minor mixup in the web security basics essay (#2652)
The current version refers to an HTTP response as a request. Fixed it.
2024-06-19 17:18:30 -04:00
Ben Croker
2f6a7f752e Docs: Revert detail.elt (#2649)
Revert `detail.elt`
2024-06-19 16:51:55 +02:00
Amr Ojjeh
dea45c84e7 Doc: Update oobBeforeSwap (#2647)
Co-authored-by: Ben Croker <57572400+bencroker@users.noreply.github.com>
2024-06-19 10:36:03 +02:00
Imbolc
f9b3f88357 Docs: Mention all extended selector keywords in hx-disabled-elt docs (#2544)
* Mention all extended selector keywords in `hx-disabled-elt` docs

* Expand possible values of the attribute similar to `hx-target`
2024-06-18 15:56:18 -06:00
pokonski
61e5fc1294 Make docs' content responsively wider (#2644)
* Make docs' content responsively wider

* Keep navigation static

* Remove unneeded class
2024-06-18 15:55:06 -06:00
Carson Gross
846a3d942f Merge remote-tracking branch 'origin/master' 2024-06-18 13:16:06 -06:00
Carson Gross
bd3032a724 update changelog to mention shadow DOM 2024-06-18 13:15:57 -06:00
Walt Chen
a1915882c9 docs: fix incorrect payload in update-other-content oob page (#2604) 2024-06-18 11:48:22 -06:00
Andre Sander
5a93241919 Remove duplicated "the" (#2621) 2024-06-18 11:46:45 -06:00
Ben Croker
ae56f211af Docs: Fix reference link to hx-inherit (#2625)
* Update reference.md

* Update reference.md
2024-06-18 11:46:20 -06:00
Paul Tuckey
5e8d8fa497 Add rust example to server-examples.md (#2626)
Add rust esample to server-examples.md
2024-06-18 11:45:58 -06:00
Meghan Denny
da5ede794f _index.md: remove malformed html fragment in sponsors table (#2631) 2024-06-18 11:45:31 -06:00
Guilherme J. Tramontina
a15fcafb8a fix 3rd party lib name in example doc (#2632) 2024-06-18 11:45:05 -06:00
pokonski
f27e3495bd Dark mode fixes (#2634)
Co-authored-by: 1cg <469183+1cg@users.noreply.github.com>
2024-06-18 11:44:45 -06:00
Sam Furr
5847fbf393 Fix: 2594 docs update (#2605)
* remove unnecessary escaping of double quotes

* add json-like syntax correction
2024-06-18 11:22:35 -06:00
Carson Gross
a328cab53a remove gremlin 2024-06-18 10:56:33 -06:00
Carson Gross
854629f271 type fix 2024-06-18 10:53:52 -06:00
Carson Gross
a8c8a43424 fix reference to non-existent UMD file in favor of CJS
fixes https://github.com/bigskysoftware/htmx/issues/2635
2024-06-18 10:41:40 -06:00
Carson Gross
d006d5c52e update the changelog
fixes https://github.com/bigskysoftware/htmx/issues/2638
2024-06-18 10:36:13 -06:00
Carson Gross
f3ae976aa2 fix outerHTML body swapping issue
fixes https://github.com/bigskysoftware/htmx/issues/2639
2024-06-18 10:28:53 -06:00
Carson Gross
d35222446d remove IE reference 2024-06-18 10:02:39 -06:00
Carson Gross
b932aff8b9 remove IE reference 2024-06-18 10:00:23 -06:00
Carson Gross
c6a89b315b fix date 2024-06-17 19:57:43 -06:00
Carson Gross
395e7065ac fix eslint 2024-06-17 12:56:33 -06:00
Carson Gross
59a1f00bba re-add extensions 2024-06-17 12:48:09 -06:00
Carson Gross
ad673ce0b0 fix base_url 2024-06-17 12:45:39 -06:00
Carson Gross
3c290f433b fix date 2024-06-17 12:44:17 -06:00
Carson Gross
32d15f5546 fix link 2024-06-17 12:43:21 -06:00
Carson Gross
f3c6b20728 improve docs 2024-06-17 12:39:54 -06:00
Carson Gross
f2d34f5405 2.0 release prep 2024-06-17 12:31:19 -06:00
Carson Gross
4952f619d8 2.0 release notes 2024-06-17 12:30:33 -06:00
Carson Gross
7bceca7a29 idk again 2024-06-17 12:26:39 -06:00
Carson Gross
106e3487a0 Merge branch 'dev' into 2.0 2024-06-17 12:16:53 -06:00
Carson Gross
6d3396c013 idk again 2024-06-17 12:16:39 -06:00
Carson Gross
9c747d767a Merge branch 'master' into dev
# Conflicts:
#	package-lock.json
#	www/content/_index.md
2024-06-17 12:16:01 -06:00
Carson Gross
387af88ea5 idk 2024-06-17 12:14:39 -06:00
Carson Gross
dd8b32aaad usgraphics bullied me into removing the border radius 2024-06-15 12:17:04 -06:00
Carson Gross
957401d447 add links to size & speed comparisons 2024-06-13 14:51:47 -06:00
Carson Gross
24c10cea39 update account name 2024-06-12 11:50:26 -06:00
Carson Gross
de9d1793d6 update link for deco 2024-06-12 11:33:41 -06:00
Carson Gross
5658b91d98 new sponsor 2024-06-11 19:17:45 -06:00
Carson Gross
d6afc5b8db fix double header issue 2024-06-07 09:13:45 -06:00
Carson Gross
15243b5d43 henning koch interview 2024-06-06 14:20:37 -06:00
Carson Gross
be89a5498f 2.0 announcement 2024-06-06 12:41:48 -06:00
Carson Gross
2ffa44824f Merge branch 'dev' into 2.0
# Conflicts:
#	www/content/_index.md
#	www/content/docs.md
2024-06-05 12:39:10 -06:00
Carson Gross
642b2d877d coverage critic sponsorship dark mode fix 2024-06-05 12:36:56 -06:00
Carson Gross
44d2d2b1dd Merge remote-tracking branch 'origin/dev' into dev 2024-06-05 12:34:59 -06:00
Carson Gross
2256a546e3 Merge branch 'master' into dev 2024-06-05 12:33:47 -06:00
Carson Gross
a378767987 coverage critic sponsorship 2024-06-05 08:01:46 -06:00
Carson Gross
4b3c3123fc coverage critic sponsorship 2024-06-05 07:27:21 -06:00
Carson Gross
0ab22904f5 history of hypermedia 2024-06-04 12:26:49 -06:00
Carson Gross
a5d7d1820f 2.0 docs scrub 2024-06-03 11:47:39 -06:00
Carson Gross
40b55c4a10 2.0 docs scrub 2024-06-03 11:01:45 -06:00
Carson Gross
14e557a9d1 Merge branch 'dev' into 2.0 2024-06-03 09:50:30 -06:00
Carson Gross
f6707b2317 Merge branch 'master' into dev 2024-06-03 09:49:25 -06:00
Carson Gross
4eadc8f492 deco sponsorship 2024-05-30 23:34:22 -06:00
pokonski
eeecde2270 Add Dark mode to the website (#2562)
* Add dark mode CSS

* Add dark mode variants of logos

* Use alternative Github star button with dark mode working

* Preserve github stars count when navigation to avoid flickers
2024-05-24 13:54:37 -06:00
Carson Gross
179b167ccf 2.0 release 2024-05-23 16:18:30 -06:00
Carson Gross
505a093ba7 2.0 release 2024-05-23 13:15:37 -06:00
Carson Gross
601d16c5ef fix links 2024-05-22 15:23:45 -06:00
Carson Gross
d7d1519be9 bump version 2024-05-22 15:05:06 -06:00
Carson Gross
ce446aadd9 announce beta4 2024-05-22 15:03:58 -06:00
Carson Gross
6944bb8040 prep htmx 2 beta4 2024-05-22 14:57:16 -06:00
Carson Gross
65ab27bb55 prep htmx 2 beta4 2024-05-22 14:57:07 -06:00
Carson Gross
039cf71a38 lint 2024-05-22 14:54:18 -06:00
Carson Gross
1e07206ed2 ignore empty files 2024-05-22 14:53:29 -06:00
Carson Gross
f35bf31a1a Merge branch 'master' into dev 2024-05-22 14:36:13 -06:00
Carson Gross
4bc7c9710e fix 2024-05-22 13:20:22 -06:00
Carson Gross
52994f9cad fix 2024-05-22 13:11:23 -06:00
Carson Gross
e692602042 fix 2024-05-22 13:09:22 -06:00
Carson Gross
7ce3bd7d51 fix 2024-05-22 12:01:58 -06:00
Carson Gross
c43cc285dc fix 2024-05-22 11:57:40 -06:00
Carson Gross
0232759b68 fix 2024-05-22 11:48:15 -06:00
Carson Gross
37332bb9bd fix 2024-05-22 11:46:20 -06:00
Carson Gross
278ad29f39 fix 2024-05-22 07:23:59 -06:00
Carson Gross
80ebc8856c htmx sucks, btw 2024-05-21 16:32:23 -06:00
Carson Gross
e8656a7594 fine, make it work on mobile 2024-05-21 07:16:59 -06:00
Carson Gross
da80ed13bf ads 2024-05-19 15:42:38 -06:00
Carson Gross
9e6bfdc006 join the uwu revolution again, again 2024-05-17 10:54:15 -06:00
Carson Gross
d226997187 join the uwu revolution again 2024-05-17 10:53:35 -06:00
Carson Gross
a90f8402ba join the uwu revolution 2024-05-17 10:00:50 -06:00
Carson Gross
12675fd1d4 prep beta4 2024-05-16 16:39:48 -06:00
Carson Gross
a216e1b799 fix type decl 2024-05-16 14:21:45 -06:00
Carson Gross
d84c2b7097 they see me lintin'
they hatin'
2024-05-16 14:18:51 -06:00
Carson Gross
8928efc40a restore tests and dynamic hx-on behavior 2024-05-16 14:16:43 -06:00
Carson Gross
20b42aaf88 Merge branch 'master' into dev
# Conflicts:
#	src/htmx.d.ts
#	src/htmx.js
#	test/core/security.js
#	www/content/docs.md
2024-05-16 12:50:13 -06:00
Carson Gross
dcb899dc90 Merge remote-tracking branch 'origin/master' 2024-05-15 13:40:51 -06:00
Viktor Szépe
31a0416368 Fix double-encoded UTF-8 characters (#2371)
* Fix double-encoded UTF-8 characters

* fix city name in active-search.md

https://en.wikipedia.org/wiki/Str%C3%A9e
2024-05-15 12:20:18 -06:00
Antoine Aumjaud
d37fe551a1 📚 fix code sample in documentation (#2498) 2024-05-15 12:11:08 -06:00
Andrew Arderne
c225c8dff1 (website) Fix url to github stars (#2521)
fix url to github stars
2024-05-15 12:08:58 -06:00
Marcos Pereira
ab458e7143 feat: Add inlineSlyeNonce configuration (#2542) 2024-05-15 12:06:40 -06:00
Carson Gross
099ed97277 join the uwu revolution 2024-05-15 11:51:47 -06:00
Carson Gross
73948e2a65 join the uwu revolution 2024-05-15 11:51:32 -06:00
Carson Gross
57595bc039 Merge remote-tracking branch 'origin/master' 2024-04-29 11:24:36 -06:00
Carson Gross
b3eaadb5d3 update sponsors 2024-04-29 11:23:13 -06:00
Denis Palashevskii
dc93911566 Add tests for getSelectors method for extension registration (#2508)
add tests for getSelectors method for extension registration
2024-04-25 13:45:33 -06:00
Denis Palashevskii
45d45c30af [2.0] Improve extension registration logic (#2505)
* add getSelectors to extension contract and extensionEnabled() to internal API

* remove extensionEnabled method (it's useless)
2024-04-25 11:39:59 -06:00
Lloyd Lobo
b176784f31 docs: fix this binding in onClick .then handler with arrow function (#2373)
docs: fix `this` binding in onClick .then handler with arrow fn

 Update the `.then()` handler in the `onClick` attribute to use an arrow function instead of a traditional `function(arg)` expression. 

This fixes the `this` binding within the lexical scope, ensuring that `this` is bound to the `button` instead of the `window` and resolving the issue.

Fixes/Closes issue  #2257
2024-04-25 12:47:21 -04:00
lookbusy1344
f9591c9790 More explicit explanation of htmx.config.getCacheBusterParam (#2376)
* More explicit explanation of htmx.config.getCacheBusterParam

* Small formatting fixes
2024-04-25 12:36:50 -04:00
Nico Ekkart
af50cde104 docs: aria-selected true in Tabs HATEOS example (#2404)
The `aria-selected` attribute should be set to true in the first tab for consistency with the class `selected` being set.
2024-04-25 12:34:38 -04:00
noman
998bd7cd4c fix(docs): Add missing apostrophes (#2405)
Add apostrophes
2024-04-25 12:34:15 -04:00
Andrej Ota
c073aa7746 Fix a trivial typo in CONTRIBUTING.md. (#2436) 2024-04-25 12:33:46 -04:00
rafkub
cc9d3f063c Update confirm.md (#2443)
Fix typo - repeated "and the"
2024-04-25 12:33:28 -04:00
rafkub
ce5bb337aa Update hx-indicator.md (#2444)
Fix typo: "add this the" -> "add this to the"
2024-04-25 12:33:14 -04:00
André Schreck
5c24e42e46 use same ID throughout the example (#2471) 2024-04-25 12:32:55 -04:00
Vincent
f0bd28b438 Remove Node 15 requirement mention from README (#2494) 2024-04-25 12:32:34 -04:00
Goksan
577c651871 Add Statusnook to the webring (#2506) 2024-04-25 12:32:15 -04:00
srcerer
3d7ecfb8d8 Update podcasts.csv (#2500)
Add The Collab Lab
2024-04-25 12:31:58 -04:00
Carson Gross
b991f20c3a make hx-on respect the hx-disable attribute 2024-04-25 09:49:50 -06:00
Carson Gross
3d099e7b1d update 2024-04-23 14:42:32 -06:00
Carson Gross
db86fa981b fix site 2024-04-19 16:43:38 -06:00
Carson Gross
74744ac337 prep for 2.0.0-beta3 release 2024-04-17 11:04:49 -06:00
Carson Gross
6bff809c13 Merge branch 'master' into dev 2024-04-17 10:58:46 -06:00
Carson Gross
adfd2c8bc8 fix sha for unminified version 2024-04-17 10:58:31 -06:00
Carson Gross
e90876fbd8 Merge branch 'master' into dev
# Conflicts:
#	dist/htmx.js
#	dist/htmx.min.js
#	dist/htmx.min.js.gz
#	package.json
#	src/htmx.js
#	www/content/extensions/_index.md
#	www/content/extensions/ajax-header.md
#	www/content/extensions/alpine-morph.md
#	www/content/extensions/class-tools.md
#	www/content/extensions/client-side-templates.md
#	www/content/extensions/debug.md
#	www/content/extensions/disable-element.md
#	www/content/extensions/event-header.md
#	www/content/extensions/head-support.md
#	www/content/extensions/include-vals.md
#	www/content/extensions/json-enc.md
#	www/content/extensions/loading-states.md
#	www/content/extensions/method-override.md
#	www/content/extensions/morphdom-swap.md
#	www/content/extensions/multi-swap.md
#	www/content/extensions/path-deps.md
#	www/content/extensions/path-params.md
#	www/content/extensions/preload.md
#	www/content/extensions/remove-me.md
#	www/content/extensions/response-targets.md
#	www/content/extensions/restored.md
#	www/content/extensions/server-sent-events.md
#	www/content/extensions/web-sockets.md
#	www/static/src/htmx.js
#	www/static/test/core/ajax.js
#	www/static/test/core/regressions.js
#	www/themes/htmx-theme/static/js/htmx.js
2024-04-17 10:56:31 -06:00
Carson Gross
7a6dd3f38a Merge remote-tracking branch 'origin/master' 2024-04-17 10:55:17 -06:00
Carson Gross
f38e07d4be prep v1.9.12 2024-04-17 10:54:49 -06:00
Carson Gross
7dd6cd7224 prep v1.9.12 2024-04-17 10:48:55 -06:00
Vincent
54381a2bf7 Fix #2317 force conversion to FormData for xhr.send (#2481)
Couldn't get the proxy object to work properly with XMLHTTPRequest's send method.
Even though calls are being forwarded to its underlying FormData through the proxy, the request won't set its type to multipart/form-data when appropriate and instead send a text/plain request
2024-04-17 05:53:07 -06:00
Carson Gross
2be7054525 lint 2024-04-17 05:45:40 -06:00
Carson Gross
01cb5e0d8d support hx-on in shadowroot 2024-04-17 05:25:28 -06:00
Carson Gross
81afe922d7 Merge branch 'master' into dev
# Conflicts:
#	www/content/extensions/client-side-templates.md
2024-04-16 14:40:31 -06:00
Denis Palashevskii
8e26d12c33 Add reference to unminified CDN distribution to the docs (#2460)
fix #2457
2024-04-03 14:59:07 -06:00
Carson Gross
c247cae9bf Merge remote-tracking branch 'origin/master' 2024-03-27 09:38:13 -06:00
Carson Gross
b0ffe98011 sponsor update 2024-03-27 09:38:02 -06:00
Carson Gross
45dd5f168d move all references to extensions over to the new site/repo 2024-03-26 16:37:26 -06:00
Carson Gross
30a9941b61 clean our references to extensions, link to the new extensions website 2024-03-26 16:00:15 -06:00
Carson Gross
6da79f2e32 restore htmx 1.x extensions for CDN compatibility 2024-03-26 15:58:10 -06:00
Carson Gross
0aed416dd6 Merge remote-tracking branch 'origin/dev' into dev 2024-03-26 15:56:50 -06:00
Carson Gross
ab4e9deabc remove manual tests for extensions 2024-03-26 15:56:40 -06:00
Alexander Petros
2f0de8bdbe Switch to default export for htmx 2 (#2428) 2024-03-24 16:11:45 -06:00
Carson Gross
cd4e6c62cf clean up bad IE references 2024-03-24 14:39:19 -06:00
airblast
075ec3afdb Fix example for mustache in client-side-templates extension. (#2409)
* Fix incorrect key name in template.

* Remove unnecessary escapes from docs.
2024-03-23 11:46:57 -04:00
Pi-Cla
0b29664545 Link to section on CSS Transitions instead of Mozilla page (#2413) 2024-03-23 11:46:28 -04:00
Carson Gross
04802e0397 fix deploy 2024-03-22 12:13:33 -06:00
Carson Gross
d92f165923 lint 2024-03-21 17:35:17 -06:00
Carson Gross
72666d0b4a remove base URL 2024-03-21 17:28:59 -06:00
Carson Gross
038e1a78f0 Merge branch 'v2.0v2.0' into dev
# Conflicts:
#	dist/htmx.js
#	dist/htmx.min.js
#	dist/htmx.min.js.gz
#	src/htmx.js
#	test/core/ajax.js
#	test/core/regressions.js
#	www/static/src/htmx.js
#	www/themes/htmx-theme/static/js/htmx.js
2024-03-21 17:28:09 -06:00
Carson Gross
e3811cf744 set root 2024-03-21 17:01:40 -06:00
Vincent
e64238dba3 Fix IE11 incompatibilities (#2408) 2024-03-21 16:06:44 -06:00
Carson Gross
bf69273701 beta2 2024-03-21 16:02:23 -06:00
Denis Palashevskii
3c8bcf55e0 Ws fix hx vals number handling (#2418)
return JS object from `getExpressionVars`
2024-03-21 15:58:09 -06:00
Carson Gross
cb96f08442 remove extensions from 2.0 2024-03-21 15:43:34 -06:00
Carson Gross
ddb7597629 add warning if people aren't using the correct version of extensions 2024-03-21 15:41:17 -06:00
eduardolat
c43d48163f Add documentation for multiple CSS selectors in hx-disabled-elt (#2421)
This update clarifies the hx-disabled-elt attribute's ability to accept multiple CSS selectors, separated by commas, enabling developers to disable multiple elements simultaneously during an HTTP request.
2024-03-21 15:25:49 -06:00
Godefroid Chapelle
8318d9af67 Fix date in CHANGELOG.md (#2419) 2024-03-21 15:25:26 -06:00
Carson Gross
9b1e9bc336 update sha 2024-03-15 08:53:37 -06:00
Carson Gross
f353023b01 fix previous firefox double exec fix fml 2024-03-14 20:33:20 -06:00
Carson Gross
ea3beb6f45 firefox double exec fix fml 2024-03-14 19:41:57 -06:00
Carson Gross
3205e652ee make install URLs versioned for extensions 2024-03-14 16:53:41 -06:00
539 changed files with 32309 additions and 100386 deletions

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ _site
test/scratch/scratch.html
.DS_Store
.vscode
/coverage

View File

@@ -1,6 +1,108 @@
# Changelog
## [1.9.11] - 2024-04-15
## [2.0.8] - 2025-10-24
* [Updated](https://github.com/bigskysoftware/htmx/commit/b9336a96fbdcf28550699971dc2218a90c7a4e01) `parseHTML` to use to use the (unfortunately named) [`Document.parseHTMLUnsafe()`](https://developer.mozilla.org/en-US/docs/Web/API/Document/parseHTMLUnsafe_static)
method for better Web Components support
* [Added](https://github.com/bigskysoftware/htmx/commit/83a1449a89b1fcd1c1655039ede02d74d61e4800) `pushURL` option to the `htmx.ajax()` API
* [Fixed](https://github.com/bigskysoftware/htmx/commit/cd045c3e0eb31776a80e3a4b4c74e37d0631c1f1) `hx-sync` and `htmx:abort` within the Shadow Dom [Issue 3419](https://github.com/bigskysoftware/htmx/issues/3419)
* [Fixed](https://github.com/bigskysoftware/htmx/commit/04d6c7249b7fd7b8518ddca92e7a70fdcc651b34) a long-standing bug in history support with respect to relative paths [Issue 3449](https://github.com/bigskysoftware/htmx/issues/3449)
* Once again, this is a release mainly done by @MichaelWest22's heroic work, thank you!
## [2.0.7] - 2025-09-08
* Fix not preventing link when inside htmx enabled element (fixes https://github.com/bigskysoftware/htmx/issues/3395)
* Implement `reportValidity()` for reporting proper form validation errors behind config flag (fixes https://github.com/bigskysoftware/htmx/issues/2372)
* Update indicator style to have `visibility:hidden` for screen readers (fixes https://github.com/bigskysoftware/htmx/issues/3354)
* Bugfix: swap="outerHTML" on <div> with style attribute leaves htmx-swapping class behind (see https://github.com/bigskysoftware/htmx/pull/3341)
## [2.0.6] - 2025-06-27
* [Fix](https://github.com/bigskysoftware/htmx/pull/3357) a [regression](https://github.com/bigskysoftware/htmx/issues/3356)
with htmx-powered links that contain other elements in them issuing full page refreshes
## [2.0.5] - 2025-06-20
* 100% test coverage! (Thank you @MichaelWest22!)
* The default recommended CDN is now jsDelivr
* The `inherit` keyword is now supported by `hx-include`, `hx-indicator` and `hx-disabled-elt` to allow you to inherit
the value from a parent and extend it.
* `hx-on` listeners are now added before processing nodes so events during processing can be captured
* Using `<button hx-verb="/endpoint" type="reset">` will now reset the associated form (after submitting to `/endpoint`)
* Using `<button formmethod="dialog">` will no longer submit its associated form
* Local history cache now uses `sessionStorage` rather than `localStorage` so cross-tab contamination doesn't occur
* History restoration now follows the standard swapping code paths
* Many other smaller bug and documentation fixes
## [2.0.4] - 2024-12-13
* Calling `htmx.ajax` with no target or source now defaults to body (previously did nothing)
* Nested [shadow root](https://github.com/bigskysoftware/htmx/commit/5ab508f6523a37890932176f7dc54be9f7a281ff) fix
* The `htmx:trigger` event now properly fires on the synthetic `load` event
* The synthetic `load` event will not be re-called when an element is reinitialized via `htmx.process()`
* Boosted `<form>` tags that issue a `GET` with no or empty `action` attributes will properly replace all existing query
parameters
* Events that are triggered on htmx-powered elements located outside a form, but that refer to a form via the`form`
attribute, now properly cancel the submission of the referred-to form
* Extension Updates
* `preload` extension was
[completely reworked](https://github.com/bigskysoftware/htmx-extensions/commit/fb68dfb48063505d2d7420d717c24ac9a8dae244)
by @marisst to be compatible with `hx-boost`, not cause side effect, etc. Thank you!
* `response-targets` was updated to not use deprecated methods
* A [small fix](https://github.com/bigskysoftware/htmx-extensions/commit/e87e1c3d0bf728b4e43861c7459f3f937883eb99) to
`ws` to avoid an error when closing in some cases
* The `head-support` extension was updated to work with the `sse` extension
## [2.0.3] - 2024-10-03
* Added support for the experimental `moveBefore()` functionality in [Chrome Canary](https://www.google.com/chrome/canary/),
see the [demo page](/examples/move-before) for more information.
* Fixed `revealed` event when a resize reveals an element
* Enabled `hx-preserve` in oob-swaps
* Better degredation of `hx-boost` on forms with query parameters in their `action`
* Improved shadowRoot support
* Many smaller bug fixes
* Moved the core extension documentation back to <https://htmx.org/extensions>
## [2.0.2] - 2024-08-12
* no longer boost forms of type `dialog`
* properly trigger the `htmx:trigger` event when an event is delayed or throttled
* file upload is now fixed
* empty templates that are not used for oob swaps are no longer removed from the DOM
* request indicators are not removed when a full page redirect or refresh occurs
* elements that have been disabled for a request are properly re-enabled before snapshotting for history
* you can now trigger events on other elements using the `HX-Trigger` response header
* The `.d.ts` file should now work properly
## [2.0.1] - 2024-07-12
* Make the `/dist/htmx.esm.js` file the `main` file in `package.json` to make installing htmx smoother
* Update `htmx.d.ts` & include it in the distribution
* A fix to avoid removing text-only templates on htmx cleanup
* A fix for outerHTML swapping of the `body` tag
* Many docs fixes
## [2.0.0] - 2024-06-17
* Removed extensions and moved to their own repos linked off of <https://extensions.htmx.org>
* The website now supports dark mode! (Thanks [@pokonski](https://github.com/pokonski)!)
* The older, deprecated [SSE & WS](https://v1.htmx.org/docs/#websockets-and-sse) attributes were removed
* Better support for [Web Components & Shadow DOM](https://htmx.org/examples/web-components/)
* HTTP `DELETE` requests now use parameters, rather than form encoded bodies, for their payload (This is in accordance w/ the spec.)
* Module support was split into different files:
* We now provide specific files in `/dist` for the various JavaScript module styles:
* ESM Modules: `/dist/htmx.esm.js`
* AMD Modules: `/dist/htmx.amd.js`
* CJS Modules: `/dist/htmx.cjs.js`
* The `/dist/htmx.js` file continues to be browser-loadable
* The `hx-on` attribute, with its special syntax, has been removed in favor of the less-hacky `hx-on:` syntax.
* See the [Upgrade Guide](https://htmx.org/migration-guide-htmx-1/) for more details on upgrade steps
* The `selectAndSwap()` internal API method was replaced with the public (and much better) [`swap()`](/api/#swap) method
## [1.9.12] - 2024-04-17
* [IE Fixes](https://github.com/bigskysoftware/htmx/commit/e64238dba3113c2eabe26b1e9e9ba7fe29ba3010)
## [1.9.11] - 2024-03-15
* Fix for new issue w/ web sockets & SSE on iOS 17.4 (thanks apple!)
* Fix for double script execution issue when using template parsing

View File

@@ -3,20 +3,43 @@ Thank you for your interest in contributing! Because we're a small team, we have
## Issues
1. Issues are the best place to propose a new feature. Keep in mind that htmx is a small library, so there are lots of great ideas that don't fit in the core; it's always best to check in about an idea before doing a bunch of work on it.
1. When proposing a new features, we will often suggest that you implement it as an [extension](https://htmx.org/extensions), so try that first. Even if we don't end up supporting it officially, you can publish it yourself and we can link to it.
1. When proposing a new feature, we will often suggest that you implement it as an [extension](https://github.com/bigskysoftware/htmx-extensions), so try that first. Even if we don't end up supporting it officially, you can publish it yourself and we can link to it.
1. Search the issues before proposing a feature to see if it is already under discussion. Referencing existing issues is a good way to increase the priority of your own.
1. We don't have an issue template yet, but the more detailed your description of the issue, the more quickly we'll be able to evaluate it.
1. See an issue that you also have? Give it a reaction (and comment, if you have something to add). We note that!
1. If you haven't gotten any traction on an issue, feel free to bump it in the #issues-and-pull-requests channel on our Discord.
1. Want to contribute but don't know where to start? Look for issues with the "help wanted" tag.
## Creating a Development Environment
### Pre-requisites
To create a development environment for htmx, you'll need the following tools on your system:
- Node.js 20.x or later
- Chrome or Chromium
Additionally, the environment variable `CHROME_PATH` must contain the full path to the Chrome or Chromium binary on your system.
### Installing Packages
To install htmx's required packages, run the following command:
```bash
npm install
```
### Running Automated Tests
To verify that your htmx environment is working correctly, you can run htmx's automated tests with the following command:
```bash
npm test
```
## Pull Requests
### Technical Requirements
1. Code, including tests, must be written in ES5 for [IE 11 compatibility](https://stackoverflow.com/questions/39902809/support-for-es6-in-internet-explorer-11).
1. Please lint all proposed changes with the `npm run format` command
1. All PRs must be made against the `dev` branch, except documentation PRs (that only modify the `www/` directory) which can be made against `master`.
1. Please avoid sending the `dist` files along your PR, only include the `src` ones.
1. Please include test cases in [`/test`](https://github.com/bigskysoftware/htmx/tree/dev/test) and docs in [`/www`](https://github.com/bigskysoftware/htmx/tree/dev/www).
1. We squash all PRs, so you're welcome to submit with as many commits are you like; they will be evaluated as a single, standalone change.
1. We squash all PRs, so you're welcome to submit with as many commits as you like; they will be evaluated as a single, standalone change.
### Review Guidelines
1. Open PRs represent issues that we're actively thinking working on merging (at a pace we can manage). If we think a proposal needs more discussion, or that the existing code would require a lot of back-and-forth to merge, we might close it and suggest you make an issue.

View File

@@ -10,15 +10,14 @@
## introduction
htmx allows you to access [AJAX](https://htmx.org/docs#ajax), [CSS Transitions](https://htmx.org/docs#css_transitions),
[WebSockets](https://htmx.org/docs#websockets) and [Server Sent Events](https://htmx.org/docs#sse)
[WebSockets](https://htmx.org/extensions/ws/) and [Server Sent Events](https://htmx.org/extensions/sse/)
directly in HTML, using [attributes](https://htmx.org/reference#attributes), so you can build
[modern user interfaces](https://htmx.org/examples) with the [simplicity](https://en.wikipedia.org/wiki/HATEOAS) and
[power](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) of hypertext
htmx is small ([~14k min.gz'd](https://unpkg.com/htmx.org/dist/)),
[dependency-free](https://github.com/bigskysoftware/htmx/blob/master/package.json),
[extendable](https://htmx.org/extensions) &
IE11 compatible
htmx is small ([~14k min.gz'd](https://cdn.jsdelivr.net/npm/htmx.org/dist/)),
[dependency-free](https://github.com/bigskysoftware/htmx/blob/master/package.json) &
[extendable](https://htmx.org/extensions)
## motivation
@@ -33,7 +32,7 @@ By removing these arbitrary constraints htmx completes HTML as a
## quick start
```html
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
<!-- have a button POST a click via AJAX -->
<button hx-post="/clicked" hx-swap="outerHTML">
Click Me
@@ -70,8 +69,6 @@ No time? Then [become a sponsor](https://github.com/sponsors/bigskysoftware#spon
To develop htmx locally, you will need to install the development dependencies.
__Requires Node 15.__
Run:
```
@@ -102,8 +99,6 @@ At this point you can modify `/src/htmx.js` to add features, and then add tests
htmx uses the [mocha](https://mochajs.org/) testing framework, the [chai](https://www.chaijs.com/) assertion framework
and [sinon](https://sinonjs.org/releases/v9/fake-xhr-and-server/) to mock out AJAX requests. They are all OK.
You can also run live tests and demo of the WebSockets and Server-Side Events extensions with `npm run ws-tests`
## haiku
*javascript fatigue:<br/>

15
SECURITY.md Normal file
View File

@@ -0,0 +1,15 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 2.x | :white_check_mark: |
| 1.9.x | :white_check_mark: |
| < 1.9 | :x: |
## Reporting a Vulnerability
If you think you've found a vulnerability, please use the _Report a vulnerability_ button found in the [security tab](https://github.com/bigskysoftware/htmx/security) of the project on Github.
This process is documented in GitHub's _Secure Coding_ guide: [Privately reporting a security vulnerability](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability).

75
TESTING.md Normal file
View File

@@ -0,0 +1,75 @@
# HTMX Testing Guide
This guide outlines how to test htmx, focusing on running tests headlessly or in a browser environment, running individual tests, and other testing concerns.
## Prerequisites
1. Ensure you have a currently supported Node.js and npm installed.
2. Install dependencies by running:
```bash
npm install
npm run test
```
During test runs it will auto install playwright
## Running All Tests
To run all tests in headless mode, execute:
```bash
npm test
```
This will run all the tests using headless Chrome.
To run all tests against all browsers in headless mode, execute:
```bash
npm run test:all
```
This will run the tests using Playwrights headless browser setup across Chrome, Firefox, and WebKit (Safari-adjacent).
To run all tests against a specific browser, execute:
```bash
npm run test:chrome
npm run test:firefox
npm run test:webkit
```
## Running Individual Tests
### Headless Mode
To run a specific test file headlessly, for example `test/core/ajax.js`, use the following command:
```bash
npm test test/core/ajax.js
```
If you want to run only one specific test, you can temporarily change `it("...` to `it.only("...` in the test file, and then specify the test file as above. Don't forget to undo this before you commit! You will get eslint warnings now to let you know when you have temporary `.only` in place to help avoid commiting these.
### Browser Mode
To run tests directly in the browser, simply `open test/index.html` in a browser.
On Ubuntu you can run:
```bash
xdg-open test/index.html
```
This runs all the tests in the browser using Mocha instead of web-test-runner for easier and faster debugging.
From the Mocha browser view you can rerun a just a single test file by clicking the header name or you can click on the play icon to re-play a single test. This makes it easy to update this test/code and refresh to re-run this single test. The browser console also now logs the names of the running tests so you can check here to find any errors or logs produced during each test execution. Adding debugger statements in your code or breakpoints in the browser lets you step though the test execution.
If you really want to open web-test-runner in headed mode, you can run:
```bash
npm run test:debug
```
This will start the server, and open the test runner in a browser. From there you can choose a test file to run. Note that all test logs will show up only in dev tools console unlike Mocha.
## Code Coverage Report
Lines of code coverage reporting will only work when running the default chrome headless testing
After a test run completes, you can open `coverage/lcov-report/index.html` to view the code coverage report. On Ubuntu you can run:
```bash
xdg-open coverage/lcov-report/index.html
```
## Test Locations
- All tests are located in the `test/attribues` and `test/core` directories. Only .js files in these directory will be discovered by the test runner.
- The `web-test-runner.config.mjs` file in the root directory contains the boilerplate HTML for the test runs, including `<script>` tags for the test dependencies.
### Local CI prediction
You can run `npm run test:ci` to locally simulate the result of the CI run. This is useful to run before pushing to GitHub to avoid fixup commits and CI reruns.

2
dist/ext/README.md vendored
View File

@@ -4,6 +4,6 @@ These are legacy extensions for htmx 1.x and are **NOT** actively maintained or
They are here because we unfortunately linked to unversioned unpkg URLs in the installation guides for them
in 1.x, so we need to keep them here to preserve those URLs and not break existing users functionality.
If you are looking for extensions for htmx 2.x, please see the [htmx 2.0 extensions site](https://extensions.htmx.org),
If you are looking for extensions for htmx 2.x, please see the [htmx 2.0 extensions site](https://htmx.org/extensions),
which has links to the new extensions repos (They have all been moved to their own NPM projects and URLs, like
they should have been from the start!)

View File

@@ -1,7 +1,11 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('ajax-header', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
evt.detail.headers['X-Requested-With'] = 'XMLHttpRequest';
}
}
});
});

View File

@@ -1,3 +1,7 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('alpine-morph', {
isInlineSwap: function (swapStyle) {
return swapStyle === 'morph';
@@ -13,4 +17,4 @@ htmx.defineExtension('alpine-morph', {
}
}
}
});
});

View File

@@ -1,5 +1,10 @@
(function () {
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
function splitOnWhitespace(trigger) {
return trigger.split(/\s+/);
}
@@ -89,4 +94,4 @@
}
}
});
})();
})();

View File

@@ -1,3 +1,7 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('client-side-templates', {
transformResponse : function(text, xhr, elt) {

4
dist/ext/debug.js vendored
View File

@@ -1,3 +1,7 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('debug', {
onEvent: function (name, evt) {
if (console.debug) {

View File

@@ -1,5 +1,7 @@
"use strict";
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
// Disable Submit Button
htmx.defineExtension('disable-element', {
onEvent: function (name, evt) {
@@ -15,4 +17,4 @@ htmx.defineExtension('disable-element', {
}
}
}
});
});

View File

@@ -1,4 +1,8 @@
(function(){
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
function stringifyEvent(event) {
var obj = {};
for (var key in event) {

View File

@@ -5,6 +5,11 @@
//==========================================================
(function(){
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
var api = null;
function log() {
@@ -138,4 +143,4 @@
}
});
})()
})()

View File

@@ -1,4 +1,8 @@
(function(){
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
function mergeObjects(obj1, obj2) {
for (var key in obj2) {

View File

@@ -1,12 +1,16 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('json-enc', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
evt.detail.headers['Content-Type'] = "application/json";
}
},
encodeParameters : function(xhr, parameters, elt) {
xhr.overrideMimeType('text/json');
return (JSON.stringify(parameters));
}
});
});

View File

@@ -1,4 +1,10 @@
;(function () {
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
let loadingStatesUndoQueue = []
function loadingStateContainer(target) {

View File

@@ -1,3 +1,7 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('method-override', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {

View File

@@ -1,3 +1,7 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('morphdom-swap', {
isInlineSwap: function(swapStyle) {
return swapStyle === 'morphdom';

View File

@@ -1,5 +1,10 @@
(function () {
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
/** @type {import("../htmx").HtmxInternalApi} */
var api;
@@ -42,4 +47,4 @@
}
}
});
})();
})();

17
dist/ext/path-deps.js vendored
View File

@@ -1,9 +1,12 @@
(function(undefined){
'use strict';
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
// Save a reference to the global object (window in the browser)
var _root = this;
function dependsOn(pathSpec, url) {
if (pathSpec === "ignore") {
return false;
@@ -30,8 +33,8 @@
if (dependsOn(elt.getAttribute('path-deps'), path)) {
htmx.trigger(elt, "path-deps");
}
}
}
}
}
htmx.defineExtension('path-deps', {
onEvent: function (name, evt) {
@@ -41,7 +44,7 @@
if (config.verb !== "get" && evt.target.getAttribute('path-deps') !== 'ignore') {
refreshPath(config.path);
}
}
}
}
});
@@ -49,12 +52,12 @@
* ********************
* Expose functionality
* ********************
*/
*/
_root.PathDeps = {
refresh: function(path) {
refreshPath(path);
}
};
}).call(this);

View File

@@ -1,3 +1,7 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('path-params', {
onEvent: function(name, evt) {
if (name === "htmx:configRequest") {
@@ -8,4 +12,4 @@ htmx.defineExtension('path-params', {
})
}
}
});
});

24
dist/ext/preload.js vendored
View File

@@ -1,5 +1,9 @@
// This adds the "preload" extension to htmx. By default, this will
// preload the targets of any tags with `href` or `hx-get` attributes
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
// This adds the "preload" extension to htmx. By default, this will
// preload the targets of any tags with `href` or `hx-get` attributes
// if they also have a `preload` attribute as well. See documentation
// for more details
htmx.defineExtension("preload", {
@@ -18,8 +22,8 @@ htmx.defineExtension("preload", {
if (node == undefined) {return undefined;}
return node.getAttribute(property) || node.getAttribute("data-" + property) || attr(node.parentElement, property)
}
// load handles the actual HTTP fetch, and uses htmx.ajax in cases where we're
// load handles the actual HTTP fetch, and uses htmx.ajax in cases where we're
// preloading an htmx resource (this sends the same HTTP headers as a regular htmx request)
var load = function(node) {
@@ -43,7 +47,7 @@ htmx.defineExtension("preload", {
}
// Special handling for HX-GET - use built-in htmx.ajax function
// so that headers match other htmx requests, then set
// so that headers match other htmx requests, then set
// node.preloadState = TRUE so that requests are not duplicated
// in the future
var hxGet = node.getAttribute("hx-get") || node.getAttribute("data-hx-get")
@@ -57,8 +61,8 @@ htmx.defineExtension("preload", {
return;
}
// Otherwise, perform a standard xhr request, then set
// node.preloadState = TRUE so that requests are not duplicated
// Otherwise, perform a standard xhr request, then set
// node.preloadState = TRUE so that requests are not duplicated
// in the future.
if (node.getAttribute("href")) {
var r = new XMLHttpRequest();
@@ -83,16 +87,16 @@ htmx.defineExtension("preload", {
if (node.preloadState !== undefined) {
return;
}
// Get event name from config.
var on = attr(node, "preload") || "mousedown"
const always = on.indexOf("always") !== -1
if (always) {
on = on.replace('always', '').trim()
}
// FALL THROUGH to here means we need to add an EventListener
// Apply the listener to the node
node.addEventListener(on, function(evt) {
if (node.preloadState === "PAUSE") { // Only add one event listener

View File

@@ -1,3 +1,7 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('rails-method', {
onEvent: function (name, evt) {
if (name === "configRequest.htmx") {

View File

@@ -1,4 +1,8 @@
(function(){
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
function maybeRemoveMe(elt) {
var timing = elt.getAttribute("remove-me") || elt.getAttribute("data-remove-me");
if (timing) {

View File

@@ -1,5 +1,10 @@
(function(){
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
/** @type {import("../htmx").HtmxInternalApi} */
var api;
@@ -12,7 +17,7 @@
/**
* @param {HTMLElement} elt
* @param {number} respCode
* @param {number} respCodeNumber
* @returns {HTMLElement | null}
*/
function getRespCodeTarget(elt, respCodeNumber) {
@@ -58,7 +63,7 @@
}
}
}
return null;
}

View File

@@ -1,3 +1,7 @@
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
htmx.defineExtension('restored', {
onEvent : function(name, evt) {
if (name === 'htmx:restored'){
@@ -12,4 +16,4 @@ htmx.defineExtension('restored', {
}
return;
}
})
})

39
dist/ext/sse.js vendored
View File

@@ -7,6 +7,11 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
(function() {
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
/** @type {import("../htmx").HtmxInternalApi} */
var api;
@@ -14,8 +19,8 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
/**
* Init saves the provided reference to the internal HTMX API.
*
* @param {import("../htmx").HtmxInternalApi} api
*
* @param {import("../htmx").HtmxInternalApi} api
* @returns void
*/
init: function(apiRef) {
@@ -30,9 +35,9 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
/**
* onEvent handles all events passed to this extension.
*
* @param {string} name
* @param {Event} evt
*
* @param {string} name
* @param {Event} evt
* @returns void
*/
onEvent: function(name, evt) {
@@ -64,8 +69,8 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
/**
* createEventSource is the default method for creating new EventSource objects.
* it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
*
* @param {string} url
*
* @param {string} url
* @returns EventSource
*/
function createEventSource(url) {
@@ -105,7 +110,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
}
/**
* registerSSE looks for attributes that can contain sse events, right
* registerSSE looks for attributes that can contain sse events, right
* now hx-trigger and sse-swap and adds listeners based on these attributes too
* the closest event source
*
@@ -183,7 +188,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
if (sseEventName.slice(0, 4) != "sse:") {
return;
}
// remove the sse: prefix from here on out
sseEventName = sseEventName.substr(4);
@@ -269,8 +274,8 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
/**
* maybeCloseSSESource confirms that the parent element still exists.
* If not, then any associated SSE source is closed and the function returns true.
*
* @param {HTMLElement} elt
*
* @param {HTMLElement} elt
* @returns boolean
*/
function maybeCloseSSESource(elt) {
@@ -287,9 +292,9 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
/**
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
*
* @param {HTMLElement} elt
* @param {string} attributeName
*
* @param {HTMLElement} elt
* @param {string} attributeName
*/
function queryAttributeOnThisOrChildren(elt, attributeName) {
@@ -310,7 +315,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
/**
* @param {HTMLElement} elt
* @param {string} content
* @param {string} content
*/
function swap(elt, content) {
@@ -340,10 +345,10 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
}
/**
* doSettle mirrors much of the functionality in htmx that
* doSettle mirrors much of the functionality in htmx that
* settles elements after their content has been swapped.
* TODO: this should be published by htmx, and not duplicated here
* @param {import("../htmx").HtmxSettleInfo} settleInfo
* @param {import("../htmx").HtmxSettleInfo} settleInfo
* @returns () => void
*/
function doSettle(settleInfo) {

5
dist/ext/ws.js vendored
View File

@@ -6,6 +6,11 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
(function () {
if (htmx.version && !htmx.version.startsWith("1.")) {
console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
/** @type {import("../htmx").HtmxInternalApi} */
var api;

1432
dist/htmx.amd.js vendored

File diff suppressed because it is too large Load Diff

1432
dist/htmx.cjs.js vendored

File diff suppressed because it is too large Load Diff

219
dist/htmx.esm.d.ts vendored Normal file
View File

@@ -0,0 +1,219 @@
export default htmx;
export type HttpVerb = "get" | "head" | "post" | "put" | "delete" | "connect" | "options" | "trace" | "patch";
export type SwapOptions = {
select?: string;
selectOOB?: string;
eventInfo?: any;
anchor?: string;
contextElement?: Element;
afterSwapCallback?: swapCallback;
afterSettleCallback?: swapCallback;
beforeSwapCallback?: swapCallback;
title?: string;
historyRequest?: boolean;
};
export type swapCallback = () => any;
export type HtmxSwapStyle = "innerHTML" | "outerHTML" | "beforebegin" | "afterbegin" | "beforeend" | "afterend" | "delete" | "none" | string;
export type HtmxSwapSpecification = {
swapStyle: HtmxSwapStyle;
swapDelay: number;
settleDelay: number;
transition?: boolean;
ignoreTitle?: boolean;
head?: string;
scroll?: "top" | "bottom" | number;
scrollTarget?: string;
show?: string;
showTarget?: string;
focusScroll?: boolean;
};
export type ConditionalFunction = ((this: Node, evt: Event) => boolean) & {
source: string;
};
export type HtmxTriggerSpecification = {
trigger: string;
pollInterval?: number;
eventFilter?: ConditionalFunction;
changed?: boolean;
once?: boolean;
consume?: boolean;
delay?: number;
from?: string;
target?: string;
throttle?: number;
queue?: string;
root?: string;
threshold?: string;
};
export type HtmxElementValidationError = {
elt: Element;
message: string;
validity: ValidityState;
};
export type HtmxHeaderSpecification = Record<string, string>;
export type HtmxAjaxHelperContext = {
source?: Element | string;
event?: Event;
handler?: HtmxAjaxHandler;
target?: Element | string;
swap?: HtmxSwapStyle;
values?: any | FormData;
headers?: Record<string, string>;
select?: string;
push?: string;
replace?: string;
selectOOB?: string;
};
export type HtmxRequestConfig = {
boosted: boolean;
useUrlParams: boolean;
formData: FormData;
/**
* formData proxy
*/
parameters: any;
unfilteredFormData: FormData;
/**
* unfilteredFormData proxy
*/
unfilteredParameters: any;
headers: HtmxHeaderSpecification;
elt: Element;
target: Element;
verb: HttpVerb;
errors: HtmxElementValidationError[];
withCredentials: boolean;
timeout: number;
path: string;
triggeringEvent: Event;
};
export type HtmxResponseInfo = {
xhr: XMLHttpRequest;
target: Element;
requestConfig: HtmxRequestConfig;
etc: HtmxAjaxEtc;
boosted: boolean;
select: string;
pathInfo: {
requestPath: string;
finalRequestPath: string;
responsePath: string | null;
anchor: string;
};
failed?: boolean;
successful?: boolean;
keepIndicators?: boolean;
};
export type HtmxAjaxEtc = {
returnPromise?: boolean;
handler?: HtmxAjaxHandler;
select?: string;
targetOverride?: Element;
swapOverride?: HtmxSwapStyle;
headers?: Record<string, string>;
values?: any | FormData;
credentials?: boolean;
timeout?: number;
push?: string;
replace?: string;
selectOOB?: string;
};
export type HtmxResponseHandlingConfig = {
code?: string;
swap: boolean;
error?: boolean;
ignoreTitle?: boolean;
select?: string;
target?: string;
swapOverride?: string;
event?: string;
};
export type HtmxBeforeSwapDetails = HtmxResponseInfo & {
shouldSwap: boolean;
serverResponse: any;
isError: boolean;
ignoreTitle: boolean;
selectOverride: string;
swapOverride: string;
};
export type HtmxAjaxHandler = (elt: Element, responseInfo: HtmxResponseInfo) => any;
export type HtmxSettleTask = (() => void);
export type HtmxSettleInfo = {
tasks: HtmxSettleTask[];
elts: Element[];
title?: string;
};
export type HtmxExtension = {
init: (api: any) => void;
onEvent: (name: string, event: CustomEvent) => boolean;
transformResponse: (text: string, xhr: XMLHttpRequest, elt: Element) => string;
isInlineSwap: (swapStyle: HtmxSwapStyle) => boolean;
handleSwap: (swapStyle: HtmxSwapStyle, target: Node, fragment: Node, settleInfo: HtmxSettleInfo) => boolean | Node[];
encodeParameters: (xhr: XMLHttpRequest, parameters: FormData, elt: Node) => any | string | null;
getSelectors: () => string[] | null;
};
declare namespace htmx {
let onLoad: (callback: (elt: Node) => void) => EventListener;
let process: (elt: Element | string) => void;
let on: (arg1: EventTarget | string, arg2: string | EventListener, arg3?: EventListener | any | boolean, arg4?: any | boolean) => EventListener;
let off: (arg1: EventTarget | string, arg2: string | EventListener, arg3?: EventListener) => EventListener;
let trigger: (elt: EventTarget | string, eventName: string, detail?: any | undefined) => boolean;
let ajax: (verb: HttpVerb, path: string, context: Element | string | HtmxAjaxHelperContext) => Promise<void>;
let find: (eltOrSelector: ParentNode | string, selector?: string) => Element | null;
let findAll: (eltOrSelector: ParentNode | string, selector?: string) => NodeListOf<Element>;
let closest: (elt: Element | string, selector: string) => Element | null;
function values(elt: Element, type: HttpVerb): any;
let remove: (elt: Node, delay?: number) => void;
let addClass: (elt: Element | string, clazz: string, delay?: number) => void;
let removeClass: (node: Node | string, clazz: string, delay?: number) => void;
let toggleClass: (elt: Element | string, clazz: string) => void;
let takeClass: (elt: Node | string, clazz: string) => void;
let swap: (target: string | Element, content: string, swapSpec: HtmxSwapSpecification, swapOptions?: SwapOptions) => void;
let defineExtension: (name: string, extension: Partial<HtmxExtension>) => void;
let removeExtension: (name: string) => void;
let logAll: () => void;
let logNone: () => void;
let logger: any;
namespace config {
let historyEnabled: boolean;
let historyCacheSize: number;
let refreshOnHistoryMiss: boolean;
let defaultSwapStyle: HtmxSwapStyle;
let defaultSwapDelay: number;
let defaultSettleDelay: number;
let includeIndicatorStyles: boolean;
let indicatorClass: string;
let requestClass: string;
let addedClass: string;
let settlingClass: string;
let swappingClass: string;
let allowEval: boolean;
let allowScriptTags: boolean;
let inlineScriptNonce: string;
let inlineStyleNonce: string;
let attributesToSettle: string[];
let withCredentials: boolean;
let timeout: number;
let wsReconnectDelay: "full-jitter" | ((retryCount: number) => number);
let wsBinaryType: BinaryType;
let disableSelector: string;
let scrollBehavior: "auto" | "instant" | "smooth";
let defaultFocusScroll: boolean;
let getCacheBusterParam: boolean;
let globalViewTransitions: boolean;
let methodsThatUseUrlParams: (HttpVerb)[];
let selfRequestsOnly: boolean;
let ignoreTitle: boolean;
let scrollIntoViewOnBoost: boolean;
let triggerSpecsCache: any | null;
let disableInheritance: boolean;
let responseHandling: HtmxResponseHandlingConfig[];
let allowNestedOobSwaps: boolean;
let historyRestoreAsHxRequest: boolean;
let reportValidityOfForms: boolean;
}
let parseInterval: (str: string) => number | undefined;
let location: Location;
let _: (str: string) => any;
let version: string;
}

1434
dist/htmx.esm.js vendored

File diff suppressed because it is too large Load Diff

1432
dist/htmx.js vendored

File diff suppressed because it is too large Load Diff

2
dist/htmx.min.js vendored

File diff suppressed because one or more lines are too long

BIN
dist/htmx.min.js.gz vendored

Binary file not shown.

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
viewBox="0 0 256 256"
version="1.1"
id="svg13"
sodipodi:docname="htmx.svg"
inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs17" />
<sodipodi:namedview
id="namedview15"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
showgrid="false"
inkscape:zoom="32"
inkscape:cx="0.859375"
inkscape:cy="8.90625"
inkscape:window-width="1489"
inkscape:window-height="998"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg13" />
<path
fill="#3d72d7"
d="M 92.875785,213.11596 140.82049,41.850192 a 2.3748439,2.8675381 0 0 1 2.49698,-1.93354 l 19.88084,2.45789 a 2.3748439,2.8675381 0 0 1 2.022,3.752378 l -46.81835,168.90619 a 2.3748439,2.8675381 0 0 1 -2.2527,1.96631 l -21.034336,-0.0983 a 2.3748439,2.8675381 0 0 1 -2.239139,-3.78515 z"
id="path9"
style="stroke-width:1.49119" />
<path
fill="#333333"
d="m 33.830382,133.30008 c -1.592276,0.75375 -1.583229,1.4802 0.02714,2.17933 16.438443,7.17704 32.28883,13.91165 47.551159,20.20386 0.727419,0.30748 1.219852,1.12423 1.234919,2.04824 -0.214237,10.63702 -0.384508,19.1784 -0.401503,28.62622 -0.325692,1.01593 -1.52099,1.31634 -2.353315,0.90122 L 2.3060265,148.37514 c -0.357622,-0.18597 -0.5802653,-0.61873 -0.556392,-1.08148 l 0.123809,-25.31188 c 0.024863,-0.63224 0.8744512,-1.12426 1.7082134,-1.57743 L 79.440954,81.143657 c 0.804287,-0.421268 2.617287,0.182689 2.875279,1.21475 -0.180356,10.46196 0.296376,20.583873 0.286657,29.345013 -0.018,0.48554 -0.274377,0.9112 -0.651386,1.08148 -16.752569,7.66439 -33.358905,14.70553 -48.121122,20.51518 z m 189.580388,-0.27856 -48.43324,-20.41687 c -0.0776,-10.64493 -0.0238,-13.917539 0.0176,-30.506823 0.1719,-0.546198 0.98658,-0.599141 1.44798,-0.446206 27.0655,12.611655 55.00987,27.040189 77.98987,38.588869 0.52473,0.26218 0.78709,0.73737 0.78709,1.42558 l 0.0407,25.7423 c -0.001,0.63571 -0.31626,1.2093 -0.80066,1.45835 l -77.22992,38.50694 c -1.24994,0.10087 -2.28748,-0.64701 -2.43862,-1.68019 -0.12039,-9.77693 -0.0127,-18.13379 -0.11264,-28.51909 0.0323,-0.59314 0.35336,-1.10357 0.81423,-1.29449 16.72794,-6.80562 32.70951,-13.7314 47.94471,-20.77736 1.56513,-0.72098 1.5561,-1.41465 -0.0271,-2.08101 z"
id="path11"
style="stroke-width:1.49119"
sodipodi:nodetypes="ccccccccccccccccccccccccccccsc" />
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,7 +1,8 @@
{
"$schema": "https://raw.githubusercontent.com/JetBrains/web-types/master/schema/web-types.json",
"$schema": "https://json.schemastore.org/web-types",
"name": "htmx",
"version": "1.0.0",
"version": "2.0.8",
"default-icon": "./htmx.svg",
"js-types-syntax": "typescript",
"description-markup": "markdown",
"contributions": {
@@ -9,7 +10,7 @@
"attributes": [
{
"name": "hx-boost",
"description": "The **hx-boost** attribute allows you to \"boost\" normal anchors and form tags to use AJAX instead. This has the [nice fallback](https://en.wikipedia.org/wiki/Progressive_enhancement) that, if the user does not have javascript enabled, the site will continue to work.",
"description": "The `hx-boost` attribute allows you to \"boost\" normal anchors and form tags to use AJAX instead. This\nhas the [nice fallback](https://en.wikipedia.org/wiki/Progressive_enhancement) that, if the user does not \nhave javascript enabled, the site will continue to work.\n\nFor anchor tags, clicking on the anchor will issue a `GET` request to the url specified in the `href` and\nwill push the url so that a history entry is created. The target is the `<body>` tag, and the `innerHTML`\nswap strategy is used by default. All of these can be modified by using the appropriate attributes, except\nthe `click` trigger.",
"description-sections": {
"Inherited": ""
},
@@ -17,7 +18,7 @@
},
{
"name": "hx-confirm",
"description": "The **hx-confirm** attribute allows you to confirm an action before issuing a request. This can be useful in cases where the action is destructive and you want to ensure that the user really wants to do it.",
"description": "The `hx-confirm` attribute allows you to confirm an action before issuing a request. This can be useful\nin cases where the action is destructive and you want to ensure that the user really wants to do it.\n\nHere is an example:\n\n```html\n<button hx-delete=\"/account\" hx-confirm=\"Are you sure you wish to delete your account?\">\n Delete My Account\n</button>\n```",
"description-sections": {
"Inherited": ""
},
@@ -25,227 +26,280 @@
},
{
"name": "hx-delete",
"description": "The **hx-delete** attribute will cause an element to issue a **DELETE** to the specified URL and swap the HTML into the DOM using a swap strategy.",
"description": "The `hx-delete` attribute will cause an element to issue a `DELETE` to the specified URL and swap\nthe HTML into the DOM using a swap strategy:\n\n```html\n<button hx-delete=\"/account\" hx-target=\"body\">\n Delete Your Account\n</button>\n```",
"description-sections": {
"Not inherited": ""
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-confirm/"
"doc-url": "https://htmx.org/attributes/hx-delete/"
},
{
"name": "hx-disable",
"description": "The **hx-disable** attribute disables htmx processing for the given node and any children nodes",
"description": "The `hx-disable` attribute will disable htmx processing for a given element and all its children. This can be \nuseful as a backup for HTML escaping, when you include user generated content in your site, and you want to \nprevent malicious scripting attacks.\n\nThe value of the tag is ignored, and it cannot be reversed by any content beneath it.",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-disable"
"doc-url": "https://htmx.org/attributes/hx-disable/"
},
{
"name": "hx-encoding",
"description": "The **hx-encoding** attribute changes the request encoding type",
"name": "hx-disabled-elt",
"description": "The `hx-disabled-elt` attribute allows you to specify elements that will have the `disabled` attribute\nadded to them for the duration of the request. The value of this attribute can be:\n\n* A CSS query selector of the element to disable.\n* `this` to disable the element itself\n* `closest <CSS selector>` which will find the [closest](https://developer.mozilla.org/docs/Web/API/Element/closest)\n ancestor element or itself, that matches the given CSS selector\n (e.g. `closest fieldset` will disable the closest to the element `fieldset`).\n* `find <CSS selector>` which will find the first child descendant element that matches the given CSS selector\n* `next` which resolves to [element.nextElementSibling](https://developer.mozilla.org/docs/Web/API/Element/nextElementSibling)\n* `next <CSS selector>` which will scan the DOM forward for the first element that matches the given CSS selector\n (e.g. `next button` will disable the closest following sibling `button` element)\n* `previous` which resolves to [element.previousElementSibling](https://developer.mozilla.org/docs/Web/API/Element/previousElementSibling)\n* `previous <CSS selector>` which will scan the DOM backwards for the first element that matches the given CSS selector.\n (e.g. `previous input` will disable the closest previous sibling `input` element)",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-encoding"
},
{
"name": "hx-ext",
"description": "The **hx-ext** attribute enables extensions for an element",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-ext"
},
{
"name": "hx-get",
"description": "The **hx-get** attribute issues a `GET` to the specified URL",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-get"
},
{
"name": "hx-headers",
"description": "The **hx-headers** attribute adds to the headers that will be submitted with the request",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-headers"
},
{
"name": "hx-history-elt",
"description": "The **hx-history-elt** attribute specifies the element to snapshot and restore during history navigation",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-history-elt"
},
{
"name": "hx-include",
"description": "The **hx-include** attribute specifies additional values/inputs to include in AJAX requests",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-include"
},
{
"name": "hx-indicator",
"description": "The **hx-indicator** attribute specifies the element to put the `htmx-request` class on during the AJAX request, displaying it as a request indicator",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-indicator"
"doc-url": "https://htmx.org/attributes/hx-disabled-elt/"
},
{
"name": "hx-disinherit",
"description": "The **hx-disinherit** attribute allows you to control and disable automatic attribute inheritance for child nodes",
"description": "The default behavior for htmx is to \"inherit\" many attributes automatically: that is, an attribute such as\n[hx-target](@/attributes/hx-target.md) may be placed on a parent element, and all child elements will inherit\nthat target.\n\nThe `hx-disinherit` attribute allows you to control this automatic attribute inheritance. An example scenario is to \nallow you to place an `hx-boost` on the `body` element of a page, but overriding that behavior in a specific part\nof the page to allow for more specific behaviors.",
"description-sections": {},
"doc-url": "https://htmx.org/attributes/hx-disinherit/"
},
{
"name": "hx-encoding",
"description": "The `hx-encoding` attribute allows you to switch the request encoding from the usual `application/x-www-form-urlencoded`\nencoding to `multipart/form-data`, usually to support file uploads in an ajax request.\n\nThe value of this attribute should be `multipart/form-data`.",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-disinherit"
"doc-url": "https://htmx.org/attributes/hx-encoding/"
},
{
"name": "hx-ext",
"description": "The `hx-ext` attribute enables an htmx [extension](https://htmx.org/extensions) for an element and all its children.\n\nThe value can be a single extension name or a comma-separated list of extensions to apply.",
"description-sections": {},
"doc-url": "https://htmx.org/attributes/hx-ext/"
},
{
"name": "hx-get",
"description": "The `hx-get` attribute will cause an element to issue a `GET` to the specified URL and swap\nthe HTML into the DOM using a swap strategy:\n\n```html\n <button hx-get=\"/example\">Get Some HTML</button>\n```",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-get/"
},
{
"name": "hx-headers",
"description": "The `hx-headers` attribute allows you to add to the headers that will be submitted with an AJAX request.\n\nBy default, the value of this attribute is a list of name-expression values in [JSON (JavaScript Object Notation)](https://www.json.org/json-en.html)\nformat.",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-headers/"
},
{
"name": "hx-history-elt",
"description": "The `hx-history-elt` attribute allows you to specify the element that will be used to snapshot and\nrestore page state during navigation. By default, the `body` tag is used. This is typically\ngood enough for most setups, but you may want to narrow it down to a child element. Just make\nsure that the element is always visible in your application, or htmx will not be able to restore\nhistory navigation properly.",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-history-elt/"
},
{
"name": "hx-history",
"description": "Set the `hx-history` attribute to `false` on any element in the current document, or any html fragment loaded into the current document by htmx, to prevent sensitive data being saved to the localStorage cache when htmx takes a snapshot of the page state.\n\nHistory navigation will work as expected, but on restoration the URL will be requested from the server instead of the history cache.",
"description-sections": {},
"doc-url": "https://htmx.org/attributes/hx-history/"
},
{
"name": "hx-include",
"description": "The `hx-include` attribute allows you to include additional element values in an AJAX request. The value of this\nattribute can be:\n\n* A CSS query selector of the elements to include.\n* `this` which will include the descendants of the element.\n* `closest <CSS selector>` which will find the [closest](https://developer.mozilla.org/docs/Web/API/Element/closest)\n ancestor element or itself, that matches the given CSS selector\n (e.g. `closest tr` will target the closest table row to the element).\n* `find <CSS selector>` which will find the first child descendant element that matches the given CSS selector.\n* `next <CSS selector>` which will scan the DOM forward for the first element that matches the given CSS selector.\n (e.g. `next .error` will target the closest following sibling element with `error` class)\n* `previous <CSS selector>` which will scan the DOM backwards for the first element that matches the given CSS selector.\n (e.g. `previous .error` will target the closest previous sibling with `error` class)",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-include/"
},
{
"name": "hx-indicator",
"description": "The `hx-indicator` attribute allows you to specify the element that will have the `htmx-request` class\nadded to it for the duration of the request. This can be used to show spinners or progress indicators\nwhile the request is in flight.\n\nThe value of this attribute is a CSS query selector of the element or elements to apply the class to,\nor the keyword [`closest`](https://developer.mozilla.org/docs/Web/API/Element/closest), followed by a CSS selector, \nwhich will find the closest ancestor element or itself, that matches the given CSS selector (e.g. `closest tr`);",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-indicator/"
},
{
"name": "hx-inherit",
"description": "The default behavior for htmx is to \"inherit\" many attributes automatically: that is, an attribute such as\n[hx-target](@/attributes/hx-target.md) may be placed on a parent element, and all child elements will inherit\nthat target. Some people do not like this feature and instead prefer to explicitly specify inheritance for attributes.\n\nTo support this mode of development, htmx offers the `htmx.config.disableInheritance` setting, which can be set to\n`true` to prevent inheritance from being the default behavior for any of the htmx attributes.",
"description-sections": {},
"doc-url": "https://htmx.org/attributes/hx-inherit/"
},
{
"name": "hx-on",
"pattern": {
"or": [
{
"items": [
"/js/events"
],
"template": [
"hx-on:",
"#...",
"#item:JS event"
]
},
{
"items": [
"/js/htmx-events"
],
"template": [
"hx-on::",
"#...",
"#item:HTMX event"
]
}
]
},
"description": "The `hx-on*` attributes allow you to embed scripts inline to respond to events directly on an element; similar to the \n[`onevent` properties](https://developer.mozilla.org/en-US/docs/Web/Events/Event_handlers#using_onevent_properties) found in HTML, such as `onClick`.\n\nThe `hx-on*` attributes improve upon `onevent` by enabling the handling of any arbitrary JavaScript event,\nfor enhanced [Locality of Behaviour (LoB)](/essays/locality-of-behaviour/) even when dealing with non-standard DOM events. For example, these\nattributes allow you to handle [htmx events](/reference#events).",
"description-sections": {},
"doc-url": "https://htmx.org/attributes/hx-on/"
},
{
"name": "hx-params",
"description": "The **hx-params** attribute allows you filter the parameters that will be submitted with a request",
"description": "The `hx-params` attribute allows you to filter the parameters that will be submitted with an AJAX request.\n\nThe possible values of this attribute are:\n\n* `*` - Include all parameters (default)\n* `none` - Include no parameters\n* `not <param-list>` - Include all except the comma separated list of parameter names\n* `<param-list>` - Include all the comma separated list of parameter names",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-params"
"doc-url": "https://htmx.org/attributes/hx-params/"
},
{
"name": "hx-patch",
"description": "The **hx-patch** attribute issues a `PATCH` to the specified URL",
"description": "The `hx-patch` attribute will cause an element to issue a `PATCH` to the specified URL and swap\nthe HTML into the DOM using a swap strategy:\n\n```html\n<button hx-patch=\"/account\" hx-target=\"body\">\n Patch Your Account\n</button>\n```",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-patch"
"doc-url": "https://htmx.org/attributes/hx-patch/"
},
{
"name": "hx-post",
"description": "The **hx-post** attribute issues a `POST` to the specified URL",
"description": "The `hx-post` attribute will cause an element to issue a `POST` to the specified URL and swap\nthe HTML into the DOM using a swap strategy:\n\n```html\n<button hx-post=\"/account/enable\" hx-target=\"body\">\n Enable Your Account\n</button>\n```",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-post"
"doc-url": "https://htmx.org/attributes/hx-post/"
},
{
"name": "hx-preserve",
"description": "The **hx-preserve** attribute preserves an element between requests (requires the `id` be stable)",
"description": "The `hx-preserve` attribute allows you to keep an element unchanged during HTML replacement.\nElements with `hx-preserve` set are preserved by `id` when htmx updates any ancestor element.\nYou *must* set an unchanging `id` on elements for `hx-preserve` to work.\nThe response requires an element with the same `id`, but its type and other attributes are ignored.\n\n## Notes",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-preserve"
"doc-url": "https://htmx.org/attributes/hx-preserve/"
},
{
"name": "hx-prompt",
"description": "The **hx-prompt** attribute shows a prompt before submitting a request",
"description": "The `hx-prompt` attribute allows you to show a prompt before issuing a request. The value of\nthe prompt will be included in the request in the `HX-Prompt` header.\n\nHere is an example:\n\n```html\n<button hx-delete=\"/account\" hx-prompt=\"Enter your account name to confirm deletion\">\n Delete My Account\n</button>\n```",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-prompt"
"doc-url": "https://htmx.org/attributes/hx-prompt/"
},
{
"name": "hx-push-url",
"description": "The **hx-push-url** attribute pushes the URL into the location bar, creating a new history entry",
"description": "The `hx-push-url` attribute allows you to push a URL into the browser [location history](https://developer.mozilla.org/en-US/docs/Web/API/History_API).\nThis creates a new history entry, allowing navigation with the browsers back and forward buttons.\nhtmx snapshots the current DOM and saves it into its history cache, and restores from this cache on navigation.\n\nThe possible values of this attribute are:\n\n1. `true`, which pushes the fetched URL into history.\n2. `false`, which disables pushing the fetched URL if it would otherwise be pushed due to inheritance or [`hx-boost`](/attributes/hx-boost).\n3. A URL to be pushed into the location bar.\n This may be relative or absolute, as per [`history.pushState()`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState).",
"description-sections": {
"Not Inherited": ""
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-push-url"
"doc-url": "https://htmx.org/attributes/hx-push-url/"
},
{
"name": "hx-put",
"description": "The **hx-put** attribute issues a `PUT` to the specified URL",
"description": "The `hx-put` attribute will cause an element to issue a `PUT` to the specified URL and swap\nthe HTML into the DOM using a swap strategy:\n\n```html\n<button hx-put=\"/account\" hx-target=\"body\">\n Put Money In Your Account\n</button>\n```",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-put"
"doc-url": "https://htmx.org/attributes/hx-put/"
},
{
"name": "hx-replace-url",
"description": "The `hx-replace-url` attribute allows you to replace the current url of the browser [location history](https://developer.mozilla.org/en-US/docs/Web/API/History_API).\n\nThe possible values of this attribute are:\n\n1. `true`, which replaces the fetched URL in the browser navigation bar.\n2. `false`, which disables replacing the fetched URL if it would otherwise be replaced due to inheritance.\n3. A URL to be replaced into the location bar.\n This may be relative or absolute, as per [`history.replaceState()`](https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState).",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-replace-url/"
},
{
"name": "hx-request",
"description": "The **hx-request** attribute configures various aspects of the request",
"description": "The `hx-request` attribute allows you to configure various aspects of the request via the following attributes:\n \n* `timeout` - the timeout for the request, in milliseconds\n* `credentials` - if the request will send credentials\n* `noHeaders` - strips all headers from the request\n\nThese attributes are set using a JSON-like syntax:\n\n```html\n<div ... hx-request='{\"timeout\":100}'>\n ...\n</div>\n```",
"description-sections": {},
"doc-url": "https://htmx.org/attributes/hx-request/"
},
{
"name": "hx-select-oob",
"description": "The `hx-select-oob` attribute allows you to select content from a response to be swapped in via an out-of-band swap. \nThe value of this attribute is comma separated list of elements to be swapped out of band. This attribute is almost\nalways paired with [hx-select](@/attributes/hx-select.md).\n\nHere is an example that selects a subset of the response content:\n\n```html\n<div>\n <div id=\"alert\"></div>\n <button hx-get=\"/info\" \n hx-select=\"#info-details\" \n hx-swap=\"outerHTML\"\n hx-select-oob=\"#alert\">\n Get Info!\n </button>\n</div>\n```",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-request"
"doc-url": "https://htmx.org/attributes/hx-select-oob/"
},
{
"name": "hx-select",
"description": "The **hx-select** attribute selects a subset of the server response to process",
"description": "The `hx-select` attribute allows you to select the content you want swapped from a response. The value of\nthis attribute is a CSS query selector of the element or elements to select from the response.\n\nHere is an example that selects a subset of the response content:\n\n```html\n<div>\n <button hx-get=\"/info\" hx-select=\"#info-detail\" hx-swap=\"outerHTML\">\n Get Info!\n </button>\n</div>\n```",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-select"
},
{
"name": "hx-sse",
"description": "The **hx-sse** attribute connects the DOM to a SSE source",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-sse"
"doc-url": "https://htmx.org/attributes/hx-select/"
},
{
"name": "hx-swap-oob",
"description": "The **hx-swap-oob** attribute marks content in a response as being \"Out of Band\", i.e. swapped somewhere other than the target",
"description": "The `hx-swap-oob` attribute allows you to specify that some content in a response should be\nswapped into the DOM somewhere other than the target, that is \"Out of Band\". This allows you to piggyback updates to other element updates on a response.\n\nConsider the following response HTML:\n\n```html\n<div>\n ...\n</div>\n<div id=\"alerts\" hx-swap-oob=\"true\">\n Saved!\n</div>",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-swap-oob"
"doc-url": "https://htmx.org/attributes/hx-swap-oob/"
},
{
"name": "hx-swap",
"description": "The **hx-swap** attribute controls how the response content is swapped into the DOM (e.g. 'outerHTML' or 'beforeend')",
"description": "The `hx-swap` attribute allows you to specify how the response will be swapped in relative to the\n[target](@/attributes/hx-target.md) of an AJAX request. If you do not specify the option, the default is\n`htmx.config.defaultSwapStyle` (`innerHTML`).\n\nThe possible values of this attribute are:\n\n* `innerHTML` - Replace the inner html of the target element\n* `outerHTML` - Replace the entire target element with the response\n* `textContent` - Replace the text content of the target element, without parsing the response as HTML\n* `beforebegin` - Insert the response before the target element\n* `afterbegin` - Insert the response before the first child of the target element\n* `beforeend` - Insert the response after the last child of the target element\n* `afterend` - Insert the response after the target element\n* `delete` - Deletes the target element regardless of the response\n* `none`- Does not append content from response (out of band items will still be processed).",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-swap"
"doc-url": "https://htmx.org/attributes/hx-swap/"
},
{
"name": "hx-sync",
"description": "The **hx-sync** attribute controls requests made by different elements are synchronized with one another",
"description": "The `hx-sync` attribute allows you to synchronize AJAX requests between multiple elements.\n\nThe `hx-sync` attribute consists of a CSS selector to indicate the element to synchronize on, followed optionally\nby a colon and then by an optional syncing strategy. The available strategies are:\n\n* `drop` - drop (ignore) this request if an existing request is in flight (the default)\n* `abort` - drop (ignore) this request if an existing request is in flight, and, if that is not the case, \n *abort* this request if another request occurs while it is still in flight\n* `replace` - abort the current request, if any, and replace it with this request\n* `queue` - place this request in the request queue associated with the given element",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-sync"
"doc-url": "https://htmx.org/attributes/hx-sync/"
},
{
"name": "hx-target",
"description": "The **hx-target** attribute specifies the target element to be swapped",
"description": "The `hx-target` attribute allows you to target a different element for swapping than the one issuing the AJAX\nrequest. The value of this attribute can be:\n\n* A CSS query selector of the element to target.\n* `this` which indicates that the element that the `hx-target` attribute is on is the target.\n* `closest <CSS selector>` which will find the [closest](https://developer.mozilla.org/docs/Web/API/Element/closest)\n ancestor element or itself, that matches the given CSS selector\n (e.g. `closest tr` will target the closest table row to the element).\n* `find <CSS selector>` which will find the first child descendant element that matches the given CSS selector.\n* `next` which resolves to [element.nextElementSibling](https://developer.mozilla.org/docs/Web/API/Element/nextElementSibling)\n* `next <CSS selector>` which will scan the DOM forward for the first element that matches the given CSS selector.\n (e.g. `next .error` will target the closest following sibling element with `error` class)\n* `previous` which resolves to [element.previousElementSibling](https://developer.mozilla.org/docs/Web/API/Element/previousElementSibling)\n* `previous <CSS selector>` which will scan the DOM backwards for the first element that matches the given CSS selector.\n (e.g. `previous .error` will target the closest previous sibling with `error` class)",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-target"
"doc-url": "https://htmx.org/attributes/hx-target/"
},
{
"name": "hx-trigger",
"description": "The **hx-trigger** attribute specifies specifies the event that triggers the request",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-trigger"
},
{
"name": "hx-vals",
"description": "The **hx-vals** attribute specifies values to add to the parameters that will be submitted with the request in JSON form",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-vals"
},
{
"name": "hx-vars",
"description": "The **hx-vars** attribute specifies computed values to add to the parameters that will be submitted with the request",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-vars"
},
{
"name": "hx-ws",
"description": "The **hx-ws** attribute connects the DOM to a Web Socket source",
"description": "The `hx-trigger` attribute allows you to specify what triggers an AJAX request. A trigger\nvalue can be one of the following:\n\n* An event name (e.g. \"click\" or \"my-custom-event\") followed by an event filter and a set of event modifiers\n* A polling definition of the form `every <timing declaration>`\n* A comma-separated list of such events",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-ws"
"doc-url": "https://htmx.org/attributes/hx-trigger/"
},
{
"name": "hx-validate",
"description": "The `hx-validate` attribute will cause an element to validate itself by way of the [HTML5 Validation API](@/docs.md#validation)\nbefore it submits a request.\n\nOnly `<form>` elements validate data by default, but other elements do not. Adding `hx-validate=\"true\"` to `<input>`, `<textarea>` or `<select>` enables validation before sending requests.",
"description-sections": {
"Not Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-validate/"
},
{
"name": "hx-vals",
"description": "The `hx-vals` attribute allows you to add to the parameters that will be submitted with an AJAX request.\n\nBy default, the value of this attribute is a list of name-expression values in [JSON (JavaScript Object Notation)](https://www.json.org/json-en.html)\nformat.",
"description-sections": {
"Inherited": ""
},
"doc-url": "https://htmx.org/attributes/hx-vals/"
},
{
"name": "hx-vars",
"description": "**NOTE: `hx-vars` has been deprecated in favor of [`hx-vals`](@/attributes/hx-vals.md), which is safer by default.**\n\nThe `hx-vars` attribute allows you to dynamically add to the parameters that will be submitted with an AJAX request.",
"description-sections": {
"Inherited": ""
},
"deprecated": true,
"doc-url": "https://htmx.org/attributes/hx-vars/"
}
]
},
@@ -258,25 +312,284 @@
},
{
"name": "htmx-indicator",
"description": "A dynamically generated class that will toggle visible (opacity:1) when a `htmx-request` class is present.",
"description": "A dynamically generated class that will toggle visible (opacity:1) when a `htmx-request` class is present",
"doc-url": "https://htmx.org/reference/#classes"
},
{
"name": "htmx-request",
"description": "Applied to either the element or the element specified with `hx-indicator` while a request is ongoing.",
"description": "Applied to either the element or the element specified with [`hx-indicator`](@/attributes/hx-indicator.md) while a request is ongoing",
"doc-url": "https://htmx.org/reference/#classes"
},
{
"name": "htmx-settling",
"description": "Applied to a target after content is swapped, removed after it is settled. The duration can be modified via `hx-swap`.",
"description": "Applied to a target after content is swapped, removed after it is settled. The duration can be modified via [`hx-swap`](@/attributes/hx-swap.md).",
"doc-url": "https://htmx.org/reference/#classes"
},
{
"name": "htmx-swapping",
"description": "Applied to a target before any content is swapped, removed after it is swapped. The duration can be modified via `hx-swap`.",
"description": "Applied to a target before any content is swapped, removed after it is swapped. The duration can be modified via [`hx-swap`](@/attributes/hx-swap.md).",
"doc-url": "https://htmx.org/reference/#classes"
}
]
},
"js": {
"events": [
{
"name": "HTMX event",
"pattern": {
"items": [
"/js/htmx-events"
],
"template": [
"htmx:",
"$...",
"#item:HTMX event"
]
}
}
],
"htmx-events": [
{
"name": "abort",
"description": "This event is different than other events: htmx does not *trigger* it, but rather *listens* for it.\n\nIf you send an `htmx:abort` event to an element making a request, it will abort the request:\n\n```html\n<button id=\"request-button\" hx-post=\"/example\">Issue Request</button>\n<button onclick=\"htmx.trigger('#request-button', 'htmx:abort')\">Cancel Request</button>\n```\n\n",
"doc-url": "https://htmx.org/events/#htmx:abort"
},
{
"name": "afterOnLoad",
"description": "This event is triggered after an AJAX `onload` has finished. Note that this does not mean that the content\nhas been swapped or settled yet, only that the request has finished.\n\n##### Details\n\n* `detail.elt` - the element that dispatched the request or if the body no longer contains the element then the closest parent\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:afterOnLoad"
},
{
"name": "afterProcessNode",
"description": "This event is triggered after htmx has initialized a DOM node. It can be useful for extensions to build additional features onto a node.\n\n##### Details\n\n* `detail.elt` - the element being initialized\n\n",
"doc-url": "https://htmx.org/events/#htmx:afterProcessNode"
},
{
"name": "afterRequest",
"description": "This event is triggered after an AJAX request has finished either in the case of a successful request (although\none that may have returned a remote error code such as a `404`) or in a network error situation. This event\ncan be paired with [`htmx:beforeRequest`](#htmx:beforeRequest) to wrap behavior around a request cycle.\n\n##### Details\n\n* `detail.elt` - the element that dispatched the request or if the body no longer contains the element then the closest parent\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n* `detail.successful` - true if the response has a 20x status code or is marked `detail.isError = false` in the\n `htmx:beforeSwap` event, else false\n* `detail.failed` - true if the response does not have a 20x status code or is marked `detail.isError = true` in the\n `htmx:beforeSwap` event, else false\n\n",
"doc-url": "https://htmx.org/events/#htmx:afterRequest"
},
{
"name": "afterSettle",
"description": "This event is triggered after the DOM has [settled](@/docs.md#request-operations).\n\n##### Details\n\n* `detail.elt` - the updated element\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:afterSettle"
},
{
"name": "afterSwap",
"description": "This event is triggered after new content has been [swapped into the DOM](@/docs.md#swapping).\n\n##### Details\n\n* `detail.elt` - the swapped in element\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:afterSwap"
},
{
"name": "beforeCleanupElement",
"description": "This event is triggered before htmx [disables](@/attributes/hx-disable.md) an element or removes it from the DOM.\n\n##### Details\n\n* `detail.elt` - the element to be cleaned up\n\n",
"doc-url": "https://htmx.org/events/#htmx:beforeCleanupElement"
},
{
"name": "beforeOnLoad",
"description": "This event is triggered before any response processing occurs. If you call `preventDefault()` on the event to cancel it, no swap will occur.\n\n##### Details\n\n* `detail.elt` - the element that dispatched the request\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:beforeOnLoad"
},
{
"name": "beforeProcessNode",
"description": "This event is triggered before htmx initializes a DOM node and has processed all of its `hx-` attributes. This gives extensions and other external code the ability to modify the contents of a DOM node before it is processed.\n\n##### Details\n\n* `detail.elt` - the element being initialized\n\n",
"doc-url": "https://htmx.org/events/#htmx:beforeProcessNode"
},
{
"name": "beforeRequest",
"description": "This event is triggered before an AJAX request is issued. If you call `preventDefault()` on the event to cancel it, no request will occur.\n\n##### Details\n\n* `detail.elt` - the element that dispatched the request\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.target` - the target of the request\n* `detail.boosted` - true if the request is via an element using boosting\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:beforeRequest"
},
{
"name": "beforeSend",
"description": "This event is triggered right before a request is sent. You may not cancel the request with this event.\n\n##### Details\n\n* `detail.elt` - the element that dispatched the request\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:beforeSend"
},
{
"name": "beforeSwap",
"description": "This event is triggered before any new content has been [swapped into the DOM](@/docs.md#swapping).\nMost values on `detail` can be set to override subsequent behavior, other than where response headers take precedence.\nIf you call `preventDefault()` on the event to cancel it, no swap will occur.\n\nYou can modify the default swap behavior by modifying the `shouldSwap`, `selectOverride`, `swapOverride` and `target` properties of the event detail.\nSee the documentation on [configuring swapping](@/docs.md#modifying_swapping_behavior_with_events) for more details.\n\n##### Details\n\n* `detail.elt` - the target of the swap\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.boosted` - true if the request is via an element using boosting\n* `detail.requestConfig` - the configuration of the AJAX request\n* `detail.requestConfig.elt` - the element that dispatched the request\n* `detail.shouldSwap` - if the content will be swapped (defaults to `false` for non-200 response codes)\n* `detail.ignoreTitle` - if `true` any title tag in the response will be ignored\n* `detail.isError` - whether error events should be triggered and also determines the values of `detail.successful` and `detail.failed` in later events\n* `detail.serverResponse` - the server response as text to be used for the swap\n* `detail.selectOverride` - add this to use instead of an [`hx-select`](@/attributes/hx-select.md) value\n* `detail.swapOverride` - add this to use instead of an [`hx-swap`](@/attributes/hx-swap.md) value\n* `detail.target` - the target of the swap\n\n",
"doc-url": "https://htmx.org/events/#htmx:beforeSwap"
},
{
"name": "beforeTransition",
"description": "This event is triggered before a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) \nwrapped swap occurs. If you call `preventDefault()` on the event to cancel it, the View Transition will not occur and the normal swapping logic will\nhappen instead.\n\n##### Details\n\n* `detail.elt` - the element that dispatched the request\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.boosted` - true if the request is via an element using boosting\n* `detail.requestConfig` - the configuration of the AJAX request\n* `detail.shouldSwap` - if the content will be swapped (defaults to `false` for non-200 response codes)\n* `detail.target` - the target of the swap\n\n",
"doc-url": "https://htmx.org/events/#htmx:beforeTransition"
},
{
"name": "configRequest",
"description": "This event is triggered after htmx has collected parameters for inclusion in the request. It can be\nused to include or update the parameters that htmx will send:\n\n```javascript\ndocument.body.addEventListener('htmx:configRequest', function(evt) {\n evt.detail.parameters['auth_token'] = getAuthToken(); // add a new parameter into the mix\n});\n```\n\nNote that if an input value appears more than once the value in the `parameters` object will be an array, rather\nthan a single value.\n\n##### Details\n\n* `detail.parameters` - the parameters that will be submitted in the request\n* `detail.unfilteredParameters` - the parameters that were found before filtering by [`hx-params`](@/attributes/hx-params.md)\n* `detail.headers` - the request headers\n* `detail.elt` - the element that triggered the request\n* `detail.target` - the target of the request\n* `detail.verb` - the HTTP verb in use\n\n",
"doc-url": "https://htmx.org/events/#htmx:configRequest"
},
{
"name": "confirm",
"description": "This event is fired on every trigger for a request (not just on elements that have a hx-confirm attribute).\nIt allows you to cancel (or delay) issuing the AJAX request.\nIf you call `preventDefault()` on the event, it will not issue the given request.\nThe `detail` object contains a function, `evt.detail.issueRequest(skipConfirmation=false)`, that can be used to issue the actual AJAX request at a later point.\nCombining these two features allows you to create an asynchronous confirmation dialog.\n\nHere is a basic example that shows the basic usage of the `htmx:confirm` event without altering the default behavior:\n\n```javascript\ndocument.body.addEventListener('htmx:confirm', function(evt) {\n // 0. To modify the behavior only for elements with the hx-confirm attribute,\n // check if evt.detail.target.hasAttribute('hx-confirm')\n\n // 1. Prevent the default behavior (this will prevent the request from being issued)\n evt.preventDefault();\n \n // 2. Do your own logic here\n console.log(evt.detail)\n\n // 3. Manually issue the request when you are ready\n evt.detail.issueRequest(); // or evt.detail.issueRequest(true) to skip the built-in window.confirm()\n});\n```\n\nAnd here is an example using [sweet alert](https://sweetalert.js.org/guides/) on any element with a `confirm-with-sweet-alert=\"{question}\"` attribute on it:\n\n```javascript\ndocument.body.addEventListener('htmx:confirm', function(evt) {\n // 1. The requirement to show the sweet alert is that the element has a confirm-with-sweet-alert\n // attribute on it, if it doesn't we can return early and let the default behavior happen\n if (!evt.detail.target.hasAttribute('confirm-with-sweet-alert')) return\n\n // 2. Get the question from the attribute\n const question = evt.detail.target.getAttribute('confirm-with-sweet-alert');\n\n // 3. Prevent the default behavior (this will prevent the request from being issued)\n evt.preventDefault();\n\n // 4. Show the sweet alert\n swal({\n title: \"Are you sure?\",\n text: question || \"Are you sure you want to continue?\",\n icon: \"warning\",\n buttons: true,\n dangerMode: true,\n }).then((confirmed) => {\n if (confirmed) {\n // 5. If the user confirms, we can manually issue the request\n evt.detail.issueRequest(true); // true to skip the built-in window.confirm()\n }\n });\n});\n```\n\n##### Details\n\n* `detail.elt` - the element in question\n* `detail.etc` - additional request information (mostly unused)\n* `detail.issueRequest(skipConfirmation=false)` - a function that can be invoked to issue the request (should be paired with `evt.preventDefault()`!), if skipConfirmation is `true` the original `window.confirm()` is not executed\n* `detail.path` - the path of the request\n* `detail.target` - the element that triggered the request\n* `detail.triggeringEvent` - the original event that triggered this request\n* `detail.verb` - the verb of the request (e.g. `GET`)\n* `detail.question` - the question passed to `hx-confirm` attribute (only available if `hx-confirm` attribute is present)\n\n",
"doc-url": "https://htmx.org/events/#htmx:confirm"
},
{
"name": "historyCacheError",
"description": "This event is triggered when an attempt to save the cache to `localStorage` fails\n\n##### Details\n\n* `detail.cause` - the `Exception` that was thrown when attempting to save history to `localStorage`\n\n",
"doc-url": "https://htmx.org/events/#htmx:historyCacheError"
},
{
"name": "historyCacheHit",
"description": "This event is triggered when a cache hit occurs when restoring history\n\nYou can prevent the history restoration via `preventDefault()` to allow alternative restore handling.\nYou can also override the details of the history restoration request in this event if required\n\n##### Details\n\n* `detail.historyElt` - the history element or body that will get replaced\n* `detail.item.content` - the content of the cache that will be swapped in\n* `detail.item.title` - the page title to update from the cache\n* `detail.path` - the path and query of the page being restored\n* `detail.swapSpec` - the swapSpec to be used containing the defatul swapStyle='innerHTML'\n\n",
"doc-url": "https://htmx.org/events/#htmx:historyCacheHit"
},
{
"name": "historyCacheMiss",
"description": "This event is triggered when a cache miss occurs when restoring history\n\nYou can prevent the history restoration via `preventDefault()` to allow alternative restore handling.\nYou can also modify the xhr request or other details before it makes the the request to restore history\n\n##### Details\n\n* `detail.historyElt` - the history element or body that will get replaced\n* `detail.xhr` - the `XMLHttpRequest` that will retrieve the remote content for restoration\n* `detail.path` - the path and query of the page being restored\n* `detail.swapSpec` - the swapSpec to be used containing the defatul swapStyle='innerHTML'\n\n",
"doc-url": "https://htmx.org/events/#htmx:historyCacheMiss"
},
{
"name": "historyCacheMissLoadError",
"description": "This event is triggered when a cache miss occurs and a response has been retrieved from the server\nfor the content to restore, but the response is an error (e.g. `404`)\n\n##### Details\n\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.path` - the path and query of the page being restored\n\n",
"doc-url": "https://htmx.org/events/#htmx:historyCacheMissLoadError"
},
{
"name": "historyCacheMissLoad",
"description": "This event is triggered when a cache miss occurs and a response has been retrieved successfully from the server\nfor the content to restore\n\nYou can modify the details before it makes the swap to restore the history\n\n##### Details\n\n* `detail.historyElt` - the history element or body that will get replaced\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.path` - the path and query of the page being restored\n* `detail.response` - the response text that will be swapped in\n* `detail.swapSpec` - the swapSpec to be used containing the defatul swapStyle='innerHTML'\n\n",
"doc-url": "https://htmx.org/events/#htmx:historyCacheMissLoad"
},
{
"name": "historyRestore",
"description": "This event is triggered when htmx handles a history restoration action\n\n##### Details\n\n* `detail.path` - the path and query of the page being restored\n* `detail.cacheMiss` - set `true` if restore was a cache miss\n* `detail.serverResponse` - with cache miss has the response text replaced\n* `detail.item` - with cache hit the cache details that was restored\n\n",
"doc-url": "https://htmx.org/events/#htmx:historyRestore"
},
{
"name": "beforeHistorySave",
"description": "This event is triggered before the content is saved in the history cache.\n\nYou can modify the contents of the historyElt to remove 3rd party javascript changes so a clean copy of the content can be backed up to the history cache\n\n##### Details\n\n* `detail.path` - the path and query of the page being saved\n* `detail.historyElt` - the history element about to be saved\n\n",
"doc-url": "https://htmx.org/events/#htmx:beforeHistorySave"
},
{
"name": "load",
"description": "This event is triggered when a new node is loaded into the DOM by htmx. Note that this event is also triggered when htmx is first initialized, with the document body as the target.\n\n##### Details\n\n* `detail.elt` - the newly added element\n\n",
"doc-url": "https://htmx.org/events/#htmx:load"
},
{
"name": "noSSESourceError",
"description": "This event is triggered when an element refers to an SSE event in its trigger, but no parent SSE source has been defined\n\n##### Details\n\n* `detail.elt` - the element with the bad SSE trigger\n\n",
"doc-url": "https://htmx.org/events/#htmx:noSSESourceError"
},
{
"name": "oobAfterSwap",
"description": "This event is triggered as part of an [out of band swap](@/docs.md#oob_swaps) and behaves identically to an [after swap event](#htmx:afterSwap)\n\n##### Details\n\n* `detail.elt` - the swapped in element\n* `detail.shouldSwap` - if the content will be swapped (defaults to `true`)\n* `detail.target` - the target of the swap\n* `detail.fragment` - the response fragment\n\n",
"doc-url": "https://htmx.org/events/#htmx:oobAfterSwap"
},
{
"name": "oobBeforeSwap",
"description": "This event is triggered as part of an [out of band swap](@/docs.md#oob_swaps) and behaves identically to a [before swap event](#htmx:beforeSwap)\n\n##### Details\n\n* `detail.elt` - the target of the swap\n* `detail.shouldSwap` - if the content will be swapped (defaults to `true`)\n* `detail.target` - the target of the swap\n* `detail.fragment` - the response fragment\n\n",
"doc-url": "https://htmx.org/events/#htmx:oobBeforeSwap"
},
{
"name": "oobErrorNoTarget",
"description": "This event is triggered when an [out of band swap](@/docs.md#oob_swaps) does not have a corresponding element\nin the DOM to switch with.\n\n##### Details\n\n* `detail.content` - the element with the bad oob `id`\n\n",
"doc-url": "https://htmx.org/events/#htmx:oobErrorNoTarget"
},
{
"name": "onLoadError",
"description": "This event is triggered when an error occurs during the `load` handling of an AJAX call\n\n##### Details\n\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.elt` - the element that triggered the request\n* `detail.target` - the target of the request\n* `detail.exception` - the exception that occurred\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:onLoadError"
},
{
"name": "prompt",
"description": "This event is triggered after a prompt has been shown to the user with the [`hx-prompt`](@/attributes/hx-prompt.md)\nattribute. If this event is cancelled, the AJAX request will not occur.\n\n##### Details\n\n* `detail.elt` - the element that triggered the request\n* `detail.target` - the target of the request\n* `detail.prompt` - the user response to the prompt\n\n",
"doc-url": "https://htmx.org/events/#htmx:prompt"
},
{
"name": "beforeHistoryUpdate",
"description": "This event is triggered before a history update is performed. It can be\nused to modify the `path` or `type` used to update the history.\n\n##### Details\n\n* `detail.history` - the `path` and `type` (push, replace) for the history update\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:beforeHistoryUpdate"
},
{
"name": "pushedIntoHistory",
"description": "This event is triggered after a URL has been pushed into history.\n\n##### Details\n\n* `detail.path` - the path and query of the URL that has been pushed into history\n\n",
"doc-url": "https://htmx.org/events/#htmx:pushedIntoHistory"
},
{
"name": "replacedInHistory",
"description": "This event is triggered after a URL has been replaced in history.\n\n##### Details\n\n* `detail.path` - the path and query of the URL that has been replaced in history\n\n",
"doc-url": "https://htmx.org/events/#htmx:replacedInHistory"
},
{
"name": "responseError",
"description": "This event is triggered when an HTTP error response occurs\n\n##### Details\n\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.elt` - the element that triggered the request\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:responseError"
},
{
"name": "sendAbort",
"description": "This event is triggered when a request is aborted\n\n##### Details\n\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.elt` - the element that triggered the request\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:sendAbort"
},
{
"name": "sendError",
"description": "This event is triggered when a network error prevents an HTTP request from occurring\n\n##### Details\n\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.elt` - the element that triggered the request\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:sendError"
},
{
"name": "sseError",
"description": "This event is triggered when an error occurs with an SSE source\n\n##### Details\n\n* `detail.elt` - the element with the bad SSE source\n* `detail.error` - the error\n* `detail.source` - the SSE source\n\n",
"doc-url": "https://htmx.org/events/#htmx:sseError"
},
{
"name": "swapError",
"description": "This event is triggered when an error occurs during the swap phase\n\n##### Details\n\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.elt` - the element that triggered the request\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:swapError"
},
{
"name": "targetError",
"description": "This event is triggered when a bad selector is used for a [`hx-target`](@/attributes/hx-target.md) attribute (e.g. an\nelement ID without a preceding `#`)\n\n##### Details\n\n* `detail.elt` - the element that triggered the request\n* `detail.target` - the bad CSS selector\n\n",
"doc-url": "https://htmx.org/events/#htmx:targetError"
},
{
"name": "timeout",
"description": "This event is triggered when a request timeout occurs. This wraps the typical `timeout` event of XMLHttpRequest.\n\nTimeout time can be set using `htmx.config.timeout` or per element using [`hx-request`](@/attributes/hx-request.md)\n\n##### Details\n\n* `detail.elt` - the element that dispatched the request\n* `detail.xhr` - the `XMLHttpRequest`\n* `detail.target` - the target of the request\n* `detail.requestConfig` - the configuration of the AJAX request\n\n",
"doc-url": "https://htmx.org/events/#htmx:timeout"
},
{
"name": "trigger",
"description": "This event is triggered whenever an AJAX request would be, even if no AJAX request is specified. It\nis primarily intended to allow `hx-trigger` to execute client-side scripts; AJAX requests have more\ngranular events available, like [`htmx:beforeRequest`](#htmx:beforeRequest) or [`htmx:afterRequest`](#htmx:afterRequest).\n\n##### Details\n\n* `detail.elt` - the element that triggered the request\n\n",
"doc-url": "https://htmx.org/events/#htmx:trigger"
},
{
"name": "validateUrl",
"description": "This event is triggered before a request is made, allowing you to validate the URL that htmx is going to request. If\n`preventDefault()` is invoked on the event, the request will not be made.\n\n```javascript\ndocument.body.addEventListener('htmx:validateUrl', function (evt) {\n // only allow requests to the current server as well as myserver.com\n if (!evt.detail.sameHost && evt.detail.url.hostname !== \"myserver.com\") {\n evt.preventDefault();\n }\n});\n```\n\n##### Details\n\n* `detail.elt` - the element that triggered the request\n* `detail.url` - the URL Object representing the URL that a request will be sent to.\n* `detail.sameHost` - will be `true` if the request is to the same host as the document\n\n",
"doc-url": "https://htmx.org/events/#htmx:validateUrl"
},
{
"name": "validation:validate",
"description": "This event is triggered before an element is validated. It can be used with the `elt.setCustomValidity()` method\nto implement custom validation rules.\n\n```html\n<form hx-post=\"/test\">\n <input _=\"on htmx:validation:validate\n if my.value != 'foo'\n call me.setCustomValidity('Please enter the value foo')\n else\n call me.setCustomValidity('')\"\n name=\"example\">\n</form>\n```\n\n##### Details\n\n* `detail.elt` - the element to be validated\n\n",
"doc-url": "https://htmx.org/events/#htmx:validation:validate"
},
{
"name": "validation:failed",
"description": "This event is triggered when an element fails validation. If `preventDefault()` is invoked on the event, the reportValidity() enabled by `htmx.config.reportValidityOfForms` will not be called.\n\n##### Details\n\n* `detail.elt` - the element that failed validation\n* `detail.message` - the validation error message\n* `detail.validity` - the validity object, which contains properties specifying how validation failed\n\n",
"doc-url": "https://htmx.org/events/#htmx:validation:failed"
},
{
"name": "validation:halted",
"description": "This event is triggered when a request is halted due to validation errors.\n\n##### Details\n\n* `detail.elt` - the element that triggered the request\n* `detail.errors` - an array of error objects with the invalid elements and errors associated with them\n\n",
"doc-url": "https://htmx.org/events/#htmx:validation:halted"
},
{
"name": "xhr:abort",
"description": "This event is triggered when an ajax request aborts\n\n##### Details\n\n* `detail.elt` - the element that triggered the request\n\n",
"doc-url": "https://htmx.org/events/#htmx:xhr:abort"
},
{
"name": "xhr:loadstart",
"description": "This event is triggered when an ajax request starts\n\n##### Details\n\n* `detail.elt` - the element that triggered the request\n\n",
"doc-url": "https://htmx.org/events/#htmx:xhr:loadstart"
},
{
"name": "xhr:loadend",
"description": "This event is triggered when an ajax request finishes\n\n##### Details\n\n* `detail.elt` - the element that triggered the request\n\n",
"doc-url": "https://htmx.org/events/#htmx:xhr:loadend"
},
{
"name": "xhr:progress",
"description": "This event is triggered periodically when an ajax request that supports progress is in flight\n\n##### Details\n\n* `detail.elt` - the element that triggered the request\n",
"doc-url": "https://htmx.org/events/#htmx:xhr:progress"
}
]
}
}
}
}

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
viewBox="0 0 256 256"
version="1.1"
id="svg287"
sodipodi:docname="htmx_dark.svg"
inkscape:version="1.2.1 (9c6d41e, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs291" />
<sodipodi:namedview
id="namedview289"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
showgrid="false"
inkscape:zoom="27.40625"
inkscape:cx="6.3854048"
inkscape:cy="7.3705815"
inkscape:window-width="1259"
inkscape:window-height="820"
inkscape:window-x="2060"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg287" />
<path
fill="#3d72d7"
d="m 92.80931,211.93595 47.9447,-171.265771 a 2.3748439,2.8675381 0 0 1 2.49698,-1.93354 l 19.88084,2.45789 a 2.3748439,2.8675381 0 0 1 2.022,3.75238 L 118.33548,213.8531 a 2.3748439,2.8675381 0 0 1 -2.2527,1.96631 l -21.034331,-0.0983 a 2.3748439,2.8675381 0 0 1 -2.239139,-3.78515 z"
id="path9"
style="stroke-width:1.49119" />
<path
fill="#333333"
d="m 33.763907,132.12007 c -1.592276,0.75375 -1.583229,1.4802 0.02714,2.17933 16.438443,7.17704 32.28883,13.91165 47.551159,20.20386 0.727419,0.30748 1.219852,1.12423 1.234919,2.04824 -0.214237,10.63702 -0.384508,19.1784 -0.401503,28.62622 -0.325692,1.01593 -1.52099,1.31634 -2.353315,0.90122 L 2.2395547,147.19513 c -0.35763,-0.18597 -0.58027,-0.61873 -0.5564,-1.08148 l 0.12381,-25.31188 c 0.0249,-0.63224 0.87445,-1.12426 1.70822,-1.57743 L 79.374479,79.963647 c 0.804287,-0.42127 2.617287,0.18269 2.875279,1.21475 -0.180356,10.46196 0.296376,20.583873 0.286657,29.345013 -0.018,0.48554 -0.274377,0.9112 -0.651386,1.08148 -16.752569,7.66439 -33.358905,14.70553 -48.121122,20.51518 z m 189.580383,-0.27856 -48.43324,-20.41687 c -0.0776,-10.64493 -0.0238,-13.917543 0.0176,-30.506823 0.1719,-0.5462 0.98658,-0.59914 1.44798,-0.44621 27.0655,12.61166 55.00987,27.040193 77.98987,38.588873 0.52473,0.26218 0.78709,0.73737 0.78709,1.42558 l 0.0407,25.7423 c -0.001,0.63571 -0.31626,1.2093 -0.80066,1.45835 l -77.22992,38.50694 c -1.24994,0.10087 -2.28748,-0.64701 -2.43862,-1.68019 -0.12039,-9.77693 -0.0127,-18.13379 -0.11264,-28.51909 0.0323,-0.59314 0.35336,-1.10357 0.81423,-1.29449 16.72794,-6.80562 32.70951,-13.7314 47.94471,-20.77736 1.56513,-0.72098 1.5561,-1.41465 -0.0271,-2.08101 z"
id="path11"
style="stroke-width:1.49119;fill:#f5f5f5;fill-opacity:1"
sodipodi:nodetypes="ccccccccccccccccccccccccccccsc" />
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,19 +0,0 @@
{
"compilerOptions": {
"allowJs": true,
"declaration": true,
"noEmit": false,
"emitDeclarationOnly": true,
"outFile": "src/htmx.d.ts",
"target": "es6",
"checkJs": true,
"lib": [
"dom",
"dom.iterable"
]
},
"include": [
"./src/htmx.js"
],
"verbose": true
}

View File

@@ -4,7 +4,7 @@ publish = "public"
command = "zola build"
[build.environment]
ZOLA_VERSION = "0.17.1"
ZOLA_VERSION = "0.19.1"
[context.deploy-preview]
command = "zola build --base-url $DEPLOY_PRIME_URL"

7986
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"AJAX",
"HTML"
],
"version": "2.0.0-beta1",
"version": "2.0.8",
"homepage": "https://htmx.org/",
"bugs": {
"url": "https://github.com/bigskysoftware/htmx/issues"
@@ -14,33 +14,45 @@
"files": [
"LICENSE",
"README.md",
"dist/htmx.d.ts",
"dist/htmx.esm.d.ts",
"dist/*.js",
"dist/ext/*.js",
"dist/*.js.gz",
"editors/jetbrains/htmx.web-types.json"
],
"main": "dist/htmx.min.js",
"types": "dist/htmx.d.ts",
"main": "dist/htmx.esm.js",
"types": "dist/htmx.esm.d.ts",
"jsdelivr": "dist/htmx.min.js",
"unpkg": "dist/htmx.min.js",
"web-types": "editors/jetbrains/htmx.web-types.json",
"scripts": {
"dist": "./scripts/dist.sh",
"lint": "eslint src/htmx.js test/attributes/ test/core/ test/util/",
"lint-fix": "eslint src/htmx.js test/attributes/ test/core/ test/util/ --fix",
"format": "eslint --fix src/htmx.js test/attributes/ test/core/ test/util/",
"test": "npm run lint && tsc --project ./jsconfig.json --noEmit true --emitDeclarationOnly false && mocha-chrome test/index.html",
"type-declarations": "tsc --project ./jsconfig.json",
"dist": "./scripts/dist.sh && npm run types-generate && npm run web-types-generate",
"lint": "eslint src/htmx.js test/attributes/ test/core/ test/util/ scripts/*.mjs",
"format": "eslint --fix src/htmx.js test/attributes/ test/core/ test/util/ scripts/*.mjs",
"types-check": "tsc src/htmx.js --noEmit --checkJs --target es6 --lib dom,dom.iterable --moduleResolution node",
"types-generate": "tsc dist/htmx.esm.js --declaration --emitDeclarationOnly --allowJs --outDir dist",
"test": "npm run lint && npm run types-check && npm run test:chrome",
"test:debug": "web-test-runner --manual --open",
"test:chrome": "playwright install chromium && web-test-runner --browsers chromium --playwright",
"test:firefox": "playwright install firefox && web-test-runner --concurrency 1 --browsers firefox --playwright",
"test:webkit": "playwright install webkit && web-test-runner --browsers webkit --playwright",
"test:all": "playwright install chromium firefox webkit && web-test-runner --concurrency 1 --browsers chromium firefox webkit --playwright",
"test:ci": "npm run lint && npm run types-check && npm run test:all",
"ws-tests": "cd ./test/ws-sse && node ./server.js",
"www": "bash ./scripts/www.sh"
"web-types-generate": "node ./scripts/generate-web-types.mjs",
"www": "bash ./scripts/www.sh",
"sha": "bash ./scripts/sha.sh"
},
"repository": {
"type": "git",
"url": "git+https://github.com/bigskysoftware/htmx.git"
},
"eslintConfig": {
"extends": "standard",
"extends": ["standard", "plugin:mocha/recommended"],
"rules": {
"mocha/consistent-spacing-between-blocks": 0,
"mocha/no-setup-in-describe": 0,
"mocha/no-skipped-tests": 0,
"camelcase": 0,
"no-var": 0,
"no-undef": 0,
@@ -57,6 +69,10 @@
"no-useless-call": 0,
"no-useless-escape": 0,
"no-unused-expressions": 0,
"no-restricted-properties": ["error", {
"property": "substr",
"message": "Use .slice or .substring instead of .substr"
}],
"space-before-function-paren": [
"error",
"never"
@@ -64,19 +80,22 @@
}
},
"devDependencies": {
"chai": "^4.3.10",
"chai-dom": "^1.12.0",
"eslint": "^8.56.0",
"@types/node": "^22.18.8",
"@types/parse5": "^7.0.0",
"@web/test-runner": "^0.20.2",
"@web/test-runner-playwright": "^0.11.0",
"chai": "^4.5.0",
"chai-dom": "^1.12.1",
"eslint": "^8.57.1",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-mocha": "^10.5.0",
"fs-extra": "^9.1.0",
"mocha": "10.1.0",
"mocha-chrome": "https://github.com/Telroshan/mocha-chrome",
"mocha-webdriver": "^0.3.2",
"mocha": "^11.7.4",
"mock-socket": "^9.3.1",
"sinon": "^9.2.4",
"typescript": "^4.9.5",
"uglify-js": "^3.17.4",
"ws": "^8.14.2"
"sinon": "^10.0.1",
"typescript": "^5.9.3",
"uglify-js": "^3.19.3",
"ws": "^8.18.1"
}
}

View File

@@ -2,21 +2,21 @@
# This script is intended to be run from npm, via `npm run dist`
set -euo pipefail
HTMX_SRC=src/htmx.js
HTMX_SRC="src/htmx.js"
# Clean the dist directory
rm -rf dist/*
rm -rf dist/*.js dist/*.ts dist/*.gz
# Regular IIFE script
cp $HTMX_SRC dist/htmx.js
# Minified script
# Generate minified script
uglifyjs -m eval -o dist/htmx.min.js dist/htmx.js
# Gzipped script
# Generate gzipped script
gzip -9 -k -f dist/htmx.min.js > dist/htmx.min.js.gz
# AMD script
# Generate AMD script
cat > dist/htmx.amd.js << EOF
define(() => {
$(cat $HTMX_SRC)
@@ -24,14 +24,15 @@ return htmx
})
EOF
# CJS script
# Generate CJS script
cat > dist/htmx.cjs.js << EOF
$(cat $HTMX_SRC)
module.exports = htmx;
EOF
# ESM script
# Generate ESM script
cat > dist/htmx.esm.js << EOF
$(cat $HTMX_SRC)
export { htmx }
export default htmx
EOF

View File

@@ -0,0 +1,145 @@
import fs from 'fs'
const classes = []
const attributes = []
const events = []
const rootPath = fs.existsSync('./www') ? './' : '../'
for (const file of fs.readdirSync(rootPath + 'www/content/attributes').sort()) {
if (file.startsWith('hx-') && file.endsWith('.md')) {
const name = file.slice(0, -3)
const info = readAttributeInfo(name, rootPath + 'www/content/attributes/' + file)
attributes.push({
name,
...info,
'doc-url': 'https://htmx.org/attributes/' + name + '/'
})
}
}
readClassInfo()
readEventInfo()
const pkg = JSON.parse(fs.readFileSync(rootPath + 'package.json', { encoding: 'utf8' }))
const webTypes = {
$schema: 'https://json.schemastore.org/web-types',
name: 'htmx',
version: pkg.version,
'default-icon': './htmx.svg',
'js-types-syntax': 'typescript',
'description-markup': 'markdown',
contributions: {
html: {
attributes
},
css: {
classes
},
js: {
events: [
{
name: 'HTMX event',
pattern: {
items: ['/js/htmx-events'],
template: ['htmx:', '$...', '#item:HTMX event']
}
}
],
'htmx-events': events
}
}
}
fs.writeFileSync(rootPath + 'editors/jetbrains/htmx.web-types.json', JSON.stringify(webTypes, null, 2))
function readAttributeInfo(name, file) {
const content = fs.readFileSync(file, { encoding: 'utf8' })
const isInherited = content.indexOf('`' + name + '` is inherited') !== -1
const isNotInherited = content.indexOf('`' + name + '` is not inherited') !== -1
const deprecated = content.indexOf('`' + name + '` has been deprecated') !== -1
const sections = {}
if (isInherited) {
sections.Inherited = ''
} else if (isNotInherited) {
sections['Not Inherited'] = ''
}
const descSections = /\+\+\+\n(?:[^\n]*\n)+\+\+\+\n\n((?:[^\n]+\n)+)(?:\n((?:[^\n]+\n)+))?(?:\n((?:[^\n]+\n)+))?/mg.exec(content)
const para1 = descSections[1].trim()
const para2 = descSections[2]?.trim()
const para3 = descSections[3]?.trim()
let description = para1
if (para2) {
description += '\n\n' + para2
}
if (para2 && para2.endsWith(':') && para3) {
description += '\n\n' + para3
}
let pattern
if (name === 'hx-on') {
pattern = {
or: [
{
items: ['/js/events'],
template: ['hx-on:', '#...', '#item:JS event']
},
{
items: ['/js/htmx-events'],
template: ['hx-on::', '#...', '#item:HTMX event']
}
]
}
}
return {
pattern,
description,
'description-sections': sections,
deprecated: deprecated ? true : undefined
}
}
function readClassInfo() {
const content = fs.readFileSync(rootPath + 'www/content/reference.md', { encoding: 'utf8' })
const start = content.indexOf('| Class | Description |')
const cssTable = content.slice(start, content.indexOf('</div>', start))
const expr = /\| `([^`]+)` \| ([^\n]+)/mg
let match = expr.exec(cssTable)
while (match) {
const name = match[1]
if (name && name.startsWith('htmx-')) {
classes.push({
name,
description: match[2].trim(),
'doc-url': 'https://htmx.org/reference/#classes'
})
}
match = expr.exec(cssTable)
}
}
function readEventInfo() {
const content = fs.readFileSync(rootPath + 'www/content/events.md', { encoding: 'utf8' })
const expr = /### Event - `([^`]+)`[^\n]*\n+((?:(?:[^#\n]|#####)[^\n]*\n+)+)/mg
let match = expr.exec(content)
while (match) {
let name = match[1]
if (name && name.startsWith('htmx:')) {
name = name.slice(5)
events.push({
name,
description: match[2],
'doc-url': 'https://htmx.org/events/#htmx:' + name
})
}
match = expr.exec(content)
}
}

View File

@@ -1,2 +1,6 @@
echo "htmx.min.js:"
cat dist/htmx.min.js | openssl dgst -sha384 -binary | openssl base64 -A
echo ""
echo ""
echo "htmx.js:"
cat dist/htmx.js | openssl dgst -sha384 -binary | openssl base64 -A
echo ""

View File

@@ -1,7 +0,0 @@
htmx.defineExtension('ajax-header', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
evt.detail.headers['X-Requested-With'] = 'XMLHttpRequest';
}
}
});

View File

@@ -1,92 +0,0 @@
(function () {
function splitOnWhitespace(trigger) {
return trigger.split(/\s+/);
}
function parseClassOperation(trimmedValue) {
var split = splitOnWhitespace(trimmedValue);
if (split.length > 1) {
var operation = split[0];
var classDef = split[1].trim();
var cssClass;
var delay;
if (classDef.indexOf(":") > 0) {
var splitCssClass = classDef.split(':');
cssClass = splitCssClass[0];
delay = htmx.parseInterval(splitCssClass[1]);
} else {
cssClass = classDef;
delay = 100;
}
return {
operation: operation,
cssClass: cssClass,
delay: delay
}
} else {
return null;
}
}
function performOperation(elt, classOperation, classList, currentRunTime) {
setTimeout(function () {
elt.classList[classOperation.operation].call(elt.classList, classOperation.cssClass);
}, currentRunTime)
}
function toggleOperation(elt, classOperation, classList, currentRunTime) {
setTimeout(function () {
setInterval(function () {
elt.classList[classOperation.operation].call(elt.classList, classOperation.cssClass);
}, classOperation.delay);
}, currentRunTime)
}
function processClassList(elt, classList) {
var runs = classList.split("&");
for (var i = 0; i < runs.length; i++) {
var run = runs[i];
var currentRunTime = 0;
var classOperations = run.split(",");
for (var j = 0; j < classOperations.length; j++) {
var value = classOperations[j];
var trimmedValue = value.trim();
var classOperation = parseClassOperation(trimmedValue);
if (classOperation) {
if (classOperation.operation === "toggle") {
toggleOperation(elt, classOperation, classList, currentRunTime);
currentRunTime = currentRunTime + classOperation.delay;
} else {
currentRunTime = currentRunTime + classOperation.delay;
performOperation(elt, classOperation, classList, currentRunTime);
}
}
}
}
}
function maybeProcessClasses(elt) {
if (elt.getAttribute) {
var classList = elt.getAttribute("classes") || elt.getAttribute("data-classes");
if (classList) {
processClassList(elt, classList);
}
}
}
htmx.defineExtension('class-tools', {
onEvent: function (name, evt) {
if (name === "htmx:afterProcessNode") {
var elt = evt.detail.elt;
maybeProcessClasses(elt);
if (elt.querySelectorAll) {
var children = elt.querySelectorAll("[classes], [data-classes]");
for (var i = 0; i < children.length; i++) {
maybeProcessClasses(children[i]);
}
}
}
}
});
})();

View File

@@ -1,96 +0,0 @@
htmx.defineExtension('client-side-templates', {
transformResponse : function(text, xhr, elt) {
var mustacheTemplate = htmx.closest(elt, "[mustache-template]");
if (mustacheTemplate) {
var data = JSON.parse(text);
var templateId = mustacheTemplate.getAttribute('mustache-template');
var template = htmx.find("#" + templateId);
if (template) {
return Mustache.render(template.innerHTML, data);
} else {
throw "Unknown mustache template: " + templateId;
}
}
var mustacheArrayTemplate = htmx.closest(elt, "[mustache-array-template]");
if (mustacheArrayTemplate) {
var data = JSON.parse(text);
var templateId = mustacheArrayTemplate.getAttribute('mustache-array-template');
var template = htmx.find("#" + templateId);
if (template) {
return Mustache.render(template.innerHTML, {"data": data });
} else {
throw "Unknown mustache template: " + templateId;
}
}
var handlebarsTemplate = htmx.closest(elt, "[handlebars-template]");
if (handlebarsTemplate) {
var data = JSON.parse(text);
var templateId = handlebarsTemplate.getAttribute('handlebars-template');
var templateElement = htmx.find('#' + templateId).innerHTML;
var renderTemplate = Handlebars.compile(templateElement);
if (renderTemplate) {
return renderTemplate(data);
} else {
throw "Unknown handlebars template: " + templateId;
}
}
var handlebarsArrayTemplate = htmx.closest(elt, "[handlebars-array-template]");
if (handlebarsArrayTemplate) {
var data = JSON.parse(text);
var templateId = handlebarsArrayTemplate.getAttribute('handlebars-array-template');
var templateElement = htmx.find('#' + templateId).innerHTML;
var renderTemplate = Handlebars.compile(templateElement);
if (renderTemplate) {
return renderTemplate(data);
} else {
throw "Unknown handlebars template: " + templateId;
}
}
var nunjucksTemplate = htmx.closest(elt, "[nunjucks-template]");
if (nunjucksTemplate) {
var data = JSON.parse(text);
var templateName = nunjucksTemplate.getAttribute('nunjucks-template');
var template = htmx.find('#' + templateName);
if (template) {
return nunjucks.renderString(template.innerHTML, data);
} else {
return nunjucks.render(templateName, data);
}
}
var xsltTemplate = htmx.closest(elt, "[xslt-template]");
if (xsltTemplate) {
var templateId = xsltTemplate.getAttribute('xslt-template');
var template = htmx.find("#" + templateId);
if (template) {
var content = template.innerHTML ? new DOMParser().parseFromString(template.innerHTML, 'application/xml')
: template.contentDocument;
var processor = new XSLTProcessor();
processor.importStylesheet(content);
var data = new DOMParser().parseFromString(text, "application/xml");
var frag = processor.transformToFragment(data, document);
return new XMLSerializer().serializeToString(frag);
} else {
throw "Unknown XSLT template: " + templateId;
}
}
var nunjucksArrayTemplate = htmx.closest(elt, "[nunjucks-array-template]");
if (nunjucksArrayTemplate) {
var data = JSON.parse(text);
var templateName = nunjucksArrayTemplate.getAttribute('nunjucks-array-template');
var template = htmx.find('#' + templateName);
if (template) {
return nunjucks.renderString(template.innerHTML, {"data": data});
} else {
return nunjucks.render(templateName, {"data": data});
}
}
return text;
}
});

View File

@@ -1,11 +0,0 @@
htmx.defineExtension('debug', {
onEvent: function (name, evt) {
if (console.debug) {
console.debug(name, evt);
} else if (console) {
console.log("DEBUG:", name, evt);
} else {
throw "NO CONSOLE SUPPORTED"
}
}
});

View File

@@ -1,18 +0,0 @@
"use strict";
// Disable Submit Button
htmx.defineExtension('disable-element', {
onEvent: function (name, evt) {
let elt = evt.detail.elt;
let target = elt.getAttribute("hx-disable-element");
let targetElements = (target == "self") ? [ elt ] : document.querySelectorAll(target);
for (var i = 0; i < targetElements.length; i++) {
if (name === "htmx:beforeRequest" && targetElements[i]) {
targetElements[i].disabled = true;
} else if (name == "htmx:afterRequest" && targetElements[i]) {
targetElements[i].disabled = false;
}
}
}
});

View File

@@ -1,37 +0,0 @@
(function(){
function stringifyEvent(event) {
var obj = {};
for (var key in event) {
obj[key] = event[key];
}
return JSON.stringify(obj, function(key, value){
if(value instanceof Node){
var nodeRep = value.tagName;
if (nodeRep) {
nodeRep = nodeRep.toLowerCase();
if(value.id){
nodeRep += "#" + value.id;
}
if(value.classList && value.classList.length){
nodeRep += "." + value.classList.toString().replace(" ", ".")
}
return nodeRep;
} else {
return "Node"
}
}
if (value instanceof Window) return 'Window';
return value;
});
}
htmx.defineExtension('event-header', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
if (evt.detail.triggeringEvent) {
evt.detail.headers['Triggering-Event'] = stringifyEvent(evt.detail.triggeringEvent);
}
}
}
});
})();

View File

@@ -1,141 +0,0 @@
//==========================================================
// head-support.js
//
// An extension to htmx 1.0 to add head tag merging.
//==========================================================
(function(){
var api = null;
function log() {
//console.log(arguments);
}
function mergeHead(newContent, defaultMergeStrategy) {
if (newContent && newContent.indexOf('<head') > -1) {
const htmlDoc = document.createElement("html");
// remove svgs to avoid conflicts
var contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
// extract head tag
var headTag = contentWithSvgsRemoved.match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im);
// if the head tag exists...
if (headTag) {
var added = []
var removed = []
var preserved = []
var nodesToAppend = []
htmlDoc.innerHTML = headTag;
var newHeadTag = htmlDoc.querySelector("head");
var currentHead = document.head;
if (newHeadTag == null) {
return;
} else {
// put all new head elements into a Map, by their outerHTML
var srcToNewHeadNodes = new Map();
for (const newHeadChild of newHeadTag.children) {
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
}
}
// determine merge strategy
var mergeStrategy = api.getAttributeValue(newHeadTag, "hx-head") || defaultMergeStrategy;
// get the current head
for (const currentHeadElt of currentHead.children) {
// If the current head element is in the map
var inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
var isReAppended = currentHeadElt.getAttribute("hx-head") === "re-eval";
var isPreserved = api.getAttributeValue(currentHeadElt, "hx-preserve") === "true";
if (inNewContent || isPreserved) {
if (isReAppended) {
// remove the current version and let the new version replace it and re-execute
removed.push(currentHeadElt);
} else {
// this element already exists and should not be re-appended, so remove it from
// the new content map, preserving it in the DOM
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
preserved.push(currentHeadElt);
}
} else {
if (mergeStrategy === "append") {
// we are appending and this existing element is not new content
// so if and only if it is marked for re-append do we do anything
if (isReAppended) {
removed.push(currentHeadElt);
nodesToAppend.push(currentHeadElt);
}
} else {
// if this is a merge, we remove this content since it is not in the new head
if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: currentHeadElt}) !== false) {
removed.push(currentHeadElt);
}
}
}
}
// Push the tremaining new head elements in the Map into the
// nodes to append to the head tag
nodesToAppend.push(...srcToNewHeadNodes.values());
log("to append: ", nodesToAppend);
for (const newNode of nodesToAppend) {
log("adding: ", newNode);
var newElt = document.createRange().createContextualFragment(newNode.outerHTML);
log(newElt);
if (api.triggerEvent(document.body, "htmx:addingHeadElement", {headElement: newElt}) !== false) {
currentHead.appendChild(newElt);
added.push(newElt);
}
}
// remove all removed elements, after we have appended the new elements to avoid
// additional network requests for things like style sheets
for (const removedElement of removed) {
if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: removedElement}) !== false) {
currentHead.removeChild(removedElement);
}
}
api.triggerEvent(document.body, "htmx:afterHeadMerge", {added: added, kept: preserved, removed: removed});
}
}
}
htmx.defineExtension("head-support", {
init: function(apiRef) {
// store a reference to the internal API.
api = apiRef;
htmx.on('htmx:afterSwap', function(evt){
var serverResponse = evt.detail.xhr.response;
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
mergeHead(serverResponse, evt.detail.boosted ? "merge" : "append");
}
})
htmx.on('htmx:historyRestore', function(evt){
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
if (evt.detail.cacheMiss) {
mergeHead(evt.detail.serverResponse, "merge");
} else {
mergeHead(evt.detail.item.head, "merge");
}
}
})
htmx.on('htmx:historyItemCreated', function(evt){
var historyItem = evt.detail.item;
historyItem.head = document.head.outerHTML;
})
}
});
})()

View File

@@ -1,24 +0,0 @@
(function(){
function mergeObjects(obj1, obj2) {
for (var key in obj2) {
if (obj2.hasOwnProperty(key)) {
obj1[key] = obj2[key];
}
}
return obj1;
}
htmx.defineExtension('include-vals', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
var includeValsElt = htmx.closest(evt.detail.elt, "[include-vals],[data-include-vals]");
if (includeValsElt) {
var includeVals = includeValsElt.getAttribute("include-vals") || includeValsElt.getAttribute("data-include-vals");
var valuesToInclude = eval("({" + includeVals + "})");
mergeObjects(evt.detail.parameters, valuesToInclude);
}
}
}
});
})();

View File

@@ -1,12 +0,0 @@
htmx.defineExtension('json-enc', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
evt.detail.headers['Content-Type'] = "application/json";
}
},
encodeParameters : function(xhr, parameters, elt) {
xhr.overrideMimeType('text/json');
return (JSON.stringify(parameters));
}
});

View File

@@ -1,183 +0,0 @@
;(function () {
let loadingStatesUndoQueue = []
function loadingStateContainer(target) {
return htmx.closest(target, '[data-loading-states]') || document.body
}
function mayProcessUndoCallback(target, callback) {
if (document.body.contains(target)) {
callback()
}
}
function mayProcessLoadingStateByPath(elt, requestPath) {
const pathElt = htmx.closest(elt, '[data-loading-path]')
if (!pathElt) {
return true
}
return pathElt.getAttribute('data-loading-path') === requestPath
}
function queueLoadingState(sourceElt, targetElt, doCallback, undoCallback) {
const delayElt = htmx.closest(sourceElt, '[data-loading-delay]')
if (delayElt) {
const delayInMilliseconds =
delayElt.getAttribute('data-loading-delay') || 200
const timeout = setTimeout(function () {
doCallback()
loadingStatesUndoQueue.push(function () {
mayProcessUndoCallback(targetElt, undoCallback)
})
}, delayInMilliseconds)
loadingStatesUndoQueue.push(function () {
mayProcessUndoCallback(targetElt, function () { clearTimeout(timeout) })
})
} else {
doCallback()
loadingStatesUndoQueue.push(function () {
mayProcessUndoCallback(targetElt, undoCallback)
})
}
}
function getLoadingStateElts(loadingScope, type, path) {
return Array.from(htmx.findAll(loadingScope, "[" + type + "]")).filter(
function (elt) { return mayProcessLoadingStateByPath(elt, path) }
)
}
function getLoadingTarget(elt) {
if (elt.getAttribute('data-loading-target')) {
return Array.from(
htmx.findAll(elt.getAttribute('data-loading-target'))
)
}
return [elt]
}
htmx.defineExtension('loading-states', {
onEvent: function (name, evt) {
if (name === 'htmx:beforeRequest') {
const container = loadingStateContainer(evt.target)
const loadingStateTypes = [
'data-loading',
'data-loading-class',
'data-loading-class-remove',
'data-loading-disable',
'data-loading-aria-busy',
]
let loadingStateEltsByType = {}
loadingStateTypes.forEach(function (type) {
loadingStateEltsByType[type] = getLoadingStateElts(
container,
type,
evt.detail.pathInfo.requestPath
)
})
loadingStateEltsByType['data-loading'].forEach(function (sourceElt) {
getLoadingTarget(sourceElt).forEach(function (targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function () {
targetElt.style.display =
sourceElt.getAttribute('data-loading') ||
'inline-block' },
function () { targetElt.style.display = 'none' }
)
})
})
loadingStateEltsByType['data-loading-class'].forEach(
function (sourceElt) {
const classNames = sourceElt
.getAttribute('data-loading-class')
.split(' ')
getLoadingTarget(sourceElt).forEach(function (targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function () {
classNames.forEach(function (className) {
targetElt.classList.add(className)
})
},
function() {
classNames.forEach(function (className) {
targetElt.classList.remove(className)
})
}
)
})
}
)
loadingStateEltsByType['data-loading-class-remove'].forEach(
function (sourceElt) {
const classNames = sourceElt
.getAttribute('data-loading-class-remove')
.split(' ')
getLoadingTarget(sourceElt).forEach(function (targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function () {
classNames.forEach(function (className) {
targetElt.classList.remove(className)
})
},
function() {
classNames.forEach(function (className) {
targetElt.classList.add(className)
})
}
)
})
}
)
loadingStateEltsByType['data-loading-disable'].forEach(
function (sourceElt) {
getLoadingTarget(sourceElt).forEach(function (targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function() { targetElt.disabled = true },
function() { targetElt.disabled = false }
)
})
}
)
loadingStateEltsByType['data-loading-aria-busy'].forEach(
function (sourceElt) {
getLoadingTarget(sourceElt).forEach(function (targetElt) {
queueLoadingState(
sourceElt,
targetElt,
function () { targetElt.setAttribute("aria-busy", "true") },
function () { targetElt.removeAttribute("aria-busy") }
)
})
}
)
}
if (name === 'htmx:beforeOnLoad') {
while (loadingStatesUndoQueue.length > 0) {
loadingStatesUndoQueue.shift()()
}
}
},
})
})()

View File

@@ -1,11 +0,0 @@
htmx.defineExtension('method-override', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
var method = evt.detail.verb;
if (method !== "get" || method !== "post") {
evt.detail.headers['X-HTTP-Method-Override'] = method.toUpperCase();
evt.detail.verb = "post";
}
}
}
});

View File

@@ -1,17 +0,0 @@
htmx.defineExtension('morphdom-swap', {
isInlineSwap: function(swapStyle) {
return swapStyle === 'morphdom';
},
handleSwap: function (swapStyle, target, fragment) {
if (swapStyle === 'morphdom') {
if (fragment.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
// IE11 doesn't support DocumentFragment.firstElementChild
morphdom(target, fragment.firstElementChild || fragment.firstChild);
return [target];
} else {
morphdom(target, fragment.outerHTML);
return [target];
}
}
}
});

View File

@@ -1,45 +0,0 @@
(function () {
/** @type {import("../htmx").HtmxInternalApi} */
var api;
htmx.defineExtension('multi-swap', {
init: function (apiRef) {
api = apiRef;
},
isInlineSwap: function (swapStyle) {
return swapStyle.indexOf('multi:') === 0;
},
handleSwap: function (swapStyle, target, fragment, settleInfo) {
if (swapStyle.indexOf('multi:') === 0) {
var selectorToSwapStyle = {};
var elements = swapStyle.replace(/^multi\s*:\s*/, '').split(/\s*,\s*/);
elements.map(function (element) {
var split = element.split(/\s*:\s*/);
var elementSelector = split[0];
var elementSwapStyle = typeof (split[1]) !== "undefined" ? split[1] : "innerHTML";
if (elementSelector.charAt(0) !== '#') {
console.error("HTMX multi-swap: unsupported selector '" + elementSelector + "'. Only ID selectors starting with '#' are supported.");
return;
}
selectorToSwapStyle[elementSelector] = elementSwapStyle;
});
for (var selector in selectorToSwapStyle) {
var swapStyle = selectorToSwapStyle[selector];
var elementToSwap = fragment.querySelector(selector);
if (elementToSwap) {
api.oobSwap(swapStyle, elementToSwap, settleInfo);
} else {
console.warn("HTMX multi-swap: selector '" + selector + "' not found in source content.");
}
}
return true;
}
}
});
})();

View File

@@ -1,60 +0,0 @@
(function(undefined){
'use strict';
// Save a reference to the global object (window in the browser)
var _root = this;
function dependsOn(pathSpec, url) {
if (pathSpec === "ignore") {
return false;
}
var dependencyPath = pathSpec.split("/");
var urlPath = url.split("/");
for (var i = 0; i < urlPath.length; i++) {
var dependencyElement = dependencyPath.shift();
var pathElement = urlPath[i];
if (dependencyElement !== pathElement && dependencyElement !== "*") {
return false;
}
if (dependencyPath.length === 0 || (dependencyPath.length === 1 && dependencyPath[0] === "")) {
return true;
}
}
return false;
}
function refreshPath(path) {
var eltsWithDeps = htmx.findAll("[path-deps]");
for (var i = 0; i < eltsWithDeps.length; i++) {
var elt = eltsWithDeps[i];
if (dependsOn(elt.getAttribute('path-deps'), path)) {
htmx.trigger(elt, "path-deps");
}
}
}
htmx.defineExtension('path-deps', {
onEvent: function (name, evt) {
if (name === "htmx:beforeOnLoad") {
var config = evt.detail.requestConfig;
// mutating call
if (config.verb !== "get" && evt.target.getAttribute('path-deps') !== 'ignore') {
refreshPath(config.path);
}
}
}
});
/**
* ********************
* Expose functionality
* ********************
*/
_root.PathDeps = {
refresh: function(path) {
refreshPath(path);
}
};
}).call(this);

View File

@@ -1,11 +0,0 @@
htmx.defineExtension('path-params', {
onEvent: function(name, evt) {
if (name === "htmx:configRequest") {
evt.detail.path = evt.detail.path.replace(/{([^}]+)}/g, function (_, param) {
var val = evt.detail.parameters[param];
delete evt.detail.parameters[param];
return val === undefined ? "{" + param + "}" : encodeURIComponent(val);
})
}
}
});

View File

@@ -1,27 +0,0 @@
(function(){
function maybeRemoveMe(elt) {
var timing = elt.getAttribute("remove-me") || elt.getAttribute("data-remove-me");
if (timing) {
setTimeout(function () {
elt.parentElement.removeChild(elt);
}, htmx.parseInterval(timing));
}
}
htmx.defineExtension('remove-me', {
onEvent: function (name, evt) {
if (name === "htmx:afterProcessNode") {
var elt = evt.detail.elt;
if (elt.getAttribute) {
maybeRemoveMe(elt);
if (elt.querySelectorAll) {
var children = elt.querySelectorAll("[remove-me], [data-remove-me]");
for (var i = 0; i < children.length; i++) {
maybeRemoveMe(children[i]);
}
}
}
}
}
});
})();

View File

@@ -1,130 +0,0 @@
(function(){
/** @type {import("../htmx").HtmxInternalApi} */
var api;
var attrPrefix = 'hx-target-';
// IE11 doesn't support string.startsWith
function startsWith(str, prefix) {
return str.substring(0, prefix.length) === prefix
}
/**
* @param {HTMLElement} elt
* @param {number} respCode
* @returns {HTMLElement | null}
*/
function getRespCodeTarget(elt, respCodeNumber) {
if (!elt || !respCodeNumber) return null;
var respCode = respCodeNumber.toString();
// '*' is the original syntax, as the obvious character for a wildcard.
// The 'x' alternative was added for maximum compatibility with HTML
// templating engines, due to ambiguity around which characters are
// supported in HTML attributes.
//
// Start with the most specific possible attribute and generalize from
// there.
var attrPossibilities = [
respCode,
respCode.substr(0, 2) + '*',
respCode.substr(0, 2) + 'x',
respCode.substr(0, 1) + '*',
respCode.substr(0, 1) + 'x',
respCode.substr(0, 1) + '**',
respCode.substr(0, 1) + 'xx',
'*',
'x',
'***',
'xxx',
];
if (startsWith(respCode, '4') || startsWith(respCode, '5')) {
attrPossibilities.push('error');
}
for (var i = 0; i < attrPossibilities.length; i++) {
var attr = attrPrefix + attrPossibilities[i];
var attrValue = api.getClosestAttributeValue(elt, attr);
if (attrValue) {
if (attrValue === "this") {
return api.findThisElement(elt, attr);
} else {
return api.querySelectorExt(elt, attrValue);
}
}
}
return null;
}
/** @param {Event} evt */
function handleErrorFlag(evt) {
if (evt.detail.isError) {
if (htmx.config.responseTargetUnsetsError) {
evt.detail.isError = false;
}
} else if (htmx.config.responseTargetSetsError) {
evt.detail.isError = true;
}
}
htmx.defineExtension('response-targets', {
/** @param {import("../htmx").HtmxInternalApi} apiRef */
init: function (apiRef) {
api = apiRef;
if (htmx.config.responseTargetUnsetsError === undefined) {
htmx.config.responseTargetUnsetsError = true;
}
if (htmx.config.responseTargetSetsError === undefined) {
htmx.config.responseTargetSetsError = false;
}
if (htmx.config.responseTargetPrefersExisting === undefined) {
htmx.config.responseTargetPrefersExisting = false;
}
if (htmx.config.responseTargetPrefersRetargetHeader === undefined) {
htmx.config.responseTargetPrefersRetargetHeader = true;
}
},
/**
* @param {string} name
* @param {Event} evt
*/
onEvent: function (name, evt) {
if (name === "htmx:beforeSwap" &&
evt.detail.xhr &&
evt.detail.xhr.status !== 200) {
if (evt.detail.target) {
if (htmx.config.responseTargetPrefersExisting) {
evt.detail.shouldSwap = true;
handleErrorFlag(evt);
return true;
}
if (htmx.config.responseTargetPrefersRetargetHeader &&
evt.detail.xhr.getAllResponseHeaders().match(/HX-Retarget:/i)) {
evt.detail.shouldSwap = true;
handleErrorFlag(evt);
return true;
}
}
if (!evt.detail.requestConfig) {
return true;
}
var target = getRespCodeTarget(evt.detail.requestConfig.elt, evt.detail.xhr.status);
if (target) {
handleErrorFlag(evt);
evt.detail.shouldSwap = true;
evt.detail.target = target;
}
return true;
}
}
});
})();

View File

@@ -1,313 +0,0 @@
/*
Server Sent Events Extension
============================
This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
*/
(function() {
/** @type {import("../htmx").HtmxInternalApi} */
var api;
htmx.defineExtension("sse", {
/**
* Init saves the provided reference to the internal HTMX API.
*
* @param {import("../htmx").HtmxInternalApi} api
* @returns void
*/
init: function(apiRef) {
// store a reference to the internal API.
api = apiRef;
// set a function in the public API for creating new EventSource objects
if (htmx.createEventSource == undefined) {
htmx.createEventSource = createEventSource;
}
},
/**
* onEvent handles all events passed to this extension.
*
* @param {string} name
* @param {Event} evt
* @returns void
*/
onEvent: function(name, evt) {
switch (name) {
case "htmx:beforeCleanupElement":
var internalData = api.getInternalData(evt.target)
// Try to remove remove an EventSource when elements are removed
if (internalData.sseEventSource) {
internalData.sseEventSource.close();
}
return;
// Try to create EventSources when elements are processed
case "htmx:afterProcessNode":
ensureEventSourceOnElement(evt.target);
registerSSE(evt.target);
}
}
});
///////////////////////////////////////////////
// HELPER FUNCTIONS
///////////////////////////////////////////////
/**
* createEventSource is the default method for creating new EventSource objects.
* it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
*
* @param {string} url
* @returns EventSource
*/
function createEventSource(url) {
return new EventSource(url, { withCredentials: true });
}
/**
* registerSSE looks for attributes that can contain sse events, right
* now hx-trigger and sse-swap and adds listeners based on these attributes too
* the closest event source
*
* @param {HTMLElement} elt
*/
function registerSSE(elt) {
// Find closest existing event source
var sourceElement = api.getClosestMatch(elt, hasEventSource);
if (sourceElement == null) {
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
return null; // no eventsource in parentage, orphaned element
}
// Set internalData and source
var internalData = api.getInternalData(sourceElement);
var source = internalData.sseEventSource;
// Add message handlers for every `sse-swap` attribute
queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
var sseSwapAttr = api.getAttributeValue(child, "sse-swap");
var sseEventNames = sseSwapAttr.split(",");
for (var i = 0; i < sseEventNames.length; i++) {
var sseEventName = sseEventNames[i].trim();
var listener = function(event) {
// If the source is missing then close SSE
if (maybeCloseSSESource(sourceElement)) {
return;
}
// If the body no longer contains the element, remove the listener
if (!api.bodyContains(child)) {
source.removeEventListener(sseEventName, listener);
}
// swap the response into the DOM and trigger a notification
swap(child, event.data);
api.triggerEvent(elt, "htmx:sseMessage", event);
};
// Register the new listener
api.getInternalData(child).sseEventListener = listener;
source.addEventListener(sseEventName, listener);
}
});
// Add message handlers for every `hx-trigger="sse:*"` attribute
queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) {
var sseEventName = api.getAttributeValue(child, "hx-trigger");
if (sseEventName == null) {
return;
}
// Only process hx-triggers for events with the "sse:" prefix
if (sseEventName.slice(0, 4) != "sse:") {
return;
}
var listener = function(event) {
if (maybeCloseSSESource(sourceElement)) {
return
}
if (!api.bodyContains(child)) {
source.removeEventListener(sseEventName, listener);
}
// Trigger events to be handled by the rest of htmx
htmx.trigger(child, sseEventName, event);
htmx.trigger(child, "htmx:sseMessage", event);
}
// Register the new listener
api.getInternalData(elt).sseEventListener = listener;
source.addEventListener(sseEventName.slice(4), listener);
});
}
/**
* ensureEventSourceOnElement creates a new EventSource connection on the provided element.
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
* is created and stored in the element's internalData.
* @param {HTMLElement} elt
* @param {number} retryCount
* @returns {EventSource | null}
*/
function ensureEventSourceOnElement(elt, retryCount) {
if (elt == null) {
return null;
}
// handle extension source creation attribute
queryAttributeOnThisOrChildren(elt, "sse-connect").forEach(function(child) {
var sseURL = api.getAttributeValue(child, "sse-connect");
if (sseURL == null) {
return;
}
ensureEventSource(child, sseURL, retryCount);
});
}
function ensureEventSource(elt, url, retryCount) {
var source = htmx.createEventSource(url);
source.onerror = function(err) {
// Log an error event
api.triggerErrorEvent(elt, "htmx:sseError", { error: err, source: source });
// If parent no longer exists in the document, then clean up this EventSource
if (maybeCloseSSESource(elt)) {
return;
}
// Otherwise, try to reconnect the EventSource
if (source.readyState === EventSource.CLOSED) {
retryCount = retryCount || 0;
var timeout = Math.random() * (2 ^ retryCount) * 500;
window.setTimeout(function() {
ensureEventSourceOnElement(elt, Math.min(7, retryCount + 1));
}, timeout);
}
};
source.onopen = function(evt) {
api.triggerEvent(elt, "htmx:sseOpen", { source: source });
}
api.getInternalData(elt).sseEventSource = source;
}
/**
* maybeCloseSSESource confirms that the parent element still exists.
* If not, then any associated SSE source is closed and the function returns true.
*
* @param {HTMLElement} elt
* @returns boolean
*/
function maybeCloseSSESource(elt) {
if (!api.bodyContains(elt)) {
var source = api.getInternalData(elt).sseEventSource;
if (source != undefined) {
source.close();
// source = null
return true;
}
}
return false;
}
/**
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
*
* @param {HTMLElement} elt
* @param {string} attributeName
*/
function queryAttributeOnThisOrChildren(elt, attributeName) {
var result = [];
// If the parent element also contains the requested attribute, then add it to the results too.
if (api.hasAttribute(elt, attributeName)) {
result.push(elt);
}
// Search all child nodes that match the requested attribute
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "]").forEach(function(node) {
result.push(node);
});
return result;
}
/**
* @param {HTMLElement} elt
* @param {string} content
*/
function swap(elt, content) {
api.withExtensions(elt, function(extension) {
content = extension.transformResponse(content, null, elt);
});
var swapSpec = api.getSwapSpecification(elt);
var target = api.getTarget(elt);
var settleInfo = api.makeSettleInfo(elt);
api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
settleInfo.elts.forEach(function(elt) {
if (elt.classList) {
elt.classList.add(htmx.config.settlingClass);
}
api.triggerEvent(elt, 'htmx:beforeSettle');
});
// Handle settle tasks (with delay if requested)
if (swapSpec.settleDelay > 0) {
setTimeout(doSettle(settleInfo), swapSpec.settleDelay);
} else {
doSettle(settleInfo)();
}
}
/**
* doSettle mirrors much of the functionality in htmx that
* settles elements after their content has been swapped.
* TODO: this should be published by htmx, and not duplicated here
* @param {import("../htmx").HtmxSettleInfo} settleInfo
* @returns () => void
*/
function doSettle(settleInfo) {
return function() {
settleInfo.tasks.forEach(function(task) {
task.call();
});
settleInfo.elts.forEach(function(elt) {
if (elt.classList) {
elt.classList.remove(htmx.config.settlingClass);
}
api.triggerEvent(elt, 'htmx:afterSettle');
});
}
}
function hasEventSource(node) {
return api.getInternalData(node).sseEventSource != null;
}
})();

View File

@@ -1,476 +0,0 @@
/*
WebSockets Extension
============================
This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions.
*/
(function () {
/** @type {import("../htmx").HtmxInternalApi} */
var api;
htmx.defineExtension("ws", {
/**
* init is called once, when this extension is first registered.
* @param {import("../htmx").HtmxInternalApi} apiRef
*/
init: function (apiRef) {
// Store reference to internal API
api = apiRef;
// Default function for creating new EventSource objects
if (!htmx.createWebSocket) {
htmx.createWebSocket = createWebSocket;
}
// Default setting for reconnect delay
if (!htmx.config.wsReconnectDelay) {
htmx.config.wsReconnectDelay = "full-jitter";
}
},
/**
* onEvent handles all events passed to this extension.
*
* @param {string} name
* @param {Event} evt
*/
onEvent: function (name, evt) {
var parent = evt.target || evt.detail.elt;
switch (name) {
// Try to close the socket when elements are removed
case "htmx:beforeCleanupElement":
var internalData = api.getInternalData(parent)
if (internalData.webSocket) {
internalData.webSocket.close();
}
return;
// Try to create websockets when elements are processed
case "htmx:beforeProcessNode":
forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function (child) {
ensureWebSocket(child)
});
forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) {
ensureWebSocketSend(child)
});
}
}
});
function splitOnWhitespace(trigger) {
return trigger.trim().split(/\s+/);
}
function getLegacyWebsocketURL(elt) {
var legacySSEValue = api.getAttributeValue(elt, "hx-ws");
if (legacySSEValue) {
var values = splitOnWhitespace(legacySSEValue);
for (var i = 0; i < values.length; i++) {
var value = values[i].split(/:(.+)/);
if (value[0] === "connect") {
return value[1];
}
}
}
}
/**
* ensureWebSocket creates a new WebSocket on the designated element, using
* the element's "ws-connect" attribute.
* @param {HTMLElement} socketElt
* @returns
*/
function ensureWebSocket(socketElt) {
// If the element containing the WebSocket connection no longer exists, then
// do not connect/reconnect the WebSocket.
if (!api.bodyContains(socketElt)) {
return;
}
// Get the source straight from the element's value
var wssSource = api.getAttributeValue(socketElt, "ws-connect")
if (wssSource == null || wssSource === "") {
var legacySource = getLegacyWebsocketURL(socketElt);
if (legacySource == null) {
return;
} else {
wssSource = legacySource;
}
}
// Guarantee that the wssSource value is a fully qualified URL
if (wssSource.indexOf("/") === 0) {
var base_part = location.hostname + (location.port ? ':' + location.port : '');
if (location.protocol === 'https:') {
wssSource = "wss://" + base_part + wssSource;
} else if (location.protocol === 'http:') {
wssSource = "ws://" + base_part + wssSource;
}
}
var socketWrapper = createWebsocketWrapper(socketElt, function () {
return htmx.createWebSocket(wssSource)
});
socketWrapper.addEventListener('message', function (event) {
if (maybeCloseWebSocketSource(socketElt)) {
return;
}
var response = event.data;
if (!api.triggerEvent(socketElt, "htmx:wsBeforeMessage", {
message: response,
socketWrapper: socketWrapper.publicInterface
})) {
return;
}
api.withExtensions(socketElt, function (extension) {
response = extension.transformResponse(response, null, socketElt);
});
var settleInfo = api.makeSettleInfo(socketElt);
var fragment = api.makeFragment(response);
if (fragment.children.length) {
var children = Array.from(fragment.children);
for (var i = 0; i < children.length; i++) {
api.oobSwap(api.getAttributeValue(children[i], "hx-swap-oob") || "true", children[i], settleInfo);
}
}
api.settleImmediately(settleInfo.tasks);
api.triggerEvent(socketElt, "htmx:wsAfterMessage", { message: response, socketWrapper: socketWrapper.publicInterface })
});
// Put the WebSocket into the HTML Element's custom data.
api.getInternalData(socketElt).webSocket = socketWrapper;
}
/**
* @typedef {Object} WebSocketWrapper
* @property {WebSocket} socket
* @property {Array<{message: string, sendElt: Element}>} messageQueue
* @property {number} retryCount
* @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state
* @property {(message: string, sendElt: Element) => void} send
* @property {(event: string, handler: Function) => void} addEventListener
* @property {() => void} handleQueuedMessages
* @property {() => void} init
* @property {() => void} close
*/
/**
*
* @param socketElt
* @param socketFunc
* @returns {WebSocketWrapper}
*/
function createWebsocketWrapper(socketElt, socketFunc) {
var wrapper = {
socket: null,
messageQueue: [],
retryCount: 0,
/** @type {Object<string, Function[]>} */
events: {},
addEventListener: function (event, handler) {
if (this.socket) {
this.socket.addEventListener(event, handler);
}
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(handler);
},
sendImmediately: function (message, sendElt) {
if (!this.socket) {
api.triggerErrorEvent()
}
if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
message: message,
socketWrapper: this.publicInterface
})) {
this.socket.send(message);
sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
message: message,
socketWrapper: this.publicInterface
})
}
},
send: function (message, sendElt) {
if (this.socket.readyState !== this.socket.OPEN) {
this.messageQueue.push({ message: message, sendElt: sendElt });
} else {
this.sendImmediately(message, sendElt);
}
},
handleQueuedMessages: function () {
while (this.messageQueue.length > 0) {
var queuedItem = this.messageQueue[0]
if (this.socket.readyState === this.socket.OPEN) {
this.sendImmediately(queuedItem.message, queuedItem.sendElt);
this.messageQueue.shift();
} else {
break;
}
}
},
init: function () {
if (this.socket && this.socket.readyState === this.socket.OPEN) {
// Close discarded socket
this.socket.close()
}
// Create a new WebSocket and event handlers
/** @type {WebSocket} */
var socket = socketFunc();
// The event.type detail is added for interface conformance with the
// other two lifecycle events (open and close) so a single handler method
// can handle them polymorphically, if required.
api.triggerEvent(socketElt, "htmx:wsConnecting", { event: { type: 'connecting' } });
this.socket = socket;
socket.onopen = function (e) {
wrapper.retryCount = 0;
api.triggerEvent(socketElt, "htmx:wsOpen", { event: e, socketWrapper: wrapper.publicInterface });
wrapper.handleQueuedMessages();
}
socket.onclose = function (e) {
// If socket should not be connected, stop further attempts to establish connection
// If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause.
if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) {
var delay = getWebSocketReconnectDelay(wrapper.retryCount);
setTimeout(function () {
wrapper.retryCount += 1;
wrapper.init();
}, delay);
}
// Notify client code that connection has been closed. Client code can inspect `event` field
// to determine whether closure has been valid or abnormal
api.triggerEvent(socketElt, "htmx:wsClose", { event: e, socketWrapper: wrapper.publicInterface })
};
socket.onerror = function (e) {
api.triggerErrorEvent(socketElt, "htmx:wsError", { error: e, socketWrapper: wrapper });
maybeCloseWebSocketSource(socketElt);
};
var events = this.events;
Object.keys(events).forEach(function (k) {
events[k].forEach(function (e) {
socket.addEventListener(k, e);
})
});
},
close: function () {
this.socket.close()
}
}
wrapper.init();
wrapper.publicInterface = {
send: wrapper.send.bind(wrapper),
sendImmediately: wrapper.sendImmediately.bind(wrapper),
queue: wrapper.messageQueue
};
return wrapper;
}
/**
* ensureWebSocketSend attaches trigger handles to elements with
* "ws-send" attribute
* @param {HTMLElement} elt
*/
function ensureWebSocketSend(elt) {
var legacyAttribute = api.getAttributeValue(elt, "hx-ws");
if (legacyAttribute && legacyAttribute !== 'send') {
return;
}
var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
processWebSocketSend(webSocketParent, elt);
}
/**
* hasWebSocket function checks if a node has webSocket instance attached
* @param {HTMLElement} node
* @returns {boolean}
*/
function hasWebSocket(node) {
return api.getInternalData(node).webSocket != null;
}
/**
* processWebSocketSend adds event listeners to the <form> element so that
* messages can be sent to the WebSocket server when the form is submitted.
* @param {HTMLElement} socketElt
* @param {HTMLElement} sendElt
*/
function processWebSocketSend(socketElt, sendElt) {
var nodeData = api.getInternalData(sendElt);
var triggerSpecs = api.getTriggerSpecs(sendElt);
triggerSpecs.forEach(function (ts) {
api.addTriggerHandler(sendElt, ts, nodeData, function (elt, evt) {
if (maybeCloseWebSocketSource(socketElt)) {
return;
}
/** @type {WebSocketWrapper} */
var socketWrapper = api.getInternalData(socketElt).webSocket;
var headers = api.getHeaders(sendElt, api.getTarget(sendElt));
var results = api.getInputValues(sendElt, 'post');
var errors = results.errors;
var rawParameters = results.values;
var expressionVars = api.getExpressionVars(sendElt);
var allParameters = api.mergeObjects(rawParameters, expressionVars);
var filteredParameters = api.filterValues(allParameters, sendElt);
var sendConfig = {
parameters: filteredParameters,
unfilteredParameters: allParameters,
headers: headers,
errors: errors,
triggeringEvent: evt,
messageBody: undefined,
socketWrapper: socketWrapper.publicInterface
};
if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
return;
}
if (errors && errors.length > 0) {
api.triggerEvent(elt, 'htmx:validation:halted', errors);
return;
}
var body = sendConfig.messageBody;
if (body === undefined) {
var toSend = Object.assign({}, sendConfig.parameters);
if (sendConfig.headers)
toSend['HEADERS'] = headers;
body = JSON.stringify(toSend);
}
socketWrapper.send(body, elt);
if (evt && api.shouldCancel(evt, elt)) {
evt.preventDefault();
}
});
});
}
/**
* getWebSocketReconnectDelay is the default easing function for WebSocket reconnects.
* @param {number} retryCount // The number of retries that have already taken place
* @returns {number}
*/
function getWebSocketReconnectDelay(retryCount) {
/** @type {"full-jitter" | ((retryCount:number) => number)} */
var delay = htmx.config.wsReconnectDelay;
if (typeof delay === 'function') {
return delay(retryCount);
}
if (delay === 'full-jitter') {
var exp = Math.min(retryCount, 6);
var maxDelay = 1000 * Math.pow(2, exp);
return maxDelay * Math.random();
}
logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"');
}
/**
* maybeCloseWebSocketSource checks to the if the element that created the WebSocket
* still exists in the DOM. If NOT, then the WebSocket is closed and this function
* returns TRUE. If the element DOES EXIST, then no action is taken, and this function
* returns FALSE.
*
* @param {*} elt
* @returns
*/
function maybeCloseWebSocketSource(elt) {
if (!api.bodyContains(elt)) {
api.getInternalData(elt).webSocket.close();
return true;
}
return false;
}
/**
* createWebSocket is the default method for creating new WebSocket objects.
* it is hoisted into htmx.createWebSocket to be overridden by the user, if needed.
*
* @param {string} url
* @returns WebSocket
*/
function createWebSocket(url) {
var sock = new WebSocket(url, []);
sock.binaryType = htmx.config.wsBinaryType;
return sock;
}
/**
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
*
* @param {HTMLElement} elt
* @param {string} attributeName
*/
function queryAttributeOnThisOrChildren(elt, attributeName) {
var result = []
// If the parent element also contains the requested attribute, then add it to the results too.
if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-ws")) {
result.push(elt);
}
// Search all child nodes that match the requested attribute
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [data-hx-ws], [hx-ws]").forEach(function (node) {
result.push(node)
})
return result
}
/**
* @template T
* @param {T[]} arr
* @param {(T) => void} func
*/
function forEach(arr, func) {
if (arr) {
for (var i = 0; i < arr.length; i++) {
func(arr[i]);
}
}
}
})();

193
src/htmx.d.ts vendored
View File

@@ -1,193 +0,0 @@
declare namespace htmx {
const onLoad: (callback: (elt: Node) => void) => EventListener;
const process: (elt: string | Element) => void;
const on: (arg1: string | EventTarget, arg2: string | EventListener, arg3?: EventListener) => EventListener;
const off: (arg1: string | EventTarget, arg2: string | EventListener, arg3?: EventListener) => EventListener;
const trigger: (elt: string | EventTarget, eventName: string, detail?: any) => boolean;
const ajax: (verb: HttpVerb, path: string, context: string | Element | HtmxAjaxHelperContext) => Promise<void>;
const find: (eltOrSelector: string | ParentNode, selector?: string) => Element;
const findAll: (eltOrSelector: string | ParentNode, selector?: string) => NodeListOf<Element>;
const closest: (elt: string | Element, selector: string) => Element;
function values(elt: Element, type: HttpVerb): any;
const remove: (elt: Node, delay?: number) => void;
const addClass: (elt: string | Element, clazz: string, delay?: number) => void;
const removeClass: (node: string | Node, clazz: string, delay?: number) => void;
const toggleClass: (elt: string | Element, clazz: string) => void;
const takeClass: (elt: string | Node, clazz: string) => void;
const swap: (target: string | Element, content: string, swapSpec: HtmxSwapSpecification, swapOptions?: SwapOptions) => void;
const defineExtension: (name: string, extension: any) => void;
const removeExtension: (name: string) => void;
const logAll: () => void;
const logNone: () => void;
const logger: any;
namespace config {
const historyEnabled: boolean;
const historyCacheSize: number;
const refreshOnHistoryMiss: boolean;
const defaultSwapStyle: HtmxSwapStyle;
const defaultSwapDelay: number;
const defaultSettleDelay: number;
const includeIndicatorStyles: boolean;
const indicatorClass: string;
const requestClass: string;
const addedClass: string;
const settlingClass: string;
const swappingClass: string;
const allowEval: boolean;
const allowScriptTags: boolean;
const inlineScriptNonce: string;
const attributesToSettle: string[];
const withCredentials: boolean;
const timeout: number;
const wsReconnectDelay: "full-jitter" | ((retryCount: number) => number);
const wsBinaryType: BinaryType;
const disableSelector: string;
const scrollBehavior: 'auto' | 'instant' | 'smooth';
const defaultFocusScroll: boolean;
const getCacheBusterParam: boolean;
const globalViewTransitions: boolean;
const methodsThatUseUrlParams: (HttpVerb)[];
const selfRequestsOnly: boolean;
const ignoreTitle: boolean;
const scrollIntoViewOnBoost: boolean;
const triggerSpecsCache: any | null;
const disableInheritance: boolean;
const responseHandling: HtmxResponseHandlingConfig[];
}
const parseInterval: (str: string) => number;
const _: (str: string) => any;
const version: string;
}
type HttpVerb = 'get' | 'head' | 'post' | 'put' | 'delete' | 'connect' | 'options' | 'trace' | 'patch';
type SwapOptions = {
select?: string;
selectOOB?: string;
eventInfo?: any;
anchor?: string;
contextElement?: Element;
afterSwapCallback?: swapCallback;
afterSettleCallback?: swapCallback;
};
type swapCallback = () => any;
type HtmxSwapStyle = 'innerHTML' | 'outerHTML' | 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend' | 'delete' | 'none' | string;
type HtmxSwapSpecification = {
swapStyle: HtmxSwapStyle;
swapDelay: number;
settleDelay: number;
transition?: boolean;
ignoreTitle?: boolean;
head?: string;
scroll?: 'top' | 'bottom';
scrollTarget?: string;
show?: string;
showTarget?: string;
focusScroll?: boolean;
};
type ConditionalFunction = ((this: Node, evt: Event) => boolean) & {
source: string;
};
type HtmxTriggerSpecification = {
trigger: string;
pollInterval?: number;
eventFilter?: ConditionalFunction;
changed?: boolean;
once?: boolean;
consume?: boolean;
delay?: number;
from?: string;
target?: string;
throttle?: number;
queue?: string;
root?: string;
threshold?: string;
};
type HtmxElementValidationError = {
elt: Element;
message: string;
validity: ValidityState;
};
type HtmxHeaderSpecification = Record<string, string>;
type HtmxAjaxHelperContext = {
source?: Element | string;
event?: Event;
handler?: HtmxAjaxHandler;
target: Element | string;
swap?: HtmxSwapStyle;
values?: any | FormData;
headers?: Record<string, string>;
select?: string;
};
type HtmxRequestConfig = {
boosted: boolean;
useUrlParams: boolean;
formData: FormData;
/**
* formData proxy
*/
parameters: any;
unfilteredFormData: FormData;
/**
* unfilteredFormData proxy
*/
unfilteredParameters: any;
headers: HtmxHeaderSpecification;
target: Element;
verb: HttpVerb;
errors: HtmxElementValidationError[];
withCredentials: boolean;
timeout: number;
path: string;
triggeringEvent: Event;
};
type HtmxResponseInfo = {
xhr: XMLHttpRequest;
target: Element;
requestConfig: HtmxRequestConfig;
etc: HtmxAjaxEtc;
boosted: boolean;
select: string;
pathInfo: {
requestPath: string;
finalRequestPath: string;
responsePath: string | null;
anchor: string;
};
failed?: boolean;
successful?: boolean;
};
type HtmxAjaxEtc = {
returnPromise?: boolean;
handler?: HtmxAjaxHandler;
select?: string;
targetOverride?: Element;
swapOverride?: HtmxSwapStyle;
headers?: Record<string, string>;
values?: any | FormData;
credentials?: boolean;
timeout?: number;
};
type HtmxResponseHandlingConfig = {
code?: string;
swap: boolean;
error?: boolean;
ignoreTitle?: boolean;
select?: string;
target?: string;
swapOverride?: string;
event?: string;
};
type HtmxBeforeSwapDetails = HtmxResponseInfo & {
shouldSwap: boolean;
serverResponse: any;
isError: boolean;
ignoreTitle: boolean;
selectOverride: string;
};
type HtmxAjaxHandler = (elt: Element, responseInfo: HtmxResponseInfo) => any;
type HtmxSettleTask = (() => void);
type HtmxSettleInfo = {
tasks: HtmxSettleTask[];
elts: Element[];
title?: string;
};
type HtmxExtension = any;

File diff suppressed because it is too large Load Diff

View File

@@ -71,6 +71,14 @@ describe('hx-boost attribute', function() {
div.innerHTML.should.equal('Boosted')
})
it('does not boost forms with method="dialog"', function() {
make('<div hx-boost="true"><form id="f1" action="/test" method="dialog"><button id="b1">close</button></form></div>')
var form = byId('f1')
var internalData = htmx._('getInternalData')(form)
should.equal(undefined, internalData.boosted)
})
it('handles basic anchor properly w/ data-* prefix', function() {
this.server.respondWith('GET', '/test', 'Boosted')
var div = make('<div data-hx-target="this" data-hx-boost="true"><a id="a1" href="/test">Foo</a></div>')
@@ -110,4 +118,104 @@ describe('hx-boost attribute', function() {
this.server.respond()
btn.innerHTML.should.equal('Boosted!')
})
it('form get w/ search params in action property excludes search params', function() {
this.server.respondWith('GET', /\/test.*/, function(xhr) {
should.equal(undefined, getParameters(xhr).foo)
xhr.respond(200, {}, 'Boosted!')
})
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="/test?foo=bar" method="get"><button id="b1">Submit</button></form></div>')
var btn = byId('b1')
btn.click()
this.server.respond()
div.innerHTML.should.equal('Boosted!')
})
it('form post w/ query params in action property uses full url', function() {
this.server.respondWith('POST', /\/test.*/, function(xhr) {
should.equal(undefined, getParameters(xhr).foo)
xhr.respond(200, {}, 'Boosted!')
})
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="/test?foo=bar" method="post"><button id="b1">Submit</button></form></div>')
var btn = byId('b1')
btn.click()
this.server.respond()
div.innerHTML.should.equal('Boosted!')
})
it('form get with an unset action properly submits', function() {
this.server.respondWith('GET', /\/*/, function(xhr) {
xhr.respond(200, {}, 'Boosted!')
})
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" method="get"><button id="b1">Submit</button></form></div>')
var btn = byId('b1')
btn.click()
this.server.respond()
div.innerHTML.should.equal('Boosted!')
})
it('form get with no action properly clears existing parameters on submit', function() {
/// add a foo=bar to the current url
var path = location.href
if (!path.includes('foo=bar')) {
if (!path.includes('?')) {
path += '?foo=bar'
} else {
path += '&foo=bar'
}
}
history.replaceState({ htmx: true }, '', path)
this.server.respondWith('GET', /\/*/, function(xhr) {
// foo should not be present because the form is a get with no action
should.equal(undefined, getParameters(xhr).foo)
xhr.respond(200, {}, 'Boosted!')
})
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" method="get"><button id="b1">Submit</button></form></div>')
var btn = byId('b1')
btn.click()
this.server.respond()
div.innerHTML.should.equal('Boosted!')
})
it('form get with an empty action properly clears existing parameters on submit', function() {
/// add a foo=bar to the current url
var path = location.href
if (!path.includes('foo=bar')) {
if (!path.includes('?')) {
path += '?foo=bar'
} else {
path += '&foo=bar'
}
}
history.replaceState({ htmx: true }, '', path)
this.server.respondWith('GET', /\/*/, function(xhr) {
// foo should not be present because the form is a get with no action
should.equal(undefined, getParameters(xhr).foo)
xhr.respond(200, {}, 'Boosted!')
})
var div = make('<div hx-target="this" hx-boost="true"><form id="f1" action="" method="get"><button id="b1">Submit</button></form></div>')
var btn = byId('b1')
btn.click()
this.server.respond()
div.innerHTML.should.equal('Boosted!')
})
if (/headlesschrome/i.test(navigator.userAgent)) {
it('ctrlKey mouse click does not boost', function() {
// Test only works well in playwright with chome for code coverage as otherwise it opens a new tab breaking things
this.server.respondWith('GET', '/test', 'Boosted')
var div = make('<div hx-target="this" hx-boost="true"><a id="a1" href="/test">Foo</a></div>')
var a = byId('a1')
var evt = new MouseEvent('click', { ctrlKey: true })
a.dispatchEvent(evt)
this.server.respond()
div.innerHTML.should.not.equal('Boosted')
})
}
})

View File

@@ -68,23 +68,6 @@ describe('hx-confirm attribute', function() {
}
})
it('should allow skipping built-in window.confirm when using issueRequest', function() {
this.server.respondWith('GET', '/test', 'Clicked!')
try {
var btn = make('<button hx-get="/test" hx-confirm="Sure?">Click Me!</button>')
var handler = htmx.on('htmx:confirm', function(evt) {
evt.detail.question.should.equal('Sure?')
evt.preventDefault()
evt.detail.issueRequest(true)
})
btn.click()
confirm.called.should.equal(false)
this.server.respond()
btn.innerHTML.should.equal('Clicked!')
} finally {
htmx.off('htmx:confirm', handler)
}
})
it('should allow skipping built-in window.confirm when using issueRequest', function() {
this.server.respondWith('GET', '/test', 'Clicked!')
try {

View File

@@ -80,4 +80,55 @@ describe('hx-disabled-elt attribute', function() {
b2.hasAttribute('disabled').should.equal(false)
b3.hasAttribute('disabled').should.equal(false)
})
it('load trigger does not prevent disabled element working', function() {
this.server.respondWith('GET', '/test', 'Loaded!')
var div1 = make('<div id="d1" hx-get="/test" hx-disabled-elt="#b1" hx-trigger="load">Load Me!</div><button id="b1">Demo</button>')
var div = byId('d1')
var btn = byId('b1')
div.innerHTML.should.equal('Load Me!')
btn.hasAttribute('disabled').should.equal(true)
this.server.respond()
div.innerHTML.should.equal('Loaded!')
btn.hasAttribute('disabled').should.equal(false)
})
it('hx-disabled-elt supports multiple extended selectors', function() {
this.server.respondWith('GET', '/test', 'Clicked!')
var form = make('<form hx-get="/test" hx-disabled-elt="find input[type=\'text\'], find button" hx-swap="none"><input id="i1" type="text" placeholder="Type here..."><button id="b2" type="submit">Send</button></form>')
var i1 = byId('i1')
var b2 = byId('b2')
i1.hasAttribute('disabled').should.equal(false)
b2.hasAttribute('disabled').should.equal(false)
b2.click()
i1.hasAttribute('disabled').should.equal(true)
b2.hasAttribute('disabled').should.equal(true)
this.server.respond()
i1.hasAttribute('disabled').should.equal(false)
b2.hasAttribute('disabled').should.equal(false)
})
it('closest/find/next/previous handle nothing to find without exception', function() {
this.server.respondWith('GET', '/test', 'Clicked!')
var btn1 = make('<button hx-get="/test" hx-disabled-elt="closest input">Click Me!</button>')
var btn2 = make('<button hx-get="/test" hx-disabled-elt="find input">Click Me!</button>')
var btn3 = make('<button hx-get="/test" hx-disabled-elt="next input">Click Me!</button>')
var btn4 = make('<button hx-get="/test" hx-disabled-elt="previous input">Click Me!</button>')
btn1.click()
btn1.hasAttribute('disabled').should.equal(false)
this.server.respond()
btn2.click()
btn2.hasAttribute('disabled').should.equal(false)
this.server.respond()
btn3.click()
btn3.hasAttribute('disabled').should.equal(false)
this.server.respond()
btn4.click()
btn4.hasAttribute('disabled').should.equal(false)
this.server.respond()
})
})

View File

@@ -1,8 +1,8 @@
describe('hx-ext attribute', function() {
var ext1Calls, ext2Calls, ext3Calls, ext4Calls
var ext1Calls, ext2Calls, ext3Calls, ext4Calls, ext5Calls
beforeEach(function() {
ext1Calls = ext2Calls = ext3Calls = ext4Calls = 0
ext1Calls = ext2Calls = ext3Calls = ext4Calls = ext5Calls = 0
this.server = makeServer()
clearWorkArea()
htmx.defineExtension('ext-1', {
@@ -24,6 +24,12 @@ describe('hx-ext attribute', function() {
if (name === 'htmx:afterRequest') {
ext3Calls++
}
},
isInlineSwap: function(swapStyle) {
if (swapStyle === 'invalid') throw new Error('simulate exception handling in isInlineSwap')
},
handleSwap: function(swapStyle) {
if (swapStyle === 'invalid') throw new Error('simulate exception handling in handleSwap')
}
})
htmx.defineExtension('ext-4', {
@@ -31,6 +37,23 @@ describe('hx-ext attribute', function() {
if (name === 'namespace:example') {
ext4Calls++
}
},
isInlineSwap: function(swapStyle) {
return swapStyle === 'inline'
},
handleSwap: function(swapStyle, target, fragment, settleInfo) {
if (swapStyle === 'inline') {
const swapOuterHTML = htmx._('swapOuterHTML')
swapOuterHTML(target, fragment, settleInfo)
}
}
})
htmx.defineExtension('ext-5', {
getSelectors: function() { return ['[foo]'] },
onEvent: function(name, evt) {
if (name === 'htmx:beforeProcessNode' && evt.target.getAttribute('foo')) {
ext5Calls++
}
}
})
})
@@ -41,6 +64,8 @@ describe('hx-ext attribute', function() {
htmx.removeExtension('ext-1')
htmx.removeExtension('ext-2')
htmx.removeExtension('ext-3')
htmx.removeExtension('ext-4')
htmx.removeExtension('ext-5')
})
it('A simple extension is invoked properly', function() {
@@ -111,11 +136,23 @@ describe('hx-ext attribute', function() {
ext4Calls.should.equal(1)
})
it('A simple extension is invoked properly for elements it specified in getSelectors', function() {
this.server.respondWith('GET', '/test', [200, { 'HX-Trigger': 'namespace:example' }, ''])
var btn = make('<div data-hx-ext="ext-5"><div foo="bar">test</div></div>')
btn.click()
this.server.respond()
ext1Calls.should.equal(0)
ext2Calls.should.equal(0)
ext3Calls.should.equal(0)
ext4Calls.should.equal(0)
ext5Calls.should.equal(1)
})
it('Extensions are ignored properly', function() {
this.server.respondWith('GET', '/test', 'Clicked!')
make('<div id="div-AA" hx-ext="ext-1, ext-2"><button id="btn-AA" hx-get="/test">Click Me!</button>' +
'<div id="div-BB" hx-ext="ignore:ext-1"><button id="btn-BB" hx-get="/test"></div></div>')
make('<div id="div-AA" hx-ext="ext-1,ext-2,ext-5"><button id="btn-AA" hx-get="/test" foo="foo">Click Me!</button>' +
'<div id="div-BB" hx-ext="ignore:ext-1,ignore:ext-5"><button id="btn-BB" hx-get="/test" foo="foo"></button></div></div>')
var btn1 = byId('btn-AA')
var btn2 = byId('btn-BB')
@@ -131,5 +168,31 @@ describe('hx-ext attribute', function() {
ext1Calls.should.equal(1)
ext2Calls.should.equal(2)
ext3Calls.should.equal(0)
ext5Calls.should.equal(1)
})
it('oob swap via swap extension uses isInlineSwap correctly', function() {
this.server.respondWith(
'GET',
'/test',
'<div id="b1" hx-swap-oob="inline">Bar</div>'
)
var btn = make('<div hx-get="/test" hx-swap="none" data-hx-ext="ext-4"><div id="b1">Foo</div></div>')
btn.click()
this.server.respond()
byId('b1').innerHTML.should.equal('Bar')
})
it('isInlineSwap/handleSwap handling catches, logs and ignores exceptions in extension code', function() {
this.server.respondWith(
'GET',
'/test',
'<div id="b1" hx-swap-oob="invalid">Bar</div>'
)
var btn = make('<div hx-get="/test" hx-swap="none" data-hx-ext="ext-3"><div id="b1">Foo</div></div>')
btn.click()
this.server.respond()
byId('b1').innerHTML.should.equal('Bar')
})
})

View File

@@ -64,6 +64,18 @@ describe('hx-get attribute', function() {
form.innerHTML.should.equal('Clicked!')
})
it('GET on form with anchor works properly and scrolls to anchor id', function() {
this.server.respondWith('GET', /\/test.*/, function(xhr) {
getParameters(xhr).foo.should.equal('bar')
getParameters(xhr).i1.should.equal('value')
xhr.respond(200, {}, '<div id="foo">Clicked</div>')
})
var form = make('<form hx-trigger="click" hx-get="/test?foo=bar#foo"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>')
form.click()
this.server.respond()
form.innerHTML.should.equal('<div id="foo">Clicked</div>')
})
it('issues a GET request on click and swaps content w/ data-* prefix', function() {
this.server.respondWith('GET', '/test', 'Clicked!')

View File

@@ -100,7 +100,7 @@ describe('hx-headers attribute', function() {
div.innerHTML.should.equal('Clicked!')
})
it('multiple hx-headers works', function() {
it('multiple hx-headers works with javascript', function() {
this.server.respondWith('POST', '/vars', function(xhr) {
xhr.requestHeaders.v1.should.equal('test')
xhr.requestHeaders.v2.should.equal('42')
@@ -112,7 +112,7 @@ describe('hx-headers attribute', function() {
div.innerHTML.should.equal('Clicked!')
})
it('hx-headers can be on parents', function() {
it('hx-headers can be on parents with javascript', function() {
this.server.respondWith('POST', '/vars', function(xhr) {
xhr.requestHeaders.i1.should.equal('test')
xhr.respond(200, {}, 'Clicked!')
@@ -124,7 +124,7 @@ describe('hx-headers attribute', function() {
div.innerHTML.should.equal('Clicked!')
})
it('hx-headers can override parents', function() {
it('hx-headers can override parents with javascript', function() {
this.server.respondWith('POST', '/vars', function(xhr) {
xhr.requestHeaders.i1.should.equal('best')
xhr.respond(200, {}, 'Clicked!')
@@ -136,7 +136,7 @@ describe('hx-headers attribute', function() {
div.innerHTML.should.equal('Clicked!')
})
it('hx-headers overrides inputs', function() {
it('hx-headers overrides inputs with javascript', function() {
this.server.respondWith('POST', '/include', function(xhr) {
xhr.requestHeaders.i1.should.equal('best')
xhr.respond(200, {}, 'Clicked!')

View File

@@ -0,0 +1,38 @@
describe('hx-history attribute', function() {
var HTMX_HISTORY_CACHE_NAME = 'htmx-history-cache'
beforeEach(function() {
this.server = makeServer()
clearWorkArea()
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
})
afterEach(function() {
this.server.restore()
clearWorkArea()
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
})
it('content of hx-history-elt is used during history replacment', function() {
this.server.respondWith('GET', '/test1', '<div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test1</div>')
this.server.respondWith('GET', '/test2', '<div id="d3" hx-push-url="true" hx-get="/test3" hx-swap="outerHTML settle:0">test2</div>')
make('<div id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0">init</div>')
byId('d1').click()
this.server.respond()
var workArea = getWorkArea()
workArea.textContent.should.equal('test1')
byId('d2').click()
this.server.respond()
workArea.textContent.should.equal('test2')
this.server.respondWith('GET', '/test1', '<div>content outside of hx-history-elt not included</div><div id="work-area" hx-history-elt><div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test3</div></div>')
// clear cache so it makes a full page request on history restore
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
htmx._('restoreHistory')('/test1')
this.server.respond()
getWorkArea().textContent.should.equal('test3')
})
})

View File

@@ -4,12 +4,12 @@ describe('hx-history attribute', function() {
beforeEach(function() {
this.server = makeServer()
clearWorkArea()
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
})
afterEach(function() {
this.server.restore()
clearWorkArea()
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
})
it('history cache should not contain embargoed content', function() {
@@ -32,8 +32,8 @@ describe('hx-history attribute', function() {
this.server.respond()
workArea.textContent.should.equal('test3')
// embargoed content should NOT be in the localStorage cache
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
// embargoed content should NOT be in the sessionStorage cache
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache.length.should.equal(2)
// on history navigation, embargoed content is retrieved from server

View File

@@ -136,6 +136,104 @@ describe('hx-include attribute', function() {
div.innerHTML.should.equal('Clicked!')
})
it('Input can be referred to externally and then via a form then it will only be included once', function() {
this.server.respondWith('POST', '/include', function(xhr) {
var params = getParameters(xhr)
params.i1.should.equal('test')
xhr.respond(200, {}, 'Clicked!')
})
make('<form id="f1"><input id="i1" name="i1" value="test"/></form>')
var div = make('<div hx-post="/include" hx-include="previous #i1,#f1"></div>')
div.click()
this.server.respond()
div.innerHTML.should.equal('Clicked!')
})
it('checkbox can be referred to externally', function() {
this.server.respondWith('POST', '/include', function(xhr) {
var params = getParameters(xhr)
params.i1.should.equal('on')
xhr.respond(200, {}, 'Clicked!')
})
make('<input id="i1" name="i1" type="checkbox" checked/>')
var div = make('<div hx-post="/include" hx-include="#i1"></div>')
div.click()
this.server.respond()
div.innerHTML.should.equal('Clicked!')
})
it('files input can be referred to externally', function() {
// This test is just to make loc coverage complete and does not test that real file values are sent
this.server.respondWith('POST', '/include', function(xhr) {
xhr.respond(200, {}, 'Clicked!')
})
make('<input id="i1" name="i1" type="file" multiple/>')
var div = make('<div hx-post="/include" hx-include="#i1"></div>')
div.click()
this.server.respond()
div.innerHTML.should.equal('Clicked!')
})
it('properly handles multiple select input referred to externally', function() {
var values
this.server.respondWith('Post', '/include', function(xhr) {
values = getParameters(xhr)
xhr.respond(204, {}, '')
})
make('<select id="multiSelect" name="multiSelect" multiple="multiple">' +
'<option id="m1" value="m1">m1</option>' +
'<option id="m2" value="m2">m2</option>' +
'<option id="m3" value="m3">m3</option>' +
'<option id="m4" value="m4">m4</option>' +
'</select>')
var div = make('<div hx-post="/include" hx-include="#multiSelect"></div>')
div.click()
this.server.respond()
values.should.deep.equal({})
byId('m1').selected = true
div.click()
this.server.respond()
values.should.deep.equal({ multiSelect: 'm1' })
byId('m1').selected = true
byId('m3').selected = true
div.click()
this.server.respond()
values.should.deep.equal({ multiSelect: ['m1', 'm3'] })
})
it('properly handles multiple select input referred to externally and then via a form then it will only be included once', function() {
var values
this.server.respondWith('Post', '/include', function(xhr) {
values = getParameters(xhr)
xhr.respond(204, {}, '')
})
make('<form id="f1"><select id="multiSelect" name="multiSelect" multiple="multiple">' +
'<option id="m1" value="m1">m1</option>' +
'<option id="m2" value="m2">m2</option>' +
'<option id="m3" value="m3">m3</option>' +
'<option id="m4" value="m4">m4</option>' +
'</select></form>')
var div = make('<div hx-post="/include" hx-include="previous #multiSelect,#f1"></div>')
div.click()
this.server.respond()
values.should.deep.equal({})
byId('m1').selected = true
div.click()
this.server.respond()
values.should.deep.equal({ multiSelect: 'm1' })
byId('m1').selected = true
byId('m3').selected = true
div.click()
this.server.respond()
values.should.deep.equal({ multiSelect: ['m1', 'm3'] })
})
it('Two inputs can be referred to externally', function() {
this.server.respondWith('POST', '/include', function(xhr) {
var params = getParameters(xhr)
@@ -224,4 +322,200 @@ describe('hx-include attribute', function() {
this.server.respond()
btn.innerHTML.should.equal('Clicked!')
})
it('Multiple extended selectors can be used in hx-include', function() {
this.server.respondWith('POST', '/include', function(xhr) {
var params = getParameters(xhr)
params.i1.should.equal('test')
params.i2.should.equal('foo')
params.i3.should.equal('bar')
params.i4.should.equal('test2')
xhr.respond(200, {}, 'Clicked!')
})
make('<input name="i4" value="test2" id="i4"/>' +
'<div id="i">' +
'<input name="i1" value="test"/>' +
'<input name="i2" value="foo"/>' +
'<button id="btn" hx-post="/include" hx-include="closest div, next input, #i4"></button>' +
'</div>' +
'<input name="i3" value="bar"/>')
var btn = byId('btn')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Clicked!')
})
it('hx-include processes extended selector in between standard selectors', function() {
this.server.respondWith('POST', '/include', function(xhr) {
var params = getParameters(xhr)
params.i1.should.equal('test')
should.equal(params.i2, undefined)
params.i3.should.equal('bar')
params.i4.should.equal('test2')
xhr.respond(200, {}, 'Clicked!')
})
make('<input name="i4" value="test2" id="i4"/>' +
'<div id="i">' +
'<input name="i1" value="test" id="i1"/>' +
'<input name="i2" value="foo"/>' +
'<button id="btn" hx-post="/include" hx-include="#i1, next input, #i4"></button>' +
'</div>' +
'<input name="i3" value="bar"/>')
var btn = byId('btn')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Clicked!')
})
it('hx-include processes nested standard selectors correctly', function() {
this.server.respondWith('POST', '/include', function(xhr) {
var params = getParameters(xhr)
params.i1.should.equal('test')
params.i2.should.equal('foo')
params.i3.should.equal('bar')
should.equal(params.i4, undefined)
should.equal(params.i5, undefined)
xhr.respond(200, {}, 'Clicked!')
})
make('<input name="i4" value="test2" id="i4"/>' +
'<div id="i">' +
'<input name="i1" value="test" id="i1"/>' +
'<input name="i2" value="foo"/>' +
'<input name="i5" value="test"/>' +
'<button id="btn" hx-post="/include" hx-include="next input, #i > :is([name=\'i1\'], [name=\'i2\'])"></button>' +
'</div>' +
'<input name="i3" value="bar"/>')
var btn = byId('btn')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Clicked!')
})
it('hx-include processes wrapped next/previous selectors correctly', function() {
this.server.respondWith('POST', '/include', function(xhr) {
var params = getParameters(xhr)
should.equal(params.i1, undefined)
params.i2.should.equal('foo')
params.i3.should.equal('bar')
should.equal(params.i4, undefined)
should.equal(params.i5, undefined)
xhr.respond(200, {}, 'Clicked!')
})
make('<input name="i4" value="test2" id="i4"/>' +
'<div id="i">' +
'<input name="i1" value="test" id="i1"/>' +
'<input name="i2" value="foo"/>' +
'<button id="btn" hx-post="/include" hx-include="next <#nonexistent, input/>, previous <#i5, [name=\'i2\'], #i4/>"></button>' +
'</div>' +
'<input name="i3" value="bar"/>' +
'<input name="i5" value="test"/>')
var btn = byId('btn')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Clicked!')
})
it('hx-include processes wrapped closest selector correctly', function() {
this.server.respondWith('POST', '/include', function(xhr) {
var params = getParameters(xhr)
should.equal(params.i1, undefined)
params.i2.should.equal('bar')
xhr.respond(200, {}, 'Clicked!')
})
make('<section>' +
'<input name="i1" value="foo"/>' +
'<div>' +
'<input name="i2" value="bar"/>' +
'<button id="btn" hx-post="/include" hx-include="closest <section, div/>"></button>' +
'</div>' +
'</section>')
var btn = byId('btn')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Clicked!')
})
it('`inherit` can be used to expand parent hx-include', function() {
this.server.respondWith('POST', '/include', function(xhr) {
var params = getParameters(xhr)
params.i1.should.equal('test1')
params.i2.should.equal('test2')
xhr.respond(200, {}, 'Clicked!')
})
make('<div hx-include="#i1">' +
' <button id="btn" hx-include="inherit, #i2" hx-post="/include"></button>' +
'</div>' +
'<input id="i1" name="i1" value="test1"/>' +
'<input id="i2" name="i2" value="test2"/>')
var btn = byId('btn')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Clicked!')
})
it('`inherit` can be used to expand multiple parents hx-include', function() {
this.server.respondWith('POST', '/include', function(xhr) {
var params = getParameters(xhr)
params.i1.should.equal('test1')
params.i2.should.equal('test2')
params.i3.should.equal('test3')
xhr.respond(200, {}, 'Clicked!')
})
make('<div hx-include="#i1">' +
' <div hx-include="inherit, #i2">' +
' <button id="btn" hx-include="inherit, #i3" hx-post="/include"></button>' +
' </div>' +
'</div>' +
'<input id="i1" name="i1" value="test1"/>' +
'<input id="i2" name="i2" value="test2"/>' +
'<input id="i3" name="i3" value="test3"/>')
var btn = byId('btn')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Clicked!')
})
it('`inherit` chain breaks properly', function() {
this.server.respondWith('POST', '/include', function(xhr) {
var params = getParameters(xhr)
should.not.exist(params.i1)
params.i2.should.equal('test2')
params.i3.should.equal('test3')
xhr.respond(200, {}, 'Clicked!')
})
make('<div hx-include="#i1">' +
' <div hx-include="#i2">' +
' <button id="btn" hx-include="inherit, #i3" hx-post="/include"></button>' +
' </div>' +
'</div>' +
'<input id="i1" name="i1" value="test1"/>' +
'<input id="i2" name="i2" value="test2"/>' +
'<input id="i3" name="i3" value="test3"/>')
var btn = byId('btn')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Clicked!')
})
it('`inherit` syntax regex properly catches keyword', function() {
this.server.respondWith('POST', '/include', function(xhr) {
var params = getParameters(xhr)
params.i1.should.equal('test1')
params.i2.should.equal('test2')
params.i3.should.equal('test3')
xhr.respond(200, {}, 'Clicked!')
})
make('<div hx-include="#i1">' +
' <div hx-include="#i2, inherit,.nonexistent-class">' +
' <button id="btn" hx-include="customtag,inherit , #i3" hx-post="/include"></button>' +
' </div>' +
'</div>' +
'<input id="i1" name="i1" value="test1"/>' +
'<input id="i2" name="i2" value="test2"/>' +
'<input id="i3" name="i3" value="test3"/>')
var btn = byId('btn')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Clicked!')
})
})

View File

@@ -123,4 +123,68 @@ describe('hx-indicator attribute', function() {
b2.classList.contains('htmx-request').should.equal(false)
a1.classList.contains('htmx-request').should.equal(false)
})
it('`inherit` can be used to expand parent hx-indicator', function() {
this.server.respondWith('GET', '/test', 'Clicked!')
make('<div hx-indicator="#a1">' +
' <button id="btn" hx-get="/test" hx-indicator="inherit, #a2">Click Me!</button>' +
'</div>')
var btn = byId('btn')
var a1 = make('<a id="a1"></a>')
var a2 = make('<a id="a2"></a>')
btn.click()
btn.classList.contains('htmx-request').should.equal(false)
a1.classList.contains('htmx-request').should.equal(true)
a2.classList.contains('htmx-request').should.equal(true)
this.server.respond()
btn.classList.contains('htmx-request').should.equal(false)
a1.classList.contains('htmx-request').should.equal(false)
a2.classList.contains('htmx-request').should.equal(false)
})
it('`inherit` can be used to expand multiple parents hx-indicator', function() {
this.server.respondWith('GET', '/test', 'Clicked!')
make('<div hx-indicator="#a1">' +
' <div hx-indicator="inherit, #a2">' +
' <button id="btn" hx-get="/test" hx-indicator="inherit, #a3">Click Me!</button>' +
' </div>' +
'</div>')
var btn = byId('btn')
var a1 = make('<a id="a1"></a>')
var a2 = make('<a id="a2"></a>')
var a3 = make('<a id="a3"></a>')
btn.click()
btn.classList.contains('htmx-request').should.equal(false)
a1.classList.contains('htmx-request').should.equal(true)
a2.classList.contains('htmx-request').should.equal(true)
a3.classList.contains('htmx-request').should.equal(true)
this.server.respond()
btn.classList.contains('htmx-request').should.equal(false)
a1.classList.contains('htmx-request').should.equal(false)
a2.classList.contains('htmx-request').should.equal(false)
a3.classList.contains('htmx-request').should.equal(false)
})
it('`inherit` chain breaks properly', function() {
this.server.respondWith('GET', '/test', 'Clicked!')
make('<div hx-indicator="#a1">' +
' <div hx-indicator="#a2">' +
' <button id="btn" hx-get="/test" hx-indicator="inherit, #a3">Click Me!</button>' +
' </div>' +
'</div>')
var btn = byId('btn')
var a1 = make('<a id="a1"></a>')
var a2 = make('<a id="a2"></a>')
var a3 = make('<a id="a3"></a>')
btn.click()
btn.classList.contains('htmx-request').should.equal(false)
a1.classList.contains('htmx-request').should.equal(false)
a2.classList.contains('htmx-request').should.equal(true)
a3.classList.contains('htmx-request').should.equal(true)
this.server.respond()
btn.classList.contains('htmx-request').should.equal(false)
a1.classList.contains('htmx-request').should.equal(false)
a2.classList.contains('htmx-request').should.equal(false)
a3.classList.contains('htmx-request').should.equal(false)
})
})

View File

@@ -133,6 +133,20 @@ describe('hx-on:* attribute', function() {
delete window.foo
})
it('should fire when triggered by load', function() {
this.server.respondWith('POST', '/test', 'test')
make("<div hx-trigger='load' hx-post='/test' hx-on:htmx:config-request='foo = true'></div>")
window.foo.should.equal(true)
delete window.foo
})
it('should fire when triggered by revealed', function() {
this.server.respondWith('POST', '/test', 'test')
make("<div hx-trigger='revealed' hx-post='/test' hx-on:htmx:config-request='foo = true' style='position: fixed; top: 1px; left: 1px; border: 3px solid red'></div>")
window.foo.should.equal(true)
delete window.foo
})
it('de-initializes hx-on-* content properly', function() {
window.tempCount = 0
this.server.respondWith('POST', '/test', function(xhr) {

View File

@@ -1,193 +0,0 @@
describe('hx-on attribute', function() {
beforeEach(function() {
this.server = makeServer()
clearWorkArea()
})
afterEach(function() {
this.server.restore()
clearWorkArea()
})
it('can handle basic events w/ no other attributes', function() {
var btn = make("<button hx-on='click: window.foo = true'>Foo</button>")
btn.click()
window.foo.should.equal(true)
delete window.foo
})
it('can modify a parameter via htmx:configRequest', function() {
this.server.respondWith('POST', '/test', function(xhr) {
var params = parseParams(xhr.requestBody)
xhr.respond(200, {}, params.foo)
})
var btn = make("<button hx-on='htmx:configRequest: event.detail.parameters.foo = \"bar\"' hx-post='/test'>Foo</button>")
btn.click()
this.server.respond()
btn.innerText.should.equal('bar')
})
it('can cancel an event via preventDefault for htmx:configRequest', function() {
this.server.respondWith('POST', '/test', function(xhr) {
xhr.respond(200, {}, '<button>Bar</button>')
})
var btn = make("<button hx-on='htmx:configRequest: event.preventDefault()' hx-post='/test' hx-swap='outerHTML'>Foo</button>")
btn.click()
this.server.respond()
btn.innerText.should.equal('Foo')
})
it('can respond to kebab-case events', function() {
this.server.respondWith('POST', '/test', function(xhr) {
var params = parseParams(xhr.requestBody)
xhr.respond(200, {}, params.foo)
})
var btn = make("<button hx-on='htmx:config-request: event.detail.parameters.foo = \"bar\"' hx-post='/test'>Foo</button>")
btn.click()
this.server.respond()
btn.innerText.should.equal('bar')
})
it('has the this symbol set to the element', function() {
this.server.respondWith('POST', '/test', function(xhr) {
xhr.respond(200, {}, 'foo')
})
var btn = make("<button hx-on='htmx:config-request: window.elt = this' hx-post='/test'>Foo</button>")
btn.click()
this.server.respond()
btn.innerText.should.equal('foo')
btn.should.equal(window.elt)
delete window.elt
})
it('can handle multi-line JSON', function() {
this.server.respondWith('POST', '/test', function(xhr) {
xhr.respond(200, {}, 'foo')
})
var btn = make("<button hx-on='htmx:config-request: window.elt = {foo: true,\n" +
" bar: false}' hx-post='/test'>Foo</button>")
btn.click()
this.server.respond()
btn.innerText.should.equal('foo')
var obj = { foo: true, bar: false }
obj.should.deep.equal(window.elt)
delete window.elt
})
it('can handle multiple event handlers in the presence of multi-line JSON', function() {
this.server.respondWith('POST', '/test', function(xhr) {
xhr.respond(200, {}, 'foo')
})
var btn = make("<button hx-on='htmx:config-request: window.elt = {foo: true,\n" +
' bar: false}\n' +
" htmx:afterRequest: window.foo = true'" +
" hx-post='/test'>Foo</button>")
btn.click()
this.server.respond()
btn.innerText.should.equal('foo')
var obj = { foo: true, bar: false }
obj.should.deep.equal(window.elt)
delete window.elt
window.foo.should.equal(true)
delete window.foo
})
it('de-initializes hx-on content properly', function() {
window.tempCount = 0
this.server.respondWith('POST', '/test', function(xhr) {
xhr.respond(200, {}, "<button id='foo' hx-on=\"click: window.tempCount++;\">increment</button>")
})
var div = make("<div hx-post='/test'>Foo</div>")
// get response
div.click()
this.server.respond()
// click button
byId('foo').click()
window.tempCount.should.equal(1)
// get second response
div.click()
this.server.respond()
// click button again
byId('foo').click()
window.tempCount.should.equal(2)
delete window.tempCount
})
it('is not evaluated when allowEval is false', function() {
var calledEvent = false
var handler = htmx.on('htmx:evalDisallowedError', function() {
calledEvent = true
})
htmx.config.allowEval = false
try {
var btn = make("<button hx-on='click: window.foo = true'>Foo</button>")
btn.click()
should.not.exist(window.foo)
} finally {
htmx.config.allowEval = true
htmx.off('htmx:evalDisallowedError', handler)
delete window.foo
}
calledEvent.should.equal(true)
})
it('can handle event types with dots', function() {
var btn = make("<button hx-on='my.custom.event: window.foo = true'>Foo</button>")
// IE11 doesn't support `new CustomEvent()` so call htmx' internal utility function
btn.dispatchEvent(htmx._('makeEvent')('my.custom.event'))
window.foo.should.equal(true)
delete window.foo
})
it('can handle being swapped using innerHTML', function() {
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(200, {}, '<button id="bar" hx-on="click: window.bar = true">Bar</button>')
})
make(
'<div>' +
'<button id="swap" hx-get="/test" hx-target="#baz" hx-swap="innerHTML">Swap</button>' +
'<div id="baz"><button id="foo" hx-on="click: window.foo = true">Foo</button></div>' +
'</div>'
)
var fooBtn = byId('foo')
fooBtn.click()
window.foo.should.equal(true)
var swapBtn = byId('swap')
swapBtn.click()
this.server.respond()
var barBtn = byId('bar')
barBtn.click()
window.bar.should.equal(true)
delete window.foo
delete window.bar
})
it('cleans up all handlers when the DOM updates', function() {
// setup
window.foo = 0
window.bar = 0
var div = make("<div hx-on='increment-foo: window.foo++\nincrement-bar: window.bar++'>Foo</div>")
make('<div>Another Div</div>') // sole purpose is to update the DOM
// check there is just one handler against each event
htmx.trigger(div, 'increment-foo')
htmx.trigger(div, 'increment-bar')
window.foo.should.equal(1)
window.bar.should.equal(1)
// teardown
delete window.foo
delete window.bar
})
})

View File

@@ -34,4 +34,46 @@ describe('hx-preserve attribute', function() {
byId('d1').innerHTML.should.equal('Old Content')
byId('d2').innerHTML.should.equal('New Content')
})
it('preserved element should not be swapped if it is part of a oob swap', function() {
this.server.respondWith('GET', '/test', "Normal Content<div id='d2' hx-swap-oob='true'><div id='d3' hx-preserve>New oob Content</div><div id='d4'>New oob Content</div></div>")
var div1 = make("<div id='d1' hx-get='/test'>Click Me!</div>")
var div2 = make("<div id='d2'><div id='d3' hx-preserve>Old Content</div></div>")
div1.click()
this.server.respond()
byId('d1').innerHTML.should.equal('Normal Content')
byId('d3').innerHTML.should.equal('Old Content')
byId('d4').innerHTML.should.equal('New oob Content')
})
it('preserved element should not be swapped if it is part of a hx-select-oob swap', function() {
this.server.respondWith('GET', '/test', "Normal Content<div id='d2'><div id='d3' hx-preserve>New oob Content</div><div id='d4'>New oob Content</div></div>")
var div1 = make("<div id='d1' hx-get='/test' hx-select-oob='#d2'>Click Me!</div>")
var div2 = make("<div id='d2'><div id='d3' hx-preserve>Old Content</div></div>")
div1.click()
this.server.respond()
byId('d1').innerHTML.should.equal('Normal Content')
byId('d3').innerHTML.should.equal('Old Content')
byId('d4').innerHTML.should.equal('New oob Content')
})
it('preserved element should relocated unchanged if it is part of a oob swap targeting a different loction', function() {
this.server.respondWith('GET', '/test', "Normal Content<div id='d2' hx-swap-oob='innerHTML:#d5'><div id='d3' hx-preserve>New oob Content</div><div id='d4'>New oob Content</div></div>")
var div1 = make("<div id='d1' hx-get='/test'>Click Me!</div>")
var div2 = make("<div id='d2'><div id='d3' hx-preserve>Old Content</div></div>")
var div5 = make("<div id='d5'></div>")
div1.click()
this.server.respond()
byId('d1').innerHTML.should.equal('Normal Content')
byId('d2').innerHTML.should.equal('')
byId('d5').innerHTML.should.equal('<div id="d3" hx-preserve="">Old Content</div><div id="d4">New oob Content</div>')
})
it('when moveBefore is disabled/missing preseved content is copied into fragment instead of pantry', function() {
var div = make("<div hx-get='/test'><div id='d1' hx-preserve>Old Content</div><div id='d2'>Old Content</div></div>")
var fragment = htmx._('makeFragment')('<div id="d1" hx-preserve>New Content</div>')
fragment.firstChild.moveBefore = undefined
htmx._('handlePreservedElements')(fragment)
fragment.firstChild.innerHTML.should.equal('Old Content')
})
})

View File

@@ -0,0 +1,37 @@
describe('hx-prompt attribute', function() {
beforeEach(function() {
this.server = makeServer()
clearWorkArea()
})
afterEach(function() {
this.server.restore()
clearWorkArea()
})
it('hx-prompt should set request header to prompt response', function() {
this.server.respondWith('GET', '/test', function(xhr) {
should.equal(xhr.requestHeaders['HX-Prompt'], 'foo')
xhr.respond(200, {}, 'Clicked!')
})
var promptSave = window.prompt
window.prompt = function() { return 'foo' }
var btn = make('<button hx-get="/test" hx-prompt="test prompt">Click Me!</a>')
btn.click()
this.server.respond()
window.prompt = promptSave
btn.innerHTML.should.equal('Clicked!')
})
it('hx-prompt that is cancled returns null and blocks the request', function() {
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(200, {}, 'Clicked!')
})
var promptSave = window.prompt
window.prompt = function() { return null }
var btn = make('<button hx-get="/test" hx-prompt="test prompt">Click Me!</a>')
btn.click()
this.server.respond()
window.prompt = promptSave
btn.innerHTML.should.equal('Click Me!')
})
})

View File

@@ -1,15 +1,16 @@
describe('hx-push-url attribute', function() {
const chai = window.chai
var HTMX_HISTORY_CACHE_NAME = 'htmx-history-cache'
beforeEach(function() {
this.server = makeServer()
clearWorkArea()
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
})
afterEach(function() {
this.server.restore()
clearWorkArea()
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
})
it('navigation should push an element into the cache when true', function() {
@@ -21,20 +22,33 @@ describe('hx-push-url attribute', function() {
div.click()
this.server.respond()
getWorkArea().textContent.should.equal('second')
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache[cache.length - 1].url.should.equal('/test')
})
it('navigation should push an element into the cache when string', function() {
it('navigation should not push an element into the cache when false', function() {
this.server.respondWith('GET', '/test', 'second')
getWorkArea().innerHTML.should.be.equal('')
var div = make('<div hx-push-url="/abc123" hx-get="/test">first</div>')
var div = make('<div hx-push-url="false" hx-get="/test">first</div>')
div.click()
this.server.respond()
div.click()
this.server.respond()
getWorkArea().textContent.should.equal('second')
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
should.equal(cache, null)
})
it('navigation should push an element into the cache when string', function() {
this.server.respondWith('GET', '/test', 'second')
getWorkArea().innerHTML.should.be.equal('')
var div = make('<div hx-push-url="abc123" hx-get="/test">first</div>')
div.click()
this.server.respond()
div.click()
this.server.respond()
getWorkArea().textContent.should.equal('second')
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache.length.should.equal(2)
cache[1].url.should.equal('/abc123')
})
@@ -54,7 +68,7 @@ describe('hx-push-url attribute', function() {
this.server.respond()
workArea.textContent.should.equal('test2')
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache.length.should.equal(2)
htmx._('restoreHistory')('/test1')
@@ -92,7 +106,7 @@ describe('hx-push-url attribute', function() {
byId('d1').click()
this.server.respond()
}
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache.length.should.equal(10) // should only be 10 elements
})
@@ -111,15 +125,26 @@ describe('hx-push-url attribute', function() {
this.server.respond()
workArea.textContent.should.equal('test2')
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache.length.should.equal(2)
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME) // clear cache
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME) // clear cache
htmx._('restoreHistory')('/test1')
this.server.respond()
getWorkArea().textContent.should.equal('test1')
})
it('cache miss should refresh when refreshOnHistoryMiss true', function() {
htmx.config.refreshOnHistoryMiss = true
var refresh = false
htmx.location = { reload: function() { refresh = true } }
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME) // clear cache
htmx._('restoreHistory')('/test3')
refresh.should.equal(true)
htmx.location = window.location
htmx.config.refreshOnHistoryMiss = false
})
it('navigation should push an element into the cache w/ data-* prefix', function() {
this.server.respondWith('GET', '/test', 'second')
getWorkArea().innerHTML.should.be.equal('')
@@ -127,20 +152,20 @@ describe('hx-push-url attribute', function() {
div.click()
this.server.respond()
getWorkArea().textContent.should.equal('second')
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache.length.should.equal(1)
})
it('deals with malformed JSON in history cache when getting', function() {
localStorage.setItem(HTMX_HISTORY_CACHE_NAME, 'Invalid JSON')
sessionStorage.setItem(HTMX_HISTORY_CACHE_NAME, 'Invalid JSON')
var history = htmx._('getCachedHistory')('url')
should.equal(history, null)
})
it('deals with malformed JSON in history cache when saving', function() {
localStorage.setItem(HTMX_HISTORY_CACHE_NAME, 'Invalid JSON')
sessionStorage.setItem(HTMX_HISTORY_CACHE_NAME, 'Invalid JSON')
htmx._('saveToHistoryCache')('url', make('<div>'))
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache.length.should.equal(1)
})
@@ -149,21 +174,32 @@ describe('hx-push-url attribute', function() {
htmx._('saveToHistoryCache')('url2', make('<div>'))
htmx._('saveToHistoryCache')('url3', make('<div>'))
htmx._('saveToHistoryCache')('url2', make('<div>'))
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache.length.should.equal(3)
})
it('setting history cache size to 0 clears cache', function() {
htmx._('saveToHistoryCache')('url1', make('<div>'))
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache.length.should.equal(1)
htmx.config.historyCacheSize = 0
htmx._('saveToHistoryCache')('url2', make('<div>'))
cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
should.equal(cache, null)
htmx.config.historyCacheSize = 10
})
it('history cache is LRU', function() {
htmx._('saveToHistoryCache')('url1', make('<div>'))
htmx._('saveToHistoryCache')('url2', make('<div>'))
htmx._('saveToHistoryCache')('url3', make('<div>'))
htmx._('saveToHistoryCache')('url2', make('<div>'))
htmx._('saveToHistoryCache')('url1', make('<div>'))
var cache = JSON.parse(localStorage.getItem(HTMX_HISTORY_CACHE_NAME))
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache.length.should.equal(3)
cache[0].url.should.equal('url3')
cache[1].url.should.equal('url2')
cache[2].url.should.equal('url1')
cache[0].url.should.equal('/url3')
cache[1].url.should.equal('/url2')
cache[2].url.should.equal('/url1')
})
it('htmx:afterSettle is called when replacing outerHTML', function() {
@@ -204,17 +240,159 @@ describe('hx-push-url attribute', function() {
})
it('saveToHistoryCache should not throw', function() {
this.timeout(4000)
var bigContent = 'Dummy'
for (var i = 0; i < 20; i++) {
bigContent += bigContent
}
try {
localStorage.removeItem('htmx-history-cache')
sessionStorage.removeItem('htmx-history-cache')
htmx._('saveToHistoryCache')('/dummy', make('<div>' + bigContent + '</div>'), 'Foo', 0)
should.equal(localStorage.getItem('htmx-history-cache'), null)
should.equal(sessionStorage.getItem('htmx-history-cache'), null)
} finally {
// clear history cache afterwards
localStorage.removeItem('htmx-history-cache')
sessionStorage.removeItem('htmx-history-cache')
}
})
if (/chrome/i.test(navigator.userAgent)) {
it('when sessionStorage disabled history not saved fine', function() {
var setItem = sessionStorage.setItem
sessionStorage.setItem = undefined
this.server.respondWith('GET', '/test', 'second')
getWorkArea().innerHTML.should.be.equal('')
var div = make('<div hx-push-url="true" hx-get="/test">first</div>')
div.click()
this.server.respond()
div.click()
this.server.respond()
getWorkArea().textContent.should.equal('second')
var hist = htmx._('getCachedHistory')('/test')
should.equal(hist, null)
sessionStorage.setItem = setItem
})
}
it.skip('normalizePath falls back to no normalization if path not valid URL', function() {
// path normalization has a bug breaking it right now preventing this test
htmx._('saveToHistoryCache')('http://', make('<div>'))
htmx._('saveToHistoryCache')('http//', make('<div>'))
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache.length.should.equal(2)
cache[0].url.should.equal('http://') // no normalization as invalid
cache[1].url.should.equal('/http') // can normalize this one
})
it('history cache clears out disabled attribute', function() {
htmx._('saveToHistoryCache')('/url1', make('<div><div data-disabled-by-htmx disabled></div></div>'))
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache.length.should.equal(1)
cache[0].url.should.equal('/url1')
cache[0].content.should.equal('<div data-disabled-by-htmx=""></div>')
})
it('ensure cache-busting parameter not pushed to history url', function() {
this.server.respondWith('GET', /\/test.*/, function(xhr) {
getParameters(xhr)['org.htmx.cache-buster'].should.equal('foo')
xhr.respond(200, {}, 'Clicked!')
})
try {
htmx.config.getCacheBusterParam = true
var btn = make('<button hx-push-url="true" hx-get="/test" id="foo">Click Me!</button>')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Clicked!')
} finally {
htmx.config.getCacheBusterParam = false
}
sessionStorage.getItem('htmx-current-path-for-history').should.equal('/test')
})
it('ensure history pushState called', function() {
if (!byId('mocha')) { // This test does not work in browser using mocha
this.server.respondWith('GET', /\/test.*/, function(xhr) {
xhr.respond(200, {}, 'Clicked!')
})
try {
htmx.config.historyEnabled = true
var btn = make('<button hx-push-url="true" hx-get="/test" id="foo">Click Me!</button>')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Clicked!')
} finally {
htmx.config.historyEnabled = false
}
}
})
it('should handle HX-Push response header', function() {
var path
var handler = htmx.on('htmx:pushedIntoHistory', function(event) {
path = event.detail.path
})
this.server.respondWith('GET', '/test', [200, { 'HX-Push': '/pushpath' }, 'Result'])
var div1 = make('<div id="d1" hx-get="/test"></div>')
div1.click()
this.server.respond()
div1.innerHTML.should.equal('Result')
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache.length.should.equal(1)
path.should.equal('/pushpath')
htmx.off('htmx:pushedIntoHistory', handler)
})
it('should handle HX-Push-Url response header', function() {
var path
var handler = htmx.on('htmx:pushedIntoHistory', function(event) {
path = event.detail.path
})
this.server.respondWith('GET', '/test', [200, { 'HX-Push-Url': '/pushpath' }, 'Result'])
var div1 = make('<div id="d1" hx-get="/test"></div>')
div1.click()
this.server.respond()
div1.innerHTML.should.equal('Result')
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache.length.should.equal(1)
path.should.equal('/pushpath')
htmx.off('htmx:pushedIntoHistory', handler)
})
it('should ignore HX-Push-Url=false response header', function() {
var path = ''
var handler = htmx.on('htmx:pushedIntoHistory', function(event) {
path = event.detail.path
})
this.server.respondWith('GET', '/test', [200, { 'HX-Push-Url': 'false' }, 'Result'])
var div1 = make('<div id="d1" hx-get="/test"></div>')
div1.click()
this.server.respond()
div1.innerHTML.should.equal('Result')
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
should.equal(cache, null)
path.should.equal('')
htmx.off('htmx:pushedIntoHistory', handler)
})
it('pushing url without anchor will retain the page anchor tag', function() {
var handler = htmx.on('htmx:configRequest', function(evt) {
evt.detail.path = evt.detail.path + '#test'
})
var path = ''
var handler2 = htmx.on('htmx:pushedIntoHistory', function(evt) {
path = evt.detail.path
})
try {
this.server.respondWith('GET', '/test', 'Clicked!')
var div = make("<div hx-get='/test' hx-push-url='/test'></div>")
div.click()
this.server.respond()
div.innerHTML.should.equal('Clicked!')
path.should.equal('/test#test')
} finally {
htmx.off('htmx:configRequest', handler)
htmx.off('htmx:pushedIntoHistory', handler2)
}
})
})

View File

@@ -0,0 +1,43 @@
describe('hx-replace-url attribute', function() {
var HTMX_HISTORY_CACHE_NAME = 'htmx-history-cache'
beforeEach(function() {
this.server = makeServer()
clearWorkArea()
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
})
afterEach(function() {
this.server.restore()
clearWorkArea()
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
})
it('navigation should replace an element into the cache when true', function() {
this.server.respondWith('GET', '/test', 'second')
getWorkArea().innerHTML.should.be.equal('')
var div = make('<div hx-replace-url="true" hx-get="/test">first</div>')
div.click()
this.server.respond()
div.click()
this.server.respond()
getWorkArea().textContent.should.equal('second')
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache[cache.length - 1].url.should.equal('/test')
})
it('should handle HX-Replace-Url response header', function() {
var path
var handler = htmx.on('htmx:replacedInHistory', function(event) {
path = event.detail.path
})
this.server.respondWith('GET', '/test', [200, { 'HX-Replace-Url': '/pushpath' }, 'Result'])
var div1 = make('<div id="d1" hx-get="/test"></div>')
div1.click()
this.server.respond()
div1.innerHTML.should.equal('Result')
var cache = JSON.parse(sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME))
cache.length.should.equal(1)
path.should.equal('/pushpath')
htmx.off('htmx:replacedInHistory', handler)
})
})

View File

@@ -10,18 +10,19 @@ describe('hx-request attribute', function() {
it('basic hx-request timeout works', function(done) {
var timedOut = false
this.server.respondWith('GET', '/test', 'Clicked!')
var div = make("<div hx-post='/vars' hx-request='\"timeout\":1'></div>")
htmx.config.selfRequestsOnly = false
var div = make("<div hx-post='https://hypermedia.systems/www/test' hx-request='\"timeout\":1'></div>")
htmx.on(div, 'htmx:timeout', function() {
timedOut = true
})
this.server.restore() // use real xhrs
div.click()
setTimeout(function() {
htmx.config.selfRequestsOnly = true
div.innerHTML.should.equal('')
// unfortunately it looks like sinon.js doesn't implement the timeout functionality
// timedOut.should.equal(true);
timedOut.should.equal(true)
done()
}, 400)
}, 30)
})
it('hx-request header works', function() {

View File

@@ -66,6 +66,28 @@ describe('hx-swap-oob attribute', function() {
})
}
it('handles remvoing hx-swap-oob tag', function() {
this.server.respondWith('GET', '/test', "Clicked<div id='d1' data-hx-swap-oob='true'>Swapped3</div>")
var div = make('<div data-hx-get="/test">click me</div>')
make('<div id="d1"></div>')
div.click()
this.server.respond()
div.innerHTML.should.equal('Clicked')
byId('d1').innerHTML.should.equal('Swapped3')
byId('d1').hasAttribute('hx-swap-oob').should.equal(false)
})
it('handles remvoing data-hx-swap-oob tag', function() {
this.server.respondWith('GET', '/test', "Clicked<div id='d1' data-hx-swap-oob='true'>Swapped3</div>")
var div = make('<div data-hx-get="/test">click me</div>')
make('<div id="d1"></div>')
div.click()
this.server.respond()
div.innerHTML.should.equal('Clicked')
byId('d1').innerHTML.should.equal('Swapped3')
byId('d1').hasAttribute('data-hx-swap-oob').should.equal(false)
})
it('handles no id match properly', function() {
this.server.respondWith('GET', '/test', "Clicked<div id='d1' hx-swap-oob='true'>Swapped2</div>")
var div = make('<div hx-get="/test">click me</div>')
@@ -155,6 +177,7 @@ describe('hx-swap-oob attribute', function() {
it('swaps into all targets that match the selector (outerHTML)', function() {
var oobSwapContent = '<div class="new-target" hx-swap-oob="outerHTML:.target">Swapped9</div>'
var finalContent = '<div class="new-target">Swapped9</div>'
this.server.respondWith('GET', '/test', '<div>Clicked</div>' + oobSwapContent)
var div = make('<div hx-get="/test">click me</div>')
make('<div id="d1"><div>No swap</div></div>')
@@ -163,8 +186,8 @@ describe('hx-swap-oob attribute', function() {
div.click()
this.server.respond()
byId('d1').innerHTML.should.equal('<div>No swap</div>')
byId('d2').innerHTML.should.equal(oobSwapContent)
byId('d3').innerHTML.should.equal(oobSwapContent)
byId('d2').innerHTML.should.equal(finalContent)
byId('d3').innerHTML.should.equal(finalContent)
})
it('oob swap delete works properly', function() {
@@ -176,6 +199,30 @@ describe('hx-swap-oob attribute', function() {
should.equal(byId('d1'), null)
})
it('oob swap removes templates used for oob encapsulation only properly', function() {
this.server.respondWith('GET', '/test', '' +
'Clicked<template><div hx-swap-oob="outerHTML" id="d1">Foo</div></template>')
var div = make('<button hx-get="/test" id="b1">Click Me</button>' +
'<div id="d1" ></div>')
var btn = byId('b1')
btn.click()
this.server.respond()
should.equal(byId('b1').innerHTML, 'Clicked')
should.equal(byId('d1').innerHTML, 'Foo')
})
it('oob swap keeps templates not used for oob swap encapsulation', function() {
this.server.respondWith('GET', '/test', '' +
'Clicked<template></template>')
var div = make('<button hx-get="/test" id="b1">Click Me</button>' +
'<div id="d1" ></div>')
var btn = byId('b1')
btn.click()
this.server.respond()
should.equal(byId('b1').innerHTML, 'Clicked<template></template>')
should.equal(byId('d1').innerHTML, '')
})
for (const config of [{ allowNestedOobSwaps: true }, { allowNestedOobSwaps: false }]) {
it('oob swap supports table row in fragment along other oob swap elements with config ' + JSON.stringify(config), function() {
Object.assign(htmx.config, config)
@@ -213,4 +260,131 @@ describe('hx-swap-oob attribute', function() {
byId('td1').innerHTML.should.equal('hey')
})
}
for (const config of [{ allowNestedOobSwaps: true }, { allowNestedOobSwaps: false }]) {
it('handles oob target in web components with both inside shadow root and config ' + JSON.stringify(config), function() {
this.server.respondWith('GET', '/test', '<div hx-swap-oob="innerHTML:#oob-swap-target">new contents</div>Clicked')
class TestElement extends HTMLElement {
connectedCallback() {
const root = this.attachShadow({ mode: 'open' })
root.innerHTML = `
<button hx-get="/test" hx-target="next div">Click me!</button>
<div id="main-target"></div>
<div id="oob-swap-target">this should get swapped</div>
`
htmx.process(root) // Tell HTMX about this component's shadow DOM
}
}
var elementName = 'test-oobswap-inside-' + config.allowNestedOobSwaps
customElements.define(elementName, TestElement)
var div = make(`<div><div id="oob-swap-target">this should not get swapped</div><${elementName}/></div>`)
var badTarget = div.querySelector('#oob-swap-target')
var webComponent = div.querySelector(elementName)
var btn = webComponent.shadowRoot.querySelector('button')
var goodTarget = webComponent.shadowRoot.querySelector('#oob-swap-target')
var mainTarget = webComponent.shadowRoot.querySelector('#main-target')
btn.click()
this.server.respond()
should.equal(mainTarget.textContent, 'Clicked')
should.equal(goodTarget.textContent, 'new contents')
should.equal(badTarget.textContent, 'this should not get swapped')
})
}
for (const config of [{ allowNestedOobSwaps: true }, { allowNestedOobSwaps: false }]) {
it('handles oob target in web components with main target outside web component config ' + JSON.stringify(config), function() {
this.server.respondWith('GET', '/test', '<div hx-swap-oob="innerHTML:#oob-swap-target">new contents</div>Clicked')
class TestElement extends HTMLElement {
connectedCallback() {
const root = this.attachShadow({ mode: 'open' })
root.innerHTML = `
<button hx-get="/test" hx-target="global #main-target">Click me!</button>
<div id="main-target"></div>
<div id="oob-swap-target">this should get swapped</div>
`
htmx.process(root) // Tell HTMX about this component's shadow DOM
}
}
var elementName = 'test-oobswap-global-main-' + config.allowNestedOobSwaps
customElements.define(elementName, TestElement)
var div = make(`<div><div id="main-target"></div><div id="oob-swap-target">this should not get swapped</div><${elementName}/></div>`)
var badTarget = div.querySelector('#oob-swap-target')
var webComponent = div.querySelector(elementName)
var btn = webComponent.shadowRoot.querySelector('button')
var goodTarget = webComponent.shadowRoot.querySelector('#oob-swap-target')
var mainTarget = div.querySelector('#main-target')
btn.click()
this.server.respond()
should.equal(mainTarget.textContent, 'Clicked')
should.equal(goodTarget.textContent, 'new contents')
should.equal(badTarget.textContent, 'this should not get swapped')
})
}
for (const config of [{ allowNestedOobSwaps: true }, { allowNestedOobSwaps: false }]) {
it('handles global oob target in web components with main target inside web component config ' + JSON.stringify(config), function() {
this.server.respondWith('GET', '/test', '<div hx-swap-oob="innerHTML:global #oob-swap-target">new contents</div>Clicked')
class TestElement extends HTMLElement {
connectedCallback() {
const root = this.attachShadow({ mode: 'open' })
root.innerHTML = `
<button hx-get="/test" hx-target="next div">Click me!</button>
<div id="main-target"></div>
<div id="oob-swap-target">this should not get swapped</div>
`
htmx.process(root) // Tell HTMX about this component's shadow DOM
}
}
var elementName = 'test-oobswap-global-oob-' + config.allowNestedOobSwaps
customElements.define(elementName, TestElement)
var div = make(`<div><div id="main-target"></div><div id="oob-swap-target">this should get swapped</div><${elementName}/></div>`)
var webComponent = div.querySelector(elementName)
var badTarget = webComponent.shadowRoot.querySelector('#oob-swap-target')
var btn = webComponent.shadowRoot.querySelector('button')
var goodTarget = div.querySelector('#oob-swap-target')
var mainTarget = webComponent.shadowRoot.querySelector('#main-target')
btn.click()
this.server.respond()
should.equal(mainTarget.textContent, 'Clicked')
should.equal(goodTarget.textContent, 'new contents')
should.equal(badTarget.textContent, 'this should not get swapped')
})
}
it.skip('triggers htmx:oobErrorNoTarget when no targets found', function(done) {
// this test fails right now because when targets not found it returns an empty array which makes it miss the event as it should be if (targets.length)
this.server.respondWith('GET', '/test', "Clicked<div id='nonexistent' hx-swap-oob='true'>Swapped</div>")
var div = make('<div hx-get="/test">click me</div>')
// Define the event listener function so it can be removed later
var eventListenerFunction = function(event) {
event.detail.content.innerHTML.should.equal('Swapped')
document.body.removeEventListener('htmx:oobErrorNoTarget', eventListenerFunction)
done()
}
document.body.addEventListener('htmx:oobErrorNoTarget', eventListenerFunction)
div.click()
this.server.respond()
})
it('handles elements with IDs containing special characters properly', function() {
this.server.respondWith('GET', '/test', '<div id="foo-/bar/" hx-swap-oob="innerHTML">Swapped10</div>')
var div = make('<div hx-get="/test">click me</div>')
make('<div id="foo-/bar/">Existing Content</div>')
div.click()
this.server.respond()
var swappedElement = document.querySelector('[id="foo-/bar/"]')
swappedElement.innerHTML.should.equal('Swapped10')
})
it('handles one swap into multiple elements with the same ID properly', function() {
this.server.respondWith('GET', '/test', '<div id="foo-/bar/" hx-swap-oob="innerHTML">Swapped11</div>')
var div = make('<div hx-get="/test">click me</div>')
make('<div id="foo-/bar/">Existing Content 1</div>')
make('<div id="foo-/bar/">Existing Content 2</div>')
div.click()
this.server.respond()
var swappedElements = document.querySelectorAll('[id="foo-/bar/"]')
swappedElements.forEach(function(element) {
element.innerHTML.should.equal('Swapped11')
})
})
})

View File

@@ -83,6 +83,15 @@ describe('hx-swap attribute', function() {
byId('a1').innerHTML.should.equal('Clicked!')
})
it('swap outerHTML on body falls back to innerHTML properly', function() {
var fakebody = htmx._('parseHTML')('<body id="b1">Old Content</body>')
var wa = getWorkArea()
var fragment = htmx._('makeFragment')('<body hx-get="/test" hx-swap="outerHTML">Changed!</body>')
wa.append(fakebody.querySelector('body'))
htmx._('swapOuterHTML')(byId('b1'), fragment, {})
byId('b1').innerHTML.should.equal('Changed!')
})
it('swap beforebegin properly', function() {
var i = 0
this.server.respondWith('GET', '/test', function(xhr) {
@@ -274,6 +283,8 @@ describe('hx-swap attribute', function() {
swapSpec(make("<div hx-swap='settle:0s swap:10'/>")).swapDelay.should.equal(10)
swapSpec(make("<div hx-swap='settle:0s swap:10'/>")).settleDelay.should.equal(0)
swapSpec(make("<div hx-swap='transition:true'/>")).transition.should.equal(true)
swapSpec(make("<div hx-swap='customstyle settle:11 swap:10'/>")).swapStyle.should.equal('customstyle')
})
@@ -300,6 +311,21 @@ describe('hx-swap attribute', function() {
done()
})
if (/chrome/i.test(navigator.userAgent)) {
it('works with transition:true', function(done) {
this.server.respondWith('GET', '/test', 'Clicked!')
var div = make(
"<div hx-get='/test' hx-swap='innerHTML transition:true'></div>"
)
div.click()
this.server.respond()
div.innerText.should.equal('')
setTimeout(function() {
div.innerText.should.equal('Clicked!')
done()
}, 50)
})
}
it('works with a settle delay', function(done) {
this.server.respondWith('GET', '/test', "<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML settle:10ms'></div>")
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML settle:10ms'></div>")
@@ -330,6 +356,115 @@ describe('hx-swap attribute', function() {
}, 30)
})
it('works with scroll:top', function(done) {
this.server.respondWith('GET', '/test', "<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML scroll:#container:top'></div>")
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML scroll:#container:top'></div>")
var container = make('<div id="container" style="overflow: scroll; height: 150px; width: 150px;">' +
'<p>' +
'Far out in the uncharted backwaters of the unfashionable end of the western' +
'spiral arm of the Galaxy lies a small unregarded yellow sun. Orbiting this' +
'at a distance of roughly ninety-two million miles is an utterly' +
'insignificant little blue green planet whose ape-descended life forms are so' +
'amazingly primitive that they still think digital watches are a pretty neat' +
'idea.' +
'</p>' +
'</div>')
container.scrollTop = 10
div.click()
this.server.respond()
div.classList.contains('foo').should.equal(false)
setTimeout(function() {
byId('d1').classList.contains('foo').should.equal(true)
container.scrollTop.should.equal(0)
done()
}, 30)
})
it('works with scroll:bottom', function(done) {
this.server.respondWith('GET', '/test', "<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML scroll:#container:bottom'></div>")
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML scroll:#container:bottom'></div>")
var container = make('<div id="container" style="overflow: scroll; height: 150px; width: 150px;">' +
'<p>' +
'Far out in the uncharted backwaters of the unfashionable end of the western' +
'spiral arm of the Galaxy lies a small unregarded yellow sun. Orbiting this' +
'at a distance of roughly ninety-two million miles is an utterly' +
'insignificant little blue green planet whose ape-descended life forms are so' +
'amazingly primitive that they still think digital watches are a pretty neat' +
'idea.' +
'</p>' +
'</div>')
container.scrollTop = 10
div.click()
this.server.respond()
div.classList.contains('foo').should.equal(false)
setTimeout(function() {
byId('d1').classList.contains('foo').should.equal(true)
container.scrollTop.should.not.equal(10)
done()
}, 30)
})
it('works with show:top', function(done) {
this.server.respondWith('GET', '/test', "<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML show:top'></div>")
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML show:#d2:top'></div>")
var div2 = make("<div id='d2'></div>")
var scrollOptions
div2.scrollIntoView = function(options) { scrollOptions = options }
div.click()
this.server.respond()
div.classList.contains('foo').should.equal(false)
setTimeout(function() {
byId('d1').classList.contains('foo').should.equal(true)
scrollOptions.block.should.equal('start')
done()
}, 30)
})
it('works with show:bottom', function(done) {
this.server.respondWith('GET', '/test', "<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML show:bottom'></div>")
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML show:#d2:bottom'></div>")
var div2 = make("<div id='d2'></div>")
var scrollOptions
div2.scrollIntoView = function(options) { scrollOptions = options }
div.click()
this.server.respond()
div.classList.contains('foo').should.equal(false)
setTimeout(function() {
byId('d1').classList.contains('foo').should.equal(true)
scrollOptions.block.should.equal('end')
done()
}, 30)
})
it('works with show:window:bottom', function(done) {
this.server.respondWith('GET', '/test', "<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML show:window:bottom'></div>")
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML show:window:bottom'></div>")
var scrollOptions
document.body.scrollIntoView = function(options) { scrollOptions = options }
div.click()
this.server.respond()
div.classList.contains('foo').should.equal(false)
setTimeout(function() {
byId('d1').classList.contains('foo').should.equal(true)
scrollOptions.block.should.equal('end')
done()
}, 30)
})
it('works with focus-scroll:true', function(done) {
// no easy way to tell if the scroll worked as expected
this.server.respondWith('GET', '/test', "<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML focus-scroll:true'><input id='i2' type='text'></div>")
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML focus-scroll:true'><input id='i2' type='text'></div>")
byId('i2').focus()
div.click()
this.server.respond()
div.classList.contains('foo').should.equal(false)
setTimeout(function() {
byId('d1').classList.contains('foo').should.equal(true)
done()
}, 30)
})
it('swap outerHTML properly w/ data-* prefix', function() {
this.server.respondWith('GET', '/test', '<a id="a1" data-hx-get="/test2">Click Me</a>')
this.server.respondWith('GET', '/test2', 'Clicked!')
@@ -402,4 +537,27 @@ describe('hx-swap attribute', function() {
btn.innerText.should.equal('Clicked!')
window.document.title.should.equal('Test Title')
})
it('swapError fires if swap throws exception', function() {
try {
// override makeSettleInfo to cause swap function to throw exception
htmx._('htmx.backupMakeSettleInfo = makeSettleInfo')
htmx._('makeSettleInfo = function() { throw new Error("throw") }')
var error = false
var handler = htmx.on('htmx:swapError', function(evt) {
error = true
})
this.server.respondWith('GET', '/test', 'Clicked!')
var div = make("<div hx-get='/test'></div>")
div.click()
this.server.respond()
} catch (e) {
} finally {
div.innerHTML.should.equal('')
error.should.equal(true)
htmx.off('htmx:swapError', handler)
htmx._('makeSettleInfo = htmx.backupMakeSettleInfo')
}
})
})

View File

@@ -64,6 +64,7 @@ describe('hx-trigger attribute', function() {
div.innerHTML.should.equal('Requests: 1')
})
// This test and the next one should be kept in sync.
it('changed modifier works along from clause with two inputs', function() {
var requests = 0
this.server.respondWith('GET', '/test', function(xhr) {
@@ -106,6 +107,92 @@ describe('hx-trigger attribute', function() {
div.innerHTML.should.equal('Requests: 2')
})
// This test and the previous one should be kept in sync.
it('changed modifier counts each triggerspec separately', function() {
var requests = 0
this.server.respondWith('GET', '/test', function(xhr) {
requests++
xhr.respond(200, {}, 'Requests: ' + requests)
})
var input1 = make('<input type="text"/>')
var input2 = make('<input type="text"/>')
make('<div hx-trigger="click changed from:input" hx-target="#d1" hx-get="/test"></div>')
make('<div hx-trigger="click changed from:input" hx-target="#d1" hx-get="/test"></div>')
var div = make('<div id="d1"></div>')
input1.click()
this.server.respond()
div.innerHTML.should.equal('')
input2.click()
this.server.respond()
div.innerHTML.should.equal('')
input1.value = 'bar'
input2.click()
this.server.respond()
div.innerHTML.should.equal('')
input1.click()
this.server.respond()
div.innerHTML.should.equal('Requests: 2')
input1.click()
this.server.respond()
div.innerHTML.should.equal('Requests: 2')
input2.click()
this.server.respond()
div.innerHTML.should.equal('Requests: 2')
input2.value = 'foo'
input1.click()
this.server.respond()
div.innerHTML.should.equal('Requests: 2')
input2.click()
this.server.respond()
div.innerHTML.should.equal('Requests: 4')
})
it('separate changed modifier works along from clause with two inputs', function() {
var requests = 0
this.server.respondWith('GET', '/test', function(xhr) {
requests++
xhr.respond(200, {}, 'Requests: ' + requests)
})
var input1 = make('<input type="text"/>')
var input2 = make('<input type="text"/>')
make('<div hx-trigger="click changed from:input:nth-child(1), click changed from:input:nth-child(2)" hx-target="#d1" hx-get="/test"></div>')
var div = make('<div id="d1"></div>')
input1.click()
this.server.respond()
div.innerHTML.should.equal('')
input2.click()
this.server.respond()
div.innerHTML.should.equal('')
input1.value = 'bar'
input2.click()
this.server.respond()
div.innerHTML.should.equal('')
input1.click()
this.server.respond()
div.innerHTML.should.equal('Requests: 1')
input1.click()
this.server.respond()
div.innerHTML.should.equal('Requests: 1')
input2.click()
this.server.respond()
div.innerHTML.should.equal('Requests: 1')
input2.value = 'foo'
input1.click()
this.server.respond()
div.innerHTML.should.equal('Requests: 1')
input2.click()
this.server.respond()
div.innerHTML.should.equal('Requests: 2')
})
it('once modifier works', function() {
var requests = 0
this.server.respondWith('GET', '/test', function(xhr) {
@@ -367,7 +454,7 @@ describe('hx-trigger attribute', function() {
div.dispatchEvent(event)
this.server.respond()
div.innerHTML.should.equal('Not Called')
foo = true
window.foo = true
div.dispatchEvent(event)
this.server.respond()
div.innerHTML.should.equal('Called!')
@@ -409,6 +496,47 @@ describe('hx-trigger attribute', function() {
}
})
it('filters properly with true for empty condition', function() {
this.server.respondWith('GET', '/test', 'Called!')
var form = make('<form hx-get="/test" hx-trigger="evt[]">Not Called</form>')
form.click()
form.innerHTML.should.equal('Not Called')
var event = htmx._('makeEvent')('evt')
form.dispatchEvent(event)
this.server.respond()
form.innerHTML.should.equal('Called!')
})
it('syntax error in condition issues error', function() {
this.server.respondWith('GET', '/test', 'Called!')
var errorEvent = null
var handler = htmx.on('htmx:syntax:error', function(event) {
errorEvent = event
})
var form = make('<form hx-get="/test" hx-trigger="evt[{]">Not Called</form>')
try {
var event = htmx._('makeEvent')('evt')
form.dispatchEvent(event)
should.not.equal(null, errorEvent)
should.not.equal(null, errorEvent.detail.source)
console.log(errorEvent.detail.source)
} finally {
htmx.off('htmx:syntax:error', handler)
}
})
it('filters properly with condition containing square backets', function() {
this.server.respondWith('GET', '/test', 'Called!')
var form = make('<form hx-get="/test" hx-trigger="evt[foo[0]]">Not Called</form>')
form.click()
form.innerHTML.should.equal('Not Called')
var event = htmx._('makeEvent')('evt')
event.foo = [true]
form.dispatchEvent(event)
this.server.respond()
form.innerHTML.should.equal('Called!')
})
it('from clause works', function() {
var requests = 0
this.server.respondWith('GET', '/test', function(xhr) {
@@ -570,6 +698,26 @@ describe('hx-trigger attribute', function() {
div1.innerHTML.should.equal('Requests: 2')
})
it('from clause works with multiple extended selectors', function() {
var requests = 0
this.server.respondWith('GET', '/test', function(xhr) {
requests++
xhr.respond(200, {}, 'Requests: ' + requests)
})
make('<button id="btn" type="button">Click me</button>' +
'<div hx-trigger="click from:(previous button, next a)" hx-target="#a1" hx-get="/test"></div>' +
'<a id="a1">Requests: 0</a>')
var btn = byId('btn')
var a1 = byId('a1')
a1.innerHTML.should.equal('Requests: 0')
btn.click()
this.server.respond()
a1.innerHTML.should.equal('Requests: 1')
a1.click()
this.server.respond()
a1.innerHTML.should.equal('Requests: 2')
})
it('event listeners can filter on target', function() {
var requests = 0
this.server.respondWith('GET', '/test', function(xhr) {
@@ -657,7 +805,7 @@ describe('hx-trigger attribute', function() {
}, 50)
})
it('A throttle of 0 does not multiple requests from happening', function(done) {
it('A throttle of 0 does not prevent multiple requests from happening', function(done) {
var requests = 0
var server = this.server
server.respondWith('GET', '/test', function(xhr) {
@@ -875,6 +1023,60 @@ describe('hx-trigger attribute', function() {
div2.innerHTML.should.equal('test 2')
})
it('scrolling triggers revealed event', function(done) {
this.server.respondWith('GET', '/test', 'test')
this.server.autoRespond = true
this.server.autoRespondAfter = 0
var div = make('<div hx-get="/test" hx-trigger="revealed"></div>')
div.innerHTML.should.equal('')
div.scrollIntoView({ block: 'end', behavior: htmx.config.scrollBehavior })
htmx.trigger(document.body, 'scroll')
setTimeout(function() {
div.innerHTML.should.equal('test')
done()
}, 250)
})
if (/headlesschrome/i.test(navigator.userAgent)) {
it('scrolling triggers intersect event', function(done) {
// test only works reliably with playwright
this.server.respondWith('GET', '/test', 'test')
this.server.autoRespond = true
this.server.autoRespondAfter = 0
var div = make('<div hx-get="/test" hx-trigger="intersect"></div>')
div.innerHTML.should.equal('')
div.scrollIntoView({ block: 'end', behavior: htmx.config.scrollBehavior })
htmx.trigger(document.body, 'scroll')
setTimeout(function() {
div.innerHTML.should.equal('test')
done()
}, 250)
})
}
it('triggering revealed while component not yet inited still works', function(done) {
this.server.respondWith('GET', '/test', 'test')
var div = make('<div hx-get="/test" hx-trigger="revealed"></div>')
var data = div['htmx-internal-data']
delete data.initHash // simulate not inited or revealed yet
div.removeAttribute('data-hx-revealed')
var server1 = this.server
div.innerHTML.should.equal('')
div.scrollIntoView({ block: 'end', behavior: htmx.config.scrollBehavior })
htmx.trigger(document.body, 'scroll')
setTimeout(function() {
server1.autoRespond = true
server1.autoRespondAfter = 0
htmx.process(div) // processing the div should also trigger revealed event now
setTimeout(function() {
div.innerHTML.should.equal('test')
done()
}, 10)
}, 250)
})
it('reveal event works when triggered by window', function() {
this.server.respondWith('GET', '/test1', 'test 1')
var div = make('<div hx-get="/test1" hx-trigger="revealed" style="position: fixed; top: 1px; left: 1px; border: 3px solid red">foo</div>')
@@ -939,6 +1141,45 @@ describe('hx-trigger attribute', function() {
}
})
it('fires the htmx:trigger event for delayed triggers', function(done) {
var param = 'foo'
var handler = htmx.on('htmx:trigger', function(evt) {
param = 'bar'
})
var div = make('<button hx-trigger="click delay:10ms">Submit</button>')
div.click()
setTimeout(function() {
try {
should.equal(param, 'bar')
done()
} finally {
htmx.off('htmx:trigger', handler)
}
}, 50)
})
it('fires the htmx:trigger event when the trigger is a load', function(done) {
this.server.respondWith(
'GET',
'/test',
'<div hx-trigger="load delay:50ms" hx-on::trigger="this.innerText = \'Done\'">Response</div>'
)
var div = make('<div hx-get="/test">Submit</div>')
div.click()
this.server.respond()
var response = div.children[0]
response.innerText.should.equal('Response')
setTimeout(function() {
try {
response.innerText.should.equal('Done')
done()
} finally {
}
}, 100)
})
it('filters support "this" reference to the current element', function() {
this.server.respondWith('GET', '/test', 'Called!')
var form = make('<form hx-get="/test" hx-trigger="click[this.classList.contains(\'bar\')]">Not Called</form>')
@@ -984,10 +1225,6 @@ describe('hx-trigger attribute', function() {
it('correctly handles CSS descendant combinators in modifier target', function() {
this.server.respondWith('GET', '/test', 'Called')
document.addEventListener('htmx:syntax:error', function(evt) {
chai.assert.fail('htmx:syntax:error')
})
make('<div class="d1"><a id="a1" class="a1">Click me</a><a id="a2" class="a2">Click me</a></div>')
var div = make('<div hx-trigger="click from:body target:(.d1 .a2)" hx-get="/test">Not Called</div>')
@@ -1002,12 +1239,52 @@ describe('hx-trigger attribute', function() {
it('correctly handles CSS descendant combinators in modifier root', function() {
this.server.respondWith('GET', '/test', 'Called')
document.addEventListener('htmx:syntax:error', function(evt) {
chai.assert.fail('htmx:syntax:error')
var errorEvent = null
var handler = htmx.on('htmx:syntax:error', function(event) {
errorEvent = event
})
var form = make('<div hx-trigger="intersect root:{form input}" hx-get="/test">Not Called</div>')
try {
var event = htmx._('makeEvent')('evt')
form.dispatchEvent(event)
should.equal(null, errorEvent)
} finally {
htmx.off('htmx:syntax:error', handler)
}
})
make('<div hx-trigger="intersect root:{form input}" hx-get="/test">Not Called</div>')
it('correctly handles intersect with modifier threshold', function() {
this.server.respondWith('GET', '/test', 'Called')
var errorEvent = null
var handler = htmx.on('htmx:syntax:error', function(event) {
errorEvent = event
})
var form = make('<div hx-trigger="intersect threshold:0.5" hx-get="/test">Not Called</div>')
try {
var event = htmx._('makeEvent')('evt')
form.dispatchEvent(event)
should.equal(null, errorEvent)
} finally {
htmx.off('htmx:syntax:error', handler)
}
})
it('issues error with invalid trigger spec', function() {
this.server.respondWith('GET', '/test', 'Called')
var errorEvent = null
var handler = htmx.on('htmx:syntax:error', function(event) {
errorEvent = event
})
var form = make('<div hx-trigger="intersect invalid:0.5" hx-get="/test">Not Called</div>')
try {
var event = htmx._('makeEvent')('evt')
form.dispatchEvent(event)
should.not.equal(null, errorEvent)
should.not.equal(null, errorEvent.detail.source)
console.log(errorEvent.detail.source)
} finally {
htmx.off('htmx:syntax:error', handler)
}
})
it('uses trigger specs cache if defined', function() {
@@ -1062,4 +1339,35 @@ describe('hx-trigger attribute', function() {
htmx.config.triggerSpecsCache = initialCacheConfig
})
it('handles spaces at the end of trigger specs', function() {
var requests = 0
this.server.respondWith('GET', '/test', function(xhr) {
requests++
xhr.respond(200, {}, 'Requests: ' + requests)
})
var div = make('<div hx-trigger="load , click consume " hx-get="/test">Requests: 0</div>')
div.innerHTML.should.equal('Requests: 0')
this.server.respond()
div.innerHTML.should.equal('Requests: 1')
div.click()
this.server.respond()
div.innerHTML.should.equal('Requests: 2')
})
it('Removing polling trigger and processing node removes timeout', function(complete) {
this.server.respondWith('GET', '/test', 'Called!')
var div = make('<div hx-get="/test" hx-trigger="every 5ms">Not Called</div>')
div.removeAttribute('hx-trigger')
should.not.equal(div['htmx-internal-data'].timeout, undefined)
htmx.process(div)
should.equal(div['htmx-internal-data'].timeout, undefined)
this.server.autoRespond = true
this.server.autoRespondAfter = 0
setTimeout(function() {
div.innerHTML.should.equal('Not Called')
delete window.foo
complete()
}, 30)
})
})

View File

@@ -45,6 +45,23 @@ describe('hx-vals attribute', function() {
div.innerHTML.should.equal('Clicked!')
})
it('Dynamic hx-vals using spread operator works', function() {
this.server.respondWith('POST', '/vars', function(xhr) {
var params = getParameters(xhr)
params.v1.should.equal('test')
params.v2.should.equal('42')
xhr.respond(200, {}, 'Clicked!')
})
window.foo = function() {
return { v1: 'test', v2: 42 }
}
var div = make("<div hx-post='/vars' hx-vals='js:{...foo()}'></div>")
div.click()
this.server.respond()
div.innerHTML.should.equal('Clicked!')
delete window.foo
})
it('hx-vals can be on parents', function() {
this.server.respondWith('POST', '/vars', function(xhr) {
var params = getParameters(xhr)
@@ -120,7 +137,7 @@ describe('hx-vals attribute', function() {
div.innerHTML.should.equal('Clicked!')
})
it('multiple hx-vals works', function() {
it('multiple hx-vals works with javascript', function() {
this.server.respondWith('POST', '/vars', function(xhr) {
var params = getParameters(xhr)
params.v1.should.equal('test')
@@ -133,7 +150,7 @@ describe('hx-vals attribute', function() {
div.innerHTML.should.equal('Clicked!')
})
it('hx-vals can be on parents', function() {
it('hx-vals can be on parents with javascript', function() {
this.server.respondWith('POST', '/vars', function(xhr) {
var params = getParameters(xhr)
params.i1.should.equal('test')
@@ -146,7 +163,7 @@ describe('hx-vals attribute', function() {
div.innerHTML.should.equal('Clicked!')
})
it('hx-vals can override parents', function() {
it('hx-vals can override parents with javascript', function() {
this.server.respondWith('POST', '/vars', function(xhr) {
var params = getParameters(xhr)
params.i1.should.equal('best')
@@ -159,7 +176,7 @@ describe('hx-vals attribute', function() {
div.innerHTML.should.equal('Clicked!')
})
it('hx-vals overrides inputs', function() {
it('hx-vals overrides inputs with javascript', function() {
this.server.respondWith('POST', '/include', function(xhr) {
var params = getParameters(xhr)
params.i1.should.equal('best')
@@ -297,4 +314,66 @@ describe('hx-vals attribute', function() {
}
calledEvent.should.equal(true)
})
it('using js: with hx-vals has event available', function() {
this.server.respondWith('POST', '/vars', function(xhr) {
var params = getParameters(xhr)
params.i1.should.equal('test')
xhr.respond(200, {}, 'Clicked!')
})
var div = make('<div id="test" hx-post="/vars" hx-vals="js:i1:event.target.id"></div>')
div.click()
this.server.respond()
div.innerHTML.should.equal('Clicked!')
})
it('using js: with hx-vals has event available when used with a delay', function(done) {
var params = null
var div = make('<div id="test" hx-post="/vars" hx-vals="js:{i1:event.target.id}" hx-trigger="click delay:10ms"></div>')
htmx.on(div, 'htmx:configRequest', function(evt) {
evt.preventDefault()
params = evt.detail.parameters
}, { once: true })
div.click()
new Promise(resolve => setTimeout(resolve, 20)).then(function() {
params.i1.should.equal('test')
done()
}).catch(done)
})
it('hx-vals works with null values', function() {
this.server.respondWith('POST', '/vars', function(xhr) {
var params = getParameters(xhr)
params.i1.should.equal('null')
xhr.respond(200, {}, 'Clicked!')
})
var div = make("<div hx-post='/vars' hx-vals='{\"i1\": null }'></div>")
div.click()
this.server.respond()
div.innerHTML.should.equal('Clicked!')
})
it('hx-vals works with object values', function() {
this.server.respondWith('POST', '/vars', function(xhr) {
var params = getParameters(xhr)
params.i1.should.equal('{"a":"b"}')
xhr.respond(200, {}, 'Clicked!')
})
var div = make("<div hx-post='/vars' hx-vals='{\"i1\": { \"a\": \"b\" } }'></div>")
div.click()
this.server.respond()
div.innerHTML.should.equal('Clicked!')
})
it('js: this refers to the element with the hx-vals attribute', function() {
this.server.respondWith('POST', '/vars', function(xhr) {
var params = getParameters(xhr)
params.i1.should.equal('test')
xhr.respond(200, {}, 'Clicked!')
})
var div = make('<div hx-post="/vars" hx-vals="javascript:{ ...this.dataset }" data-i1="test"></div>')
div.click()
this.server.respond()
div.innerHTML.should.equal('Clicked!')
})
})

View File

@@ -559,6 +559,34 @@ describe('Core htmx AJAX Tests', function() {
values.should.deep.equal({ c1: ['cb1', 'cb3'], c2: 'cb5', c3: 'cb6' })
})
it('properly handles radio inputs', function() {
var values
this.server.respondWith('Post', '/test', function(xhr) {
values = getParameters(xhr)
xhr.respond(204, {}, '')
})
var form = make('<form hx-post="/test" hx-trigger="click">' +
'<div role="radiogroup">' +
'<input id="rb1" name="r1" value="rb1" type="radio">' +
'<input id="rb2" name="r1" value="rb2" type="radio">' +
'<input id="rb3" name="r1" value="rb3" type="radio">' +
'<input id="rb4" name="r2" value="rb4" type="radio">' +
'<input id="rb5" name="r2" value="rb5" type="radio">' +
'<input id="rb6" name="r3" value="rb6" type="radio">' +
'</div>' +
'</form>')
form.click()
this.server.respond()
values.should.deep.equal({})
byId('rb1').checked = true
form.click()
this.server.respond()
values.should.deep.equal({ r1: 'rb1' })
})
it('text nodes dont screw up settling via variable capture', function() {
this.server.respondWith('GET', '/test', "<div id='d1' hx-trigger='click consume' hx-get='/test2'></div>fooo")
this.server.respondWith('GET', '/test2', 'clicked')
@@ -936,7 +964,11 @@ describe('Core htmx AJAX Tests', function() {
it('scripts w/ src attribute are properly loaded', function(done) {
try {
this.server.respondWith('GET', '/test', "<script id='setGlobalScript' src='setGlobal.js'></script>")
if (byId('mocha')) {
this.server.respondWith('GET', '/test', "<script id='setGlobalScript' src='setGlobal.js'></script>")
} else {
this.server.respondWith('GET', '/test', "<script id='setGlobalScript' src='/test/setGlobal.js'></script>")
}
var div = make("<div hx-get='/test'></div>")
div.click()
this.server.respond()
@@ -1156,6 +1188,48 @@ describe('Core htmx AJAX Tests', function() {
values.should.deep.equal({ t1: 'textValue', b1: ['inputValue', 'buttonValue'] })
})
it('sends referenced form values when a button referencing another form is clicked', function() {
var values
this.server.respondWith('POST', '/test3', function(xhr) {
values = getParameters(xhr)
xhr.respond(205, {}, '')
})
make('<form id="externalForm" hx-post="/test">' +
'<input type="text" name="t1" value="textValue">' +
'<input type="hidden" name="b1" value="inputValue">' +
'</form>' +
'<form hx-post="/test2">' +
'<input type="text" name="t1" value="checkValue">' +
'<button id="submit" form="externalForm" hx-post="/test3" type="submit" name="b1" value="buttonValue">button</button>' +
'</form>')
byId('submit').click()
this.server.respond()
values.should.deep.equal({ t1: 'textValue', b1: ['inputValue', 'buttonValue'] })
})
it('sends referenced form values when a submit input referencing another form is clicked', function() {
var values
this.server.respondWith('POST', '/test3', function(xhr) {
values = getParameters(xhr)
xhr.respond(204, {}, '')
})
make('<form id="externalForm" hx-post="/test">' +
'<input type="text" name="t1" value="textValue">' +
'<input type="hidden" name="b1" value="inputValue">' +
'</form>' +
'<form hx-post="/test2">' +
'<input type="text" name="t1" value="checkValue">' +
'<input id="submit" form="externalForm" hx-post="/test3" type="submit" name="b1" value="buttonValue">' +
'</form>')
byId('submit').click()
this.server.respond()
values.should.deep.equal({ t1: 'textValue', b1: ['inputValue', 'buttonValue'] })
})
it('properly handles inputs external to form', function() {
var values
this.server.respondWith('Post', '/test', function(xhr) {
@@ -1178,32 +1252,18 @@ describe('Core htmx AJAX Tests', function() {
values.should.deep.equal({ t1: 'textValue', b1: ['inputValue', 'buttonValue'], s1: 'selectValue' })
})
it('handles form post with button formmethod dialog properly', function() {
var values
it('properly handles buttons with formmethod=dialog', function() {
var request = false
this.server.respondWith('POST', '/test', function(xhr) {
values = getParameters(xhr)
xhr.respond(200, {}, '')
request = true
xhr.respond(200, {}, '<button>Bar</button>')
})
make('<dialog><form hx-post="/test"><button id="submit" formmethod="dialog" name="foo" value="bar">submit</button></form></dialog>')
make('<dialog><form hx-post="/test"><button id="submit" formmethod="dialog" name="foo" value="bar">Submit</button></form></dialog>')
byId('submit').click()
this.server.respond()
values.should.deep.equal({ foo: 'bar' })
})
it('handles form get with button formmethod dialog properly', function() {
var responded = false
this.server.respondWith('GET', '/test', function(xhr) {
responded = true
xhr.respond(200, {}, '')
})
make('<dialog><form hx-get="/test"><button id="submit" formmethod="dialog">submit</button></form></dialog>')
byId('submit').click()
this.server.respond()
responded.should.equal(true)
request.should.equal(false)
})
it('can associate submit buttons from outside a form with the current version of the form after swap', function() {
@@ -1234,4 +1294,66 @@ describe('Core htmx AJAX Tests', function() {
this.server.respond()
values.should.deep.equal({ name: '', outside: '' })
})
it('properly handles form reset behaviour with a htmx enabled reset button inside a form', function() {
var values
this.server.respondWith('POST', '/reset', function(xhr) {
values = getParameters(xhr)
xhr.respond(204, {}, '')
})
make('<form id="externalForm" hx-post="/test">' +
'<input id="t1" type="text" name="t1" value="defaultValue">' +
'<button hx-post="/reset" id="reset" type="reset" name="b1" value="buttonValue">reset</button>' +
'</form>')
byId('t1').value = 'otherValue'
byId('reset').click()
this.server.respond()
values.should.deep.equal({ b1: 'buttonValue', t1: 'otherValue' })
byId('t1').value.should.equal('defaultValue')
})
it('properly handles form reset behaviour with a htmx enabled reset button outside a form', function() {
var values
this.server.respondWith('POST', '/reset', function(xhr) {
values = getParameters(xhr)
xhr.respond(204, {}, '')
})
make('<form id="externalForm" hx-post="/test">' +
'<input id="t1" type="text" name="t1" value="defaultValue">' +
'</form>' +
'<button hx-post="/reset" id="reset" form="externalForm" type="reset" name="b1" value="buttonValue">reset</button>')
byId('t1').value = 'otherValue'
byId('reset').click()
this.server.respond()
values.should.deep.equal({ b1: 'buttonValue', t1: 'otherValue' })
byId('t1').value.should.equal('defaultValue')
})
it('script tags get swapped in with nonce applied from inlineScriptNonce', function() {
var globalWasCalled = false
window.callGlobal = function() {
globalWasCalled = true
}
htmx.config.inlineScriptNonce = 'testnonce'
try {
this.server.respondWith('GET', '/test', "<script id='noncescript'>callGlobal()</script>")
var div = make("<div hx-get='/test'></div>")
div.click()
this.server.respond()
globalWasCalled.should.equal(true)
byId('noncescript').nonce.should.equal('testnonce')
} finally {
delete window.callGlobal
htmx.config.inlineScriptNonce = ''
}
})
it('normalizeScriptTags logs error when insertBefore fails', function() {
htmx.div = make('<div><script></script></div>')
htmx.div.insertBefore = undefined
htmx._('normalizeScriptTags(htmx.div)')
delete htmx.div
})
})

View File

@@ -48,6 +48,17 @@ describe('Core htmx API test', function() {
div.innerHTML.should.equal('')
})
it('remove element with delay properly', function(done) {
var div = make('<div><a></a></div>')
var a = htmx.find(div, 'a')
htmx.remove(a, 10)
div.innerHTML.should.not.equal('')
setTimeout(function() {
div.innerHTML.should.equal('')
done()
}, 30)
})
it('should remove element properly w/ selector', function() {
var div = make("<div><a id='a1'></a></div>")
var a = htmx.find(div, 'a')
@@ -88,6 +99,10 @@ describe('Core htmx API test', function() {
div.classList.contains('foo').should.equal(false)
})
it('should not error if you remove class from invalid element', function() {
htmx.removeClass(null, 'foo')
})
it('should remove class properly w/ selector', function() {
var div = make("<div id='div1'></div>")
htmx.addClass(div, 'foo')
@@ -96,7 +111,7 @@ describe('Core htmx API test', function() {
div.classList.contains('foo').should.equal(false)
})
it('should add class properly after delay', function(done) {
it('should remove class properly after delay', function(done) {
var div = make('<div></div>')
htmx.addClass(div, 'foo')
div.classList.contains('foo').should.equal(true)
@@ -213,6 +228,83 @@ describe('Core htmx API test', function() {
div.innerHTML.should.equal('foo!')
})
it('ajax api does not fall back to body when target invalid', function() {
this.server.respondWith('GET', '/test', 'foo!')
var div = make("<div id='d1'></div>")
htmx.ajax('GET', '/test', '#d2')
this.server.respond()
document.body.innerHTML.should.not.equal('foo!')
})
it('ajax api fails when target invalid', function(done) {
this.server.respondWith('GET', '/test', 'foo!')
var div = make("<div id='d1'></div>")
htmx.ajax('GET', '/test', '#d2').then(
(value) => {
},
(reason) => {
done()
}
)
this.server.respond()
div.innerHTML.should.equal('')
})
it('ajax api fails when target invalid even if source set', function(done) {
this.server.respondWith('GET', '/test', 'foo!')
var div = make("<div id='d1'></div>")
htmx.ajax('GET', '/test', {
source: div,
target: '#d2'
}).then(
(value) => {
},
(reason) => {
done()
}
)
this.server.respond()
div.innerHTML.should.equal('')
})
it('ajax api fails when source invalid and no target set', function(done) {
this.server.respondWith('GET', '/test', 'foo!')
var div = make("<div id='d1'></div>")
htmx.ajax('GET', '/test', {
source: '#d2'
}).then(
(value) => {
},
(reason) => {
done()
}
)
this.server.respond()
div.innerHTML.should.equal('')
})
it('ajax api falls back to targeting source if target not set', function() {
this.server.respondWith('GET', '/test', 'foo!')
var div = make("<div id='d1'></div>")
htmx.ajax('GET', '/test', {
source: div
})
this.server.respond()
div.innerHTML.should.equal('foo!')
})
it('ajax api falls back to targeting body if target and source not set', function() {
var target
this.server.respondWith('GET', '/test', 'foo!')
htmx.on(document.body, 'htmx:configRequest', function(evt) {
target = evt.detail.target
return false
})
htmx.ajax('GET', '/test', { swap: 'none' })
this.server.respond()
target.should.equal(document.body)
})
it('ajax api works with swapSpec', function() {
this.server.respondWith('GET', '/test', "<p class='test'>foo!</p>")
var div = make("<div><div id='target'></div></div>")
@@ -229,6 +321,16 @@ describe('Core htmx API test', function() {
div.innerHTML.should.equal('<div id="d2">bar</div>')
})
it('ajax api works with selectOOB', function() {
this.server.respondWith('GET', '/test', "<div id='oob'>OOB Content</div><div>Main Content</div>")
var target = make("<div id='target'></div>")
var oobDiv = make("<div id='oob'></div>")
htmx.ajax('GET', '/test', { target: '#target', selectOOB: '#oob:innerHTML' })
this.server.respond()
target.innerHTML.should.equal('<div>Main Content</div>')
oobDiv.innerHTML.should.equal('OOB Content')
})
it('ajax api works with Hx-Select overrides select', function() {
this.server.respondWith('GET', '/test', [200, { 'HX-Reselect': '#d2' }, "<div id='d1'>foo</div><div id='d2'>bar</div>"])
var div = make("<div id='target'></div>")
@@ -318,6 +420,17 @@ describe('Core htmx API test', function() {
div.innerHTML.should.equal('delete')
})
it('does not trigger load on re-init of an existing element', function() {
this.server.respondWith('GET', '/test', 'test')
var div = make('<div hx-get="/test" hx-trigger="load" hx-swap="beforeend"></div>')
this.server.respond()
div.innerHTML.should.equal('test')
div.setAttribute('hx-swap', 'afterbegin')
htmx.process(div)
this.server.respond()
div.innerHTML.should.equal('test')
})
it('onLoad is called... onLoad', function() {
// also tests on/off
this.server.respondWith('GET', '/test', "<div id='d1' hx-get='/test'></div>")
@@ -382,6 +495,29 @@ describe('Core htmx API test', function() {
output.innerHTML.should.be.equal('<div>Swapped!</div>')
})
it('swap works with a swap delay', function(done) {
var div = make("<div hx-get='/test'></div>")
div.innerText.should.equal('')
htmx.swap(div, 'jsswapped', { swapDelay: 10 })
div.innerText.should.equal('')
setTimeout(function() {
div.innerText.should.equal('jsswapped')
done()
}, 30)
})
if (/chrome/i.test(navigator.userAgent)) {
it('swap works with a view transition', function(done) {
var div = make("<div hx-get='/test'></div>")
div.innerText.should.equal('')
htmx.swap(div, 'jsswapped', { transition: true })
div.innerText.should.equal('')
setTimeout(function() {
div.innerText.should.equal('jsswapped')
done()
}, 50)
})
}
it('swaps content properly (with select)', function() {
var output = make('<output id="output"/>')
htmx.swap('#output', '<div><p id="select-me">Swapped!</p></div>', { swapStyle: 'innerHTML' }, { select: '#select-me' })
@@ -403,4 +539,193 @@ describe('Core htmx API test', function() {
output.innerHTML.should.be.equal('<div>Swapped!</div>')
oobDiv.innerHTML.should.be.equal('OOB Swapped!')
})
it('swap delete works when parent is removed', function() {
this.server.respondWith('DELETE', '/test', 'delete')
var parent = make('<div><div id="d1" hx-swap="delete" hx-delete="/test">click me</div></div>')
var div = htmx.find(parent, '#d1')
div.click()
div.remove()
parent.remove()
this.server.respond()
parent.children.length.should.equal(0)
})
it('swap outerHTML works when parent is removed', function() {
this.server.respondWith('GET', '/test', 'delete')
var parent = make('<div><div id="d1" hx-swap="outerHTML" hx-get="/test">click me</div></div>')
var div = htmx.find(parent, '#d1')
div.click()
div.remove()
parent.remove()
this.server.respond()
parent.children.length.should.equal(0)
})
it('values api returns formDataProxy with correct form data even if clicked button removed', function() {
make('<form id="valuesform" hx-post="/test">' +
'<input type="text" name="t1" value="textValue">' +
'<button id="submit" type="submit" name="b1" value="buttonValue">button</button>' +
'</form>')
var apiValues = htmx.values(byId('valuesform'), 'post')
apiValues.get('t1').should.equal('textValue')
should.equal(apiValues.get('b1'), null)
byId('submit').click()
apiValues = htmx.values(byId('valuesform'), 'post')
apiValues.get('t1').should.equal('textValue')
apiValues.get('b1').should.equal('buttonValue')
byId('submit').remove()
apiValues = htmx.values(byId('valuesform'), 'post')
JSON.stringify(apiValues).should.equal('{"t1":"textValue"}')
var assign = Object.assign({}, apiValues)
JSON.stringify(assign).should.equal('{"t1":"textValue"}')
})
it('tests for formDataProxy array updating and testing for loc coverage', function() {
var form = make('<form><input id="i1" name="foo" value="bar"/><input id="i2" name="do" value="rey"/><input id="i2" name="do" value="rey"/></form>')
var vals = htmx.values(form, 'post')
vals.foo.should.equal('bar')
vals.do.should.deep.equal(['rey', 'rey'])
should.equal(vals.do.toString(), undefined)
vals.do.push('test')
vals.do.should.deep.equal(['rey', 'rey', 'test'])
vals.do = ['bob', 'jim']
vals.do.should.deep.equal(['bob', 'jim'])
vals.do[0] = 'hi'
vals.do.should.deep.equal(['hi', 'jim'])
var arr = vals.do
arr[0] = ['override']
arr[0].should.equal('override')
vals.do.should.deep.equal(['override', 'jim'])
vals[Symbol.toStringTag].should.equal('FormData')
try {
vals[Symbol.toStringTag] = 'notFormData' // should do nothing
} catch (e) {}
vals[Symbol.toStringTag].should.equal('FormData')
})
it('logAll() and logNone() run without error', function() {
make('<div id="d1"></div>')
htmx.logAll()
htmx.trigger(byId('d1'), 'test-event')
htmx.logNone()
})
it('querySelectorExt internal extension api works with just string', function() {
make('<div id="d1">content</div>')
var div = htmx._('querySelectorExt("#d1")')
div.innerHTML.should.equal('content')
})
it('ajax api with no context works', function() {
// request would replace body so prevent ths with 204 response
var status
var handler = htmx.on('htmx:beforeSwap', function(event) {
status = event.detail.xhr.status
})
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(204, {}, 'foo!')
})
var div = make('<div></div>')
htmx.ajax('GET', '/test')
this.server.respond()
div.innerHTML.should.equal('')
status.should.equal(204)
htmx.off('htmx:beforeSwap', handler)
})
it('ajax api with can pass in custom handler and handle if it throws error', function() {
var onLoadError = false
var handler = htmx.on('htmx:onLoadError', function(event) {
onLoadError = true
})
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(204, {}, 'foo!')
})
var div = make('<div></div>')
try {
htmx.ajax('GET', '/test', { handler: function() { throw new Error('throw') } })
this.server.respond()
} catch (e) {}
div.innerHTML.should.equal('')
onLoadError.should.equal(true)
// repeat the error resonse a 2nd time to make sure request lock removed after error
onLoadError = false
try {
htmx.ajax('GET', '/test', { handler: function() { throw new Error('throw') } })
this.server.respond()
} catch (e) {}
div.innerHTML.should.equal('')
onLoadError.should.equal(true)
htmx.off('htmx:onLoadError', handler)
})
it('process api can process non elements fine', function() {
var div = make('<div>textNode</div>')
htmx.process(div.firstChild)
})
it('ajax api push Url should push an element into the cache when true', function() {
this.server.respondWith('POST', '/test123', 'Clicked!')
var div = make("<div id='d1'></div>")
htmx.ajax('POST', '/test123', {
target: '#d1',
swap: 'innerHTML',
push: 'true'
})
this.server.respond()
div.innerHTML.should.equal('Clicked!')
var path = sessionStorage.getItem('htmx-current-path-for-history')
path.should.equal('/test123')
})
it('ajax api push Url should push an element into the cache when string', function() {
this.server.respondWith('POST', '/test', 'Clicked!')
var div = make("<div id='d1'></div>")
htmx.ajax('POST', '/test', {
target: '#d1',
swap: 'innerHTML',
push: '/abc123'
})
this.server.respond()
div.innerHTML.should.equal('Clicked!')
var path = sessionStorage.getItem('htmx-current-path-for-history')
path.should.equal('/abc123')
})
it('ajax api replace Url should replace an element into the cache when true', function() {
this.server.respondWith('POST', '/test123', 'Clicked!')
var div = make("<div id='d1'></div>")
htmx.ajax('POST', '/test123', {
target: '#d1',
swap: 'innerHTML',
replace: 'true'
})
this.server.respond()
div.innerHTML.should.equal('Clicked!')
var path = sessionStorage.getItem('htmx-current-path-for-history')
path.should.equal('/test123')
})
it('ajax api replace Url should replace an element into the cache when string', function() {
this.server.respondWith('POST', '/test', 'Clicked!')
var div = make("<div id='d1'></div>")
htmx.ajax('POST', '/test', {
target: '#d1',
swap: 'innerHTML',
replace: '/abc123'
})
this.server.respond()
div.innerHTML.should.equal('Clicked!')
var path = sessionStorage.getItem('htmx-current-path-for-history')
path.should.equal('/abc123')
})
})

View File

@@ -90,6 +90,32 @@ describe('htmx config test', function() {
}
})
it('non mapped responseHandling config will not swap', function() {
var originalResponseHandling = htmx.config.responseHandling
try {
htmx.config.responseHandling = [{ code: '200', swap: true }]
var responseCode = null
this.server.respondWith('GET', '/test', function(xhr, id) {
xhr.respond(responseCode, { 'Content-Type': 'text/html' }, '' + responseCode)
})
responseCode = 400 // 400 should not swap as not found in config
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Click Me!')
responseCode = 200 // 200 should cause a swap by default
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('200')
} finally {
htmx.config.responseHandling = originalResponseHandling
}
})
it('can change the target of a given response code', function() {
var originalResponseHandling = htmx.config.responseHandling
try {
@@ -113,6 +139,58 @@ describe('htmx config test', function() {
}
})
it('throws targetError if you the target in responseHandling is invalid', function() {
var originalResponseHandling = htmx.config.responseHandling
try {
var error = false
var handler = htmx.on('htmx:targetError', function(evt) {
evt.detail.target.should.equal('#a-div')
error = true
})
htmx.config.responseHandling = originalResponseHandling.slice()
htmx.config.responseHandling.unshift({ code: '444', swap: true, target: '#a-div' })
var responseCode = null
this.server.respondWith('GET', '/test', function(xhr, id) {
xhr.respond(responseCode, { 'Content-Type': 'text/html' }, '' + responseCode)
})
responseCode = 444
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Click Me!')
} catch (e) {
} finally {
htmx.config.responseHandling = originalResponseHandling
htmx.off('htmx:targetError', handler)
error.should.equal(true)
}
})
it('can change the target to "this" in a given response code', function() {
var originalResponseHandling = htmx.config.responseHandling
try {
htmx.config.responseHandling = originalResponseHandling.slice()
htmx.config.responseHandling.unshift({ code: '444', swap: true, target: 'this' })
var responseCode = null
this.server.respondWith('GET', '/test', function(xhr, id) {
xhr.respond(responseCode, { 'Content-Type': 'text/html' }, '' + responseCode)
})
responseCode = 444
var div = make('<div id="a-div">Another Div</div>')
var btn = make('<button hx-target="#a-div" hx-get="/test">Click Me!</button>')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('444')
div.innerHTML.should.equal('Another Div')
} finally {
htmx.config.responseHandling = originalResponseHandling
}
})
it('can change the swap type of a given response code', function() {
var originalResponseHandling = htmx.config.responseHandling
try {

View File

@@ -1,4 +1,5 @@
describe('Core htmx Events', function() {
var HTMX_HISTORY_CACHE_NAME = 'htmx-history-cache'
beforeEach(function() {
this.server = makeServer()
clearWorkArea()
@@ -77,6 +78,21 @@ describe('Core htmx Events', function() {
}
})
it('events accept an options argument and the result works as expected', function() {
var invoked = 0
var handler = htmx.on('custom', function() {
invoked = invoked + 1
}, { once: true })
try {
var div = make("<div hx-post='/test'></div>")
htmx.trigger(div, 'custom')
htmx.trigger(div, 'custom')
invoked.should.equal(1)
} finally {
htmx.off('custom', handler)
}
})
it('htmx:configRequest allows attribute removal', function() {
var param = 'foo'
var handler = htmx.on('htmx:configRequest', function(evt) {
@@ -163,6 +179,24 @@ describe('Core htmx Events', function() {
}
})
it('htmx:afterSwap is called when replacing outerHTML, new line content', function() {
var called = false
var handler = htmx.on('htmx:afterSwap', function(evt) {
called = true
})
try {
this.server.respondWith('POST', '/test', function(xhr) {
xhr.respond(200, {}, '\n<button>Bar</button>')
})
var div = make("<button hx-post='/test' hx-swap='outerHTML'>Foo</button>")
div.click()
this.server.respond()
should.equal(called, true)
} finally {
htmx.off('htmx:afterSwap', handler)
}
})
it('htmx:oobBeforeSwap is called before swap', function() {
var called = false
var handler = htmx.on('htmx:oobBeforeSwap', function(evt) {
@@ -681,4 +715,206 @@ describe('Core htmx Events', function() {
htmx.off('htmx:afterSwap', afterSwapHandler)
}
})
it('htmx:beforeSwap can override swap style using evt.detail.swapOverride and has final say on it', function() {
var swapWasOverriden = false
var responseBody = 'look at me. im the innerHTML now.'
var beforeSwapHandler = htmx.on('htmx:beforeSwap', function(evt) {
evt.detail.swapOverride = 'innerHTML'
})
var afterSwapHandler = htmx.on('htmx:afterSwap', function(evt) {
console.log('afterSwap', byId('b').innerHTML)
swapWasOverriden = byId('b') !== null && byId('b').innerHTML === responseBody
})
try {
this.server.respondWith('GET', '/test', [200, { 'HX-Reswap': 'afterbegin' }, responseBody])
make("<div id='a' hx-get='/test' hx-target='#b' hx-swap='beforeend'></div><div id='b'> IF YOU CAN READ THIS, IT FAILED </div>")
byId('a').click()
this.server.respond()
swapWasOverriden.should.equal(true)
} finally {
htmx.off('htmx:beforeSwap', beforeSwapHandler)
htmx.off('htmx:afterSwap', afterSwapHandler)
}
})
it('preventDefault() in htmx:historyCacheMiss stops the history request', function() {
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
var handler = htmx.on('htmx:historyCacheMiss', function(evt) {
evt.preventDefault()
})
this.server.respondWith('GET', '/test1', '<div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test1</div>')
this.server.respondWith('GET', '/test2', '<div id="d3" hx-push-url="true" hx-get="/test3" hx-swap="outerHTML settle:0">test2</div>')
make('<div id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0">init</div>')
try {
byId('d1').click()
this.server.respond()
var workArea = getWorkArea()
workArea.textContent.should.equal('test1')
byId('d2').click()
this.server.respond()
workArea.textContent.should.equal('test2')
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME) // clear cache
htmx._('restoreHistory')('/test1')
this.server.respond()
getWorkArea().textContent.should.equal('test2')
} finally {
htmx.off('htmx:historyCacheMiss', handler)
}
})
it('htmx:historyCacheMissLoad event can update history swap', function() {
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
var handler = htmx.on('htmx:historyCacheMissLoad', function(evt) {
evt.detail.historyElt = byId('hist-re-target')
evt.detail.swapSpec.swapStyle = 'outerHTML'
evt.detail.response = '<div id="hist-re-target">Updated<div>'
evt.detail.path = '/test3'
})
this.server.respondWith('GET', '/test1', '<div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test1</div>')
this.server.respondWith('GET', '/test2', '<div id="d3" hx-push-url="true" hx-get="/test3" hx-swap="outerHTML settle:0">test2</div>')
make('<div id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0">init</div>')
make('<div id="hist-re-target"></div>')
try {
byId('d1').click()
this.server.respond()
var workArea = getWorkArea()
workArea.textContent.should.equal('test1')
byId('d2').click()
this.server.respond()
workArea.textContent.should.equal('test2')
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME) // clear cache
htmx._('restoreHistory')('/test1')
this.server.respond()
getWorkArea().textContent.should.equal('test2Updated')
byId('hist-re-target').textContent.should.equal('Updated')
htmx._('currentPathForHistory').should.equal('/test3')
} finally {
htmx.off('htmx:historyCacheMissLoad', handler)
}
})
it('htmx:historyCacheMiss event can set custom request headers', function() {
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
var handler = htmx.on('htmx:historyCacheMiss', function(evt) {
evt.detail.xhr.setRequestHeader('CustomHeader', 'true')
})
this.server.respondWith('GET', '/test1', function(xhr) {
should.equal(xhr.requestHeaders.CustomHeader, 'true')
xhr.respond(200, {}, '<div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test1</div>')
})
make('<div id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0">init</div>')
try {
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME) // clear cache
htmx._('restoreHistory')('/test1')
this.server.respond()
getWorkArea().textContent.should.equal('test1')
} finally {
htmx.off('htmx:historyCacheMiss', handler)
}
})
it('preventDefault() in htmx:historyCacheHit stops the history action', function() {
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
var handler = htmx.on('htmx:historyCacheHit', function(evt) {
evt.preventDefault()
})
this.server.respondWith('GET', '/test1', '<div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test1</div>')
this.server.respondWith('GET', '/test2', '<div id="d3" hx-push-url="true" hx-get="/test3" hx-swap="outerHTML settle:0">test2</div>')
make('<div id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0">init</div>')
try {
byId('d1').click()
this.server.respond()
var workArea = getWorkArea()
workArea.textContent.should.equal('test1')
byId('d2').click()
this.server.respond()
workArea.textContent.should.equal('test2')
htmx._('restoreHistory')('/test1')
getWorkArea().textContent.should.equal('test2')
} finally {
htmx.off('htmx:historyCacheHit', handler)
}
})
it('htmx:historyCacheHit event can update history swap', function() {
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
var handler = htmx.on('htmx:historyCacheHit', function(evt) {
evt.detail.historyElt = byId('hist-re-target')
evt.detail.swapSpec.swapStyle = 'outerHTML'
evt.detail.item.content = '<div id="hist-re-target">Updated<div>'
evt.detail.path = '/test3'
})
this.server.respondWith('GET', '/test1', '<div id="d2" hx-push-url="true" hx-get="/test2" hx-swap="outerHTML settle:0">test1</div>')
this.server.respondWith('GET', '/test2', '<div id="d3" hx-push-url="true" hx-get="/test3" hx-swap="outerHTML settle:0">test2</div>')
make('<div id="d1" hx-push-url="true" hx-get="/test1" hx-swap="outerHTML settle:0">init</div>')
make('<div id="hist-re-target"></div>')
try {
byId('d1').click()
this.server.respond()
var workArea = getWorkArea()
workArea.textContent.should.equal('test1')
byId('d2').click()
this.server.respond()
workArea.textContent.should.equal('test2')
htmx._('restoreHistory')('/test1')
this.server.respond()
getWorkArea().textContent.should.equal('test2Updated')
byId('hist-re-target').textContent.should.equal('Updated')
htmx._('currentPathForHistory').should.equal('/test3')
} finally {
htmx.off('htmx:historyCacheHit', handler)
}
})
it('htmx:targetError should include the hx-target value', function() {
var target = null
var handler = htmx.on('htmx:targetError', function(evt) {
target = evt.detail.target
})
try {
this.server.respondWith('GET', '/test', '')
var div = make('<div hx-post="/test" hx-target="#non-existent"></div>')
div.click()
this.server.respond()
target.should.equal('#non-existent')
} finally {
htmx.off('htmx:targetError', handler)
}
})
it('htmx:targetError can include an inherited hx-target value', function() {
var target = null
var handler = htmx.on('htmx:targetError', function(evt) {
target = evt.detail.target
})
try {
this.server.respondWith('GET', '/test', '')
make('<div hx-target="#parent-target"><div id="child" hx-post="/test"></div></div>')
byId('child').click()
this.server.respond()
target.should.equal('#parent-target')
} finally {
htmx.off('htmx:targetError', handler)
}
})
})

View File

@@ -39,4 +39,49 @@ describe('Core htmx extension tests', function() {
this.server.respond()
div.innerHTML.should.equal('Click Me!')
})
it('withExtensions catches and logs any exceptions', function() {
htmx.defineExtension('ext-prevent-request', {
onEvent: function(name, evt) {
if (name === 'htmx:beforeRequest') {
evt.preventDefault()
}
}
})
var div = make('<div hx-ext="ext-prevent-request">Foo</div>')
htmx._('withExtensions')(div, function(extension) {
throw new Error('throw error to catch and log')
})
})
it('encodeParameters works as expected', function() {
htmx.defineExtension('enc-param', {
encodeParameters: function(xhr, parameters, elt) {
return 'foo=bar'
}
})
var values
this.server.respondWith('Post', '/test', function(xhr) {
values = getParameters(xhr)
xhr.respond(200, {}, 'clicked!')
})
this.server.respondWith('GET', '/test', 'clicked!')
var div = make('<div hx-post="/test" hx-ext="enc-param">Click Me!</div>')
div.click()
this.server.respond()
div.innerHTML.should.equal('clicked!')
values.foo.should.equal('bar')
})
it('extensionBase return expected values', function() {
var extBase = htmx._('extensionBase')()
should.equal(extBase.init(), null)
should.equal(extBase.getSelectors(), null)
should.equal(extBase.onEvent(), true)
should.equal(extBase.transformResponse('text'), 'text')
should.equal(extBase.isInlineSwap(), false)
should.equal(extBase.handleSwap(), false)
should.equal(extBase.encodeParameters(), null)
})
})

View File

@@ -1,9 +1,11 @@
describe('Core htmx AJAX headers', function() {
const chai = window.chai
beforeEach(function() {
this.server = makeServer()
clearWorkArea()
})
afterEach(function() {
this.server.restore()
clearWorkArea()
@@ -131,7 +133,7 @@ describe('Core htmx AJAX headers', function() {
invokedEvent.should.equal(true)
})
it('should handle JSON with array arg HX-Trigger response header properly', function() {
it('should handle JSON with object arg HX-Trigger response header properly', function() {
this.server.respondWith('GET', '/test', [200, { 'HX-Trigger': '{"foo":{"a":1, "b":2}}' }, ''])
var div = make('<div hx-get="/test"></div>')
@@ -147,6 +149,21 @@ describe('Core htmx AJAX headers', function() {
invokedEvent.should.equal(true)
})
it('should handle JSON with target array arg HX-Trigger response header properly', function() {
this.server.respondWith('GET', '/test', [200, { 'HX-Trigger': '{"foo":{"target":"#testdiv"}}' }, ''])
var div = make('<div hx-get="/test"></div>')
var testdiv = make('<div id="testdiv"></div>')
var invokedEvent = false
testdiv.addEventListener('foo', function(evt) {
invokedEvent = true
evt.detail.elt.should.equal(testdiv)
})
div.click()
this.server.respond()
invokedEvent.should.equal(true)
})
it('should survive malformed JSON in HX-Trigger response header', function() {
this.server.respondWith('GET', '/test', [200, { 'HX-Trigger': '{not: valid}' }, ''])
@@ -252,6 +269,37 @@ describe('Core htmx AJAX headers', function() {
div2.innerHTML.should.equal('Result')
})
it('should handle HX-Retarget override back to this', function() {
this.server.respondWith('GET', '/test', [200, { 'HX-Retarget': 'this' }, 'Result'])
var div1 = make('<div id="d1" hx-get="/test" hx-target="#d2"></div>')
var div2 = make('<div id="d2"></div>')
div1.click()
this.server.respond()
div1.innerHTML.should.equal('Result')
div2.innerHTML.should.equal('')
})
it('should handle report target:error when HX-Retarget invalid', function() {
try {
var error = false
var handler = htmx.on('htmx:targetError', function(evt) {
evt.detail.target.should.equal('#d2')
error = true
})
this.server.respondWith('GET', '/test', [200, { 'HX-Retarget': '#d2' }, 'Result'])
var div1 = make('<div id="d1" hx-get="/test"></div>')
div1.click()
this.server.respond()
} catch (e) {
} finally {
htmx.off('htmx:targetError', handler)
div1.innerHTML.should.equal('')
error.should.equal(true)
}
})
it('should handle HX-Reswap', function() {
this.server.respondWith('GET', '/test', [200, { 'HX-Reswap': 'innerHTML' }, 'Result'])
@@ -271,6 +319,16 @@ describe('Core htmx AJAX headers', function() {
div.innerHTML.should.equal('<div id="d2">bar</div>')
})
it('should handle HX-Reselect unset', function() {
this.server.respondWith('GET', '/test', [200, { 'HX-Reselect': 'unset' }, 'bar'])
var div = make('<div hx-get="/test" hx-select="#d2"></div>')
div.click()
this.server.respond()
div.innerHTML.should.equal('bar')
})
it('should handle simple string HX-Trigger-After-Swap response header properly w/ outerHTML swap', function() {
this.server.respondWith('GET', '/test', [200, { 'HX-Trigger-After-Swap': 'foo' }, ''])
@@ -339,17 +397,91 @@ describe('Core htmx AJAX headers', function() {
htmx.off('bar', handlerBar)
})
it('should change body content on HX-Location', function() {
this.server.respondWith('GET', '/test', [200, { 'HX-Location': '{"path":"/test2", "target":"#testdiv"}' }, ''])
it('should change body content on HX-Location', function(done) {
this.server.respondWith('GET', '/test', [200, { 'HX-Location': '{"path":"/test2", "target":"#work-area"}' }, ''])
this.server.respondWith('GET', '/test2', [200, {}, '<div>Yay! Welcome</div>'])
var div = make('<div id="testdiv" hx-trigger="click" hx-get="/test"></div>')
div.click()
this.server.respond()
this.server.respond()
div.innerHTML.should.equal('<div>Yay! Welcome</div>')
setTimeout(function() {
getWorkArea().innerHTML.should.equal('<div>Yay! Welcome</div>')
done()
}, 30)
})
it('request to restore history should include the HX-Request header', function() {
it('should push new Url on HX-Location', function(done) {
sessionStorage.removeItem('htmx-current-path-for-history')
this.server.respondWith('GET', '/test', [200, { 'HX-Location': '{"path":"/test2", "target":"#work-area"}' }, ''])
this.server.respondWith('GET', '/test2', [200, {}, '<div>Yay! Welcome</div>'])
var div = make('<div id="testdiv" hx-trigger="click" hx-get="/test"></div>')
div.click()
this.server.respond()
this.server.respond()
setTimeout(function() {
getWorkArea().innerHTML.should.equal('<div>Yay! Welcome</div>')
var path = sessionStorage.getItem('htmx-current-path-for-history')
path.should.equal('/test2')
done()
}, 30)
})
it('should not push new Url on HX-Location if push Url false', function(done) {
sessionStorage.setItem('htmx-current-path-for-history', '/old')
this.server.respondWith('GET', '/test', [200, { 'HX-Location': '{"push":"false", "path":"/test2", "target":"#work-area"}' }, ''])
this.server.respondWith('GET', '/test2', [200, {}, '<div>Yay! Welcome</div>'])
var div = make('<div id="testdiv" hx-trigger="click" hx-get="/test"></div>')
div.click()
this.server.respond()
this.server.respond()
setTimeout(function() {
getWorkArea().innerHTML.should.equal('<div>Yay! Welcome</div>')
var path = sessionStorage.getItem('htmx-current-path-for-history')
path.should.equal('/old')
done()
}, 30)
})
it('should push different Url on HX-Location if push Url is string', function(done) {
sessionStorage.removeItem('htmx-current-path-for-history')
var HTMX_HISTORY_CACHE_NAME = 'htmx-history-cache'
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
this.server.respondWith('GET', '/test', [200, { 'HX-Location': '{"push":"/abc123", "path":"/test2", "target":"#work-area"}' }, ''])
this.server.respondWith('GET', '/test2', [200, {}, '<div>Yay! Welcome</div>'])
var div = make('<div id="testdiv" hx-trigger="click" hx-get="/test"></div>')
div.click()
this.server.respond()
this.server.respond()
setTimeout(function() {
getWorkArea().innerHTML.should.equal('<div>Yay! Welcome</div>')
var path = sessionStorage.getItem('htmx-current-path-for-history')
path.should.equal('/abc123')
done()
}, 30)
})
it('should refresh page on HX-Refresh', function() {
var refresh = false
htmx.location = { reload: function() { refresh = true } }
this.server.respondWith('GET', '/test', [200, { 'HX-Refresh': 'true' }, ''])
var div = make('<div id="testdiv" hx-trigger="click" hx-get="/test"></div>')
div.click()
this.server.respond()
refresh.should.equal(true)
htmx.location = window.location
})
it('should update location.href on HX-Redirect', function() {
htmx.location = { href: window.location.href }
this.server.respondWith('GET', '/test', [200, { 'HX-Redirect': 'https://htmx.org/headers/hx-redirect/' }, ''])
var div = make('<div id="testdiv" hx-trigger="click" hx-get="/test"></div>')
div.click()
this.server.respond()
htmx.location.href.should.equal('https://htmx.org/headers/hx-redirect/')
htmx.location = window.location
})
it('request to restore history should include the HX-Request header when historyRestoreAsHxRequest true', function() {
this.server.respondWith('GET', '/test', function(xhr) {
xhr.requestHeaders['HX-Request'].should.be.equal('true')
xhr.respond(200, {}, '')
@@ -358,6 +490,32 @@ describe('Core htmx AJAX headers', function() {
this.server.respond()
})
it('request to restore history should not include the HX-Request header when historyRestoreAsHxRequest false', function() {
htmx.config.historyRestoreAsHxRequest = false
this.server.respondWith('GET', '/test', function(xhr) {
should.equal(xhr.requestHeaders['HX-Request'], undefined)
xhr.respond(200, {}, '')
})
htmx._('loadHistoryFromServer')('/test')
this.server.respond()
htmx.config.historyRestoreAsHxRequest = true
})
it('request history from server with error status code throws error event', function() {
this.server.respondWith('GET', '/test', function(xhr) {
xhr.requestHeaders['HX-Request'].should.be.equal('true')
xhr.respond(404, {}, '')
})
var invokedEvent = false
var handler = htmx.on('htmx:historyCacheMissLoadError', function(evt) {
invokedEvent = true
})
htmx._('loadHistoryFromServer')('/test')
this.server.respond()
invokedEvent.should.equal(true)
htmx.off('htmx:historyCacheMissLoadError', handler)
})
it('request to restore history should include the HX-History-Restore-Request header', function() {
this.server.respondWith('GET', '/test', function(xhr) {
xhr.requestHeaders['HX-History-Restore-Request'].should.be.equal('true')

View File

@@ -87,22 +87,56 @@ describe('Core htmx internals Tests', function() {
var anchorThatShouldCancel = make("<a href='/foo'></a>")
htmx._('shouldCancel')({ type: 'click' }, anchorThatShouldCancel).should.equal(true)
var anchorThatShouldCancel = make("<a href='#'></a>")
anchorThatShouldCancel = make("<a href='#'></a>")
htmx._('shouldCancel')({ type: 'click' }, anchorThatShouldCancel).should.equal(true)
var anchorThatShouldNotCancel = make("<a href='#foo'></a>")
htmx._('shouldCancel')({ type: 'click' }, anchorThatShouldNotCancel).should.equal(false)
var divThatShouldNotCancel = make('<div></div>')
htmx._('shouldCancel')({ type: 'click' }, divThatShouldNotCancel).should.equal(false)
var form = make('<form></form>')
htmx._('shouldCancel')({ type: 'submit' }, form).should.equal(true)
htmx._('shouldCancel')({ type: 'submit', target: form }, form).should.equal(true)
var form = make("<form><input id='i1' type='submit'></form>")
var input = byId('i1')
htmx._('shouldCancel')({ type: 'click' }, input).should.equal(true)
// check that events targeting elements that shouldn't cancel don't cancel
htmx._('shouldCancel')({ type: 'click', target: divThatShouldNotCancel }, form).should.equal(false)
var form = make("<form><button id='b1' type='submit'></form>")
var button = byId('b1')
htmx._('shouldCancel')({ type: 'click' }, button).should.equal(true)
// check elements inside links getting click events should cancel parent links
var anchorWithButton = make("<a href='/foo'><button></button></a>")
htmx._('shouldCancel')({ type: 'click', target: anchorWithButton.firstChild }, anchorWithButton).should.equal(true)
htmx._('shouldCancel')({ type: 'click', target: anchorWithButton.firstChild }, anchorWithButton.firstChild).should.equal(true)
// check that links inside htmx elements should not cancel
var divWithLink = make("<div hx-get='/data'><a href='/page'>Link</a></div>")
var link = divWithLink.querySelector('a')
htmx._('shouldCancel')({ type: 'click', target: link }, divWithLink).should.equal(false)
form = make('<form id="f1">' +
'<input id="insideInput" type="submit">' +
'<button id="insideFormBtn"></button>' +
'<button id="insideSubmitBtn" type="submit"></button>' +
'<button id="insideResetBtn" type="reset"></button>' +
'<button id="insideButtonBtn" type="button"></button>' +
'</form>' +
'<input id="outsideInput" form="f1" type="submit">' +
'<button id="outsideFormBtn" form="f1"></button>' +
'<button id="outsideSubmitBtn" form="f1" type="submit"></button>")' +
'<button id="outsideButtonBtn" form="f1" type="button"></button>")' +
'<button id="outsideResetBtn" form="f1" type="reset"></button>")' +
'<button id="outsideNoFormBtn"></button>")')
htmx._('shouldCancel')({ type: 'click' }, byId('insideInput')).should.equal(true)
htmx._('shouldCancel')({ type: 'click' }, byId('insideFormBtn')).should.equal(true)
htmx._('shouldCancel')({ type: 'click' }, byId('insideSubmitBtn')).should.equal(true)
htmx._('shouldCancel')({ type: 'click' }, byId('insideResetBtn')).should.equal(false)
htmx._('shouldCancel')({ type: 'click' }, byId('insideButtonBtn')).should.equal(false)
htmx._('shouldCancel')({ type: 'click' }, byId('outsideInput')).should.equal(true)
htmx._('shouldCancel')({ type: 'click' }, byId('outsideFormBtn')).should.equal(true)
htmx._('shouldCancel')({ type: 'click' }, byId('outsideSubmitBtn')).should.equal(true)
htmx._('shouldCancel')({ type: 'click' }, byId('outsideButtonBtn')).should.equal(false)
htmx._('shouldCancel')({ type: 'click' }, byId('outsideResetBtn')).should.equal(false)
htmx._('shouldCancel')({ type: 'click' }, byId('outsideNoFormBtn')).should.equal(false)
})
it('unset properly unsets a given attribute', function() {
@@ -128,4 +162,67 @@ describe('Core htmx internals Tests', function() {
var value = htmx._('encodeParamsForBody')(null, form, {});
(value instanceof FormData).should.equal(true)
})
it('Calling onpopstate to trigger backup and restore of page triggers htmx:restored event', function() {
var restored
var handler = htmx.on('htmx:restored', function(event) {
restored = true
})
make('<div hx-get="/test" hx-trigger="restored">Not Called</div>')
window.onpopstate({ state: { htmx: true } })
restored.should.equal(true)
htmx.off('htmx:restored', handler)
})
it('scroll position is restored from history restore', function() {
make('<div style="height: 1000px;" hx-get="/test" hx-trigger="restored">Not Called</div>')
window.scrollTo(0, 50)
window.onpopstate({ state: { htmx: true } })
parseInt(window.scrollY).should.equal(50)
})
it('calling onpopstate with no htmx state not true calls original popstate', function() {
window.onpopstate({ state: { htmx: false } })
})
it('getPathFromResponse returns paths when valid', function() {
var path = htmx._('getPathFromResponse')({ responseURL: 'https://htmx.org/somepath?a=b#fragment' })
should.equal(path, '/somepath?a=b')
path = htmx._('getPathFromResponse')({ responseURL: 'notvalidurl' })
should.equal(path, undefined)
})
it('appendParam can process objects', function() {
var param = htmx._('appendParam')('a=b', 'jim', 'foo')
should.equal(param, 'a=b&jim=foo')
param = htmx._('appendParam')('a=b', 'jim', '{"foo":"bar"}')
should.equal(param, 'a=b&jim=%7B%22foo%22%3A%22bar%22%7D')
param = htmx._('appendParam')('a=b', 'jim', { foo: 'bar' })
should.equal(param, 'a=b&jim=%7B%22foo%22%3A%22bar%22%7D')
})
it('handleTitle falls back to setting document.title when no title head element', function() {
var oldTitle = window.document.title
document.querySelector('title').remove()
htmx._('handleTitle')('update title')
window.document.title.should.equal('update title')
window.document.title = oldTitle
})
it('without meta config getMetaConfig returns null', function() {
document.querySelector('meta[name="htmx-config"]').remove()
should.equal(htmx._('getMetaConfig')(), null)
})
it('internalAPI settleImmediately completes settle tasks', function() {
// settleImmediately is no longer used internally and may no longer be needed at all
// as swapping without settleing does not seem via internalAPI
const fragment = htmx._('makeFragment')('<div>Content</div>')
const historyElement = htmx._('getHistoryElement')()
const settleInfo = htmx._('makeSettleInfo')(historyElement)
htmx._('swapInnerHTML')(historyElement, fragment, settleInfo)
historyElement.firstChild.className.should.equal('htmx-added')
htmx._('settleImmediately')(settleInfo.tasks)
historyElement.firstChild.className.should.equal('')
})
})

View File

@@ -3,6 +3,7 @@ describe('Core htmx Parameter Handling', function() {
this.server = makeServer()
clearWorkArea()
})
afterEach(function() {
this.server.restore()
clearWorkArea()
@@ -134,8 +135,10 @@ describe('Core htmx Parameter Handling', function() {
vals.do.should.equal('rey')
vals.btn.should.equal('bar')
done()
})
}, { once: true })
button.focus()
// Headless / Hardly-throttled CPU might result in 'focusin' not being fired, double it just in case
htmx.trigger(button, 'focusin')
})
it('form includes last focused submit', function(done) {
@@ -149,8 +152,10 @@ describe('Core htmx Parameter Handling', function() {
vals.do.should.equal('rey')
vals.s1.should.equal('bar')
done()
})
}, { once: true })
button.focus()
// Headless / Hardly-throttled CPU might result in 'focusin' not being fired, double it just in case
htmx.trigger(button, 'focusin')
})
it('form does not include button when focus is lost', function() {
@@ -276,6 +281,22 @@ describe('Core htmx Parameter Handling', function() {
vals.foo.should.equal('bar')
})
it('formdata works with null values', function() {
var form = make('<form hx-post="/test"><input name="foo" value="bar"/></form>')
var vals = htmx._('getInputValues')(form, 'get').values
function updateToNull() { vals.foo = null }
updateToNull.should.not.throw()
vals.foo.should.equal('null')
})
it('formdata can be used to construct a URLSearchParams instance', function() {
var form = make('<input name="foo" value="bar"/>')
var vals = htmx._('getInputValues')(form, 'get').values
function makeSearchParams() { return new URLSearchParams(vals).toString() }
makeSearchParams.should.not.throw()
makeSearchParams().should.equal('foo=bar')
})
it('order of parameters follows order of input elements', function() {
this.server.respondWith('GET', '/test?foo=bar&bar=foo&foo=bar&foo2=bar2', function(xhr) {
xhr.respond(200, {}, 'Clicked!')
@@ -293,4 +314,102 @@ describe('Core htmx Parameter Handling', function() {
this.server.respond()
form.innerHTML.should.equal('Clicked!')
})
it('order of parameters follows order of input elements with POST', function() {
this.server.respondWith('POST', '/test', function(xhr) {
xhr.requestBody.should.equal('foo=bar&bar=foo&foo=bar&foo2=bar2')
xhr.respond(200, {}, 'Clicked!')
})
var form = make('<form hx-post="/test">' +
'<input name="foo" value="bar">' +
'<input name="bar" value="foo">' +
'<input name="foo" value="bar">' +
'<input name="foo2" value="bar2">' +
'<button id="b1">Click Me!</button>' +
'</form>')
byId('b1').click()
this.server.respond()
form.innerHTML.should.equal('Clicked!')
})
it('file is correctly uploaded with file input', function() {
this.server.respondWith('POST', '/test', function(xhr) {
should.equal(xhr.requestHeaders['Content-Type'], undefined)
const file = xhr.requestBody.get('file')
file.should.instanceOf(File)
file.name.should.equal('test.txt')
xhr.respond(200, {}, 'OK')
})
const form = make('<form hx-post="/test" hx-target="#result" hx-encoding="multipart/form-data">' +
'<input type="file" name="file">' +
'<button type="submit"></button>' +
'</form>')
const input = form.querySelector('input')
const file = new File(['Test'], 'test.txt', { type: 'text/plain' })
const dataTransfer = new DataTransfer()
dataTransfer.items.add(file)
input.files = dataTransfer.files
const result = make('<div id="result"></div>')
form.querySelector('button').click()
this.server.respond()
result.innerHTML.should.equal('OK')
})
it('file is not uploaded with blank filename', function() {
this.server.respondWith('POST', '/test', function(xhr) {
should.equal(xhr.requestHeaders['Content-Type'], undefined)
const file = xhr.requestBody.get('file')
should.equal(file, null)
xhr.respond(200, {}, 'OK')
})
const form = make('<form hx-post="/test" hx-target="#result" hx-encoding="multipart/form-data">' +
'<input type="file" name="file">' +
'<button type="submit"></button>' +
'</form>')
const input = form.querySelector('input')
const file = new File(['Test'], '', { type: 'text/plain' })
const dataTransfer = new DataTransfer()
dataTransfer.items.add(file)
input.files = dataTransfer.files
const result = make('<div id="result"></div>')
form.querySelector('button').click()
this.server.respond()
result.innerHTML.should.equal('OK')
})
it('file is correctly uploaded with htmx.ajax', function() {
this.server.respondWith('POST', '/test', function(xhr) {
should.equal(xhr.requestHeaders['Content-Type'], undefined)
const file = xhr.requestBody.get('file')
file.should.instanceOf(File)
file.name.should.equal('test.txt')
xhr.respond(200, {}, 'OK')
})
const div = make('<div hx-encoding="multipart/form-data"></div>')
htmx.ajax('POST', '/test', {
source: div,
values: {
file: new File(['Test'], 'test.txt', { type: 'text/plain' })
}
})
this.server.respond()
div.innerHTML.should.equal('OK')
})
})

View File

@@ -5,12 +5,12 @@ describe('Core htmx perf Tests', function() {
beforeEach(function() {
this.server = makeServer()
clearWorkArea()
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
})
afterEach(function() {
this.server.restore()
clearWorkArea()
localStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
sessionStorage.removeItem(HTMX_HISTORY_CACHE_NAME)
})
function stringRepeat(str, num) {
@@ -39,8 +39,8 @@ describe('Core htmx perf Tests', function() {
}
var start = performance.now()
var string = JSON.stringify(array)
localStorage.setItem(HTMX_HISTORY_CACHE_NAME, string)
var reReadString = localStorage.getItem(HTMX_HISTORY_CACHE_NAME)
sessionStorage.setItem(HTMX_HISTORY_CACHE_NAME, string)
var reReadString = sessionStorage.getItem(HTMX_HISTORY_CACHE_NAME)
var finalJson = JSON.parse(reReadString)
var end = performance.now()
var timeInMs = end - start
@@ -57,6 +57,6 @@ describe('Core htmx perf Tests', function() {
htmx._('cleanInnerHtmlForHistory')(workArea)
var end = performance.now()
var timeInMs = end - start
chai.assert(timeInMs < 50, 'Should take less than 50ms on most platforms')
chai.assert(timeInMs < 80, 'Should take less than 80ms on most platforms')
})
})

View File

@@ -100,11 +100,11 @@ describe('Core htmx Regression Tests', function() {
it('does not submit with a false condition on a form', function() {
this.server.respondWith('POST', '/test', 'Submitted')
var defaultPrevented = false
htmx.on('click', function(evt) {
htmx.on('submit', function(evt) {
defaultPrevented = evt.defaultPrevented
})
var form = make('<form hx-post="/test" hx-trigger="click[false]"></form>')
form.click()
var form = make('<form hx-post="/test" hx-trigger="submit[false]"><button id="b1">submit</button></form>')
byId('b1').click()
this.server.respond()
defaultPrevented.should.equal(true)
})
@@ -270,4 +270,216 @@ describe('Core htmx Regression Tests', function() {
done()
}, 50)
})
it('a modified click trigger on a form does not prevent the default behaviour of other elements - https://github.com/bigskysoftware/htmx/issues/2755', function(done) {
var defaultPrevented = 'unset'
make('<input type="date" id="datefield">')
make('<form hx-trigger="click from:body"></form>')
htmx.on('#datefield', 'click', function(evt) {
// we need to wait so the state of the evt is finalized
setTimeout(() => {
defaultPrevented = evt.defaultPrevented
try {
defaultPrevented.should.equal(false)
done()
} catch (err) {
done(err)
}
}, 0)
})
byId('datefield').click()
})
it('swap=outerHTML clears htmx-swapping class when old node has a style attribute and no class', function(done) {
this.server.respondWith('GET', '/test', '<div id="test-div">Test</div>')
var btn = make('<button hx-get="/test" hx-target="#test-div" hx-swap="outerHTML">Click Me!</button>')
var div = make('<div id="test-div" style></div>')
btn.click()
this.server.respond()
var div = byId('test-div')
const isSwappingClassStillThere = div.classList.contains('htmx-swapping')
isSwappingClassStillThere.should.equal(false)
done()
})
it('swap=outerHTML won\'t carry over user-defined classes when old node has a style attribute before the class attribute', function(done) {
this.server.respondWith('GET', '/test', '<div id="test-div">Test</div>')
var btn = make('<button hx-get="/test" hx-target="#test-div" hx-swap="outerHTML">Click Me!</button>')
var div = make('<div id="test-div" style class="my-class"></div>')
btn.click()
this.server.respond()
var div = byId('test-div')
div.classList.length.should.equal(0)
done()
})
it('a button clicked inside an htmx enabled link will prevent the link from navigating on click', function(done) {
var defaultPrevented = 'unset'
var link = make('<a href="/foo" hx-get="/foo"><button>test</button></a>')
var button = link.firstChild
htmx.on(link, 'click', function(evt) {
// we need to wait so the state of the evt is finalized
setTimeout(() => {
defaultPrevented = evt.defaultPrevented
try {
defaultPrevented.should.equal(true)
done()
} catch (err) {
done(err)
}
}, 0)
})
button.click()
})
it('a htmx enabled button clicked inside a link will prevent the link from navigating on click', function(done) {
var defaultPrevented = 'unset'
var link = make('<a href="/foo"><button hx-get="/foo">test</button></a>')
var button = link.firstChild
htmx.on(link, 'click', function(evt) {
// we need to wait so the state of the evt is finalized
setTimeout(() => {
defaultPrevented = evt.defaultPrevented
try {
defaultPrevented.should.equal(true)
done()
} catch (err) {
done(err)
}
}, 0)
})
button.click()
})
it('a htmx enabled button containing sub elements will prevent the button submitting a form', function(done) {
var defaultPrevented = 'unset'
var form = make('<form><button hx-get="/foo"><span>test</span></button></form>')
var button = form.firstChild
var span = button.firstChild
htmx.on(button, 'click', function(evt) {
// we need to wait so the state of the evt is finalized
setTimeout(() => {
defaultPrevented = evt.defaultPrevented
try {
defaultPrevented.should.equal(true)
done()
} catch (err) {
done(err)
}
}, 0)
})
span.click()
})
it('a htmx enabled element inside a form button will prevent the button submitting a form', function(done) {
var defaultPrevented = 'unset'
var form = make('<form><button><span hx-get="/foo">test</span></button></form>')
var button = form.firstChild
var span = button.firstChild
htmx.on(button, 'click', function(evt) {
// we need to wait so the state of the evt is finalized
setTimeout(() => {
defaultPrevented = evt.defaultPrevented
try {
defaultPrevented.should.equal(true)
done()
} catch (err) {
done(err)
}
}, 0)
})
span.click()
})
it('from: trigger on form prevents default form submission', function(done) {
var defaultPrevented = 'unset'
var form = make('<form id="test-form" action="/submit"><input type="submit" value="Submit"></form>')
var div = make('<div hx-post="/test" hx-trigger="submit from:#test-form"></div>')
var submitBtn = form.firstChild
htmx.on(form, 'submit', function(evt) {
defaultPrevented = evt.defaultPrevented // Capture state before our preventDefault
evt.preventDefault() // Prevent navigation in case test fails
setTimeout(() => {
try {
defaultPrevented.should.equal(true)
done()
} catch (err) {
done(err)
}
}, 0)
})
submitBtn.click()
})
it('from: trigger on button prevents default form submission', function(done) {
var defaultPrevented = 'unset'
var form = make('<form><button id="test-btn" type="submit">Submit</button></form>')
var div = make('<div hx-post="/test" hx-trigger="click from:#test-btn"></div>')
var button = byId('test-btn')
htmx.on(button, 'click', function(evt) {
defaultPrevented = evt.defaultPrevented // Capture state before our preventDefault
evt.preventDefault() // Prevent form submission in case test fails
setTimeout(() => {
try {
defaultPrevented.should.equal(true)
done()
} catch (err) {
done(err)
}
}, 0)
})
button.click()
})
it('from: trigger on link prevents default navigation', function(done) {
var defaultPrevented = 'unset'
var link = make('<a id="test-link" href="/page">Go to page</a>')
var div = make('<div hx-get="/test" hx-trigger="click from:#test-link"></div>')
htmx.on(link, 'click', function(evt) {
defaultPrevented = evt.defaultPrevented // Capture state before our preventDefault
evt.preventDefault() // Prevent navigation in case test fails
setTimeout(() => {
try {
defaultPrevented.should.equal(true)
done()
} catch (err) {
done(err)
}
}, 0)
})
link.click()
})
it('check deleting button during click does not trigger exception error in getRelatedFormData when button can no longer find form', function() {
var defaultPrevented = 'unset'
var form = make('<form><button>delete</button></form>')
var button = form.firstChild
htmx.on(button, 'click', function(evt) {
evt.target.remove()
})
button.click()
})
})

View File

@@ -27,6 +27,16 @@ describe('security options', function() {
btn.innerHTML.should.equal('Initial')
})
it('can disable a child elt', function() {
this.server.respondWith('GET', '/test', 'Clicked!')
var div = make('<div><button id="b1" hx-disable hx-get="/test">Initial</button></div>')
var btn = byId('b1')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Initial')
})
it('can disable a single elt dynamically', function() {
this.server.respondWith('GET', '/test', 'Clicked!')
@@ -65,6 +75,28 @@ describe('security options', function() {
btn.innerHTML.should.equal('Clicked a second time')
})
it('can disable a single a tag dynamically & enable it back with boost', function() {
this.server.respondWith('GET', '/test', 'Clicked!')
var btn = make('<a id="b1" hx-boost="true" href="/test" hx-target="this">Initial</button>')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Clicked!')
this.server.respondWith('GET', '/test', 'Clicked a second time')
btn.setAttribute('hx-disable', '')
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Clicked!')
btn.removeAttribute('hx-disable')
htmx.process(btn)
btn.click()
this.server.respond()
btn.innerHTML.should.equal('Clicked a second time')
})
it('can disable a single parent elt dynamically', function() {
this.server.respondWith('GET', '/test', 'Clicked!')
@@ -120,7 +152,67 @@ describe('security options', function() {
btn.click()
})
it("can't make egress cross site requests when htmx.config.selfRequestsOnly is enabled", function(done) {
it('can make a real local data uri request when selfRequestOnly false', function(done) {
htmx.config.selfRequestsOnly = false
var pathVerifier = htmx.on('htmx:validateUrl', function(evt) {
if (evt.detail.sameHost === false && evt.detail.url.protocol !== 'data:') {
evt.preventDefault()
}
})
this.server.restore() // use real xhrs
var btn = make('<button hx-get="data:,foo">Initial</button>')
btn.click()
htmx.config.selfRequestsOnly = true
setTimeout(function() {
htmx.off('htmx:validateUrl', pathVerifier)
btn.innerHTML.should.equal('foo')
done()
}, 30)
})
it('can disable hx-on on a single elt', function() {
var btn = make("<button hx-disable hx-on:click='window.foo = true'>Foo</button>")
btn.click()
should.equal(window.foo, undefined)
delete window.foo
})
it('can disable hx-on on a parent elt', function() {
var div = make("<div hx-disable><button id='b1' hx-on:click='window.foo = true'>Foo</button></div>")
var btn = byId('b1')
btn.click()
should.equal(window.foo, undefined)
delete window.foo
})
it('can disable hx-on on a single elt dynamically', function() {
var btn = make("<button hx-on:click='window.foo = true'>Foo</button>")
btn.click()
should.equal(window.foo, true)
delete window.foo
btn.setAttribute('hx-disable', '')
btn.click()
should.equal(window.foo, undefined)
delete window.foo
})
it('can disable hx-on on a parent elt dynamically', function() {
var div = make("<div><button id='b1' hx-on:click='window.foo = true'>Foo</button></div>")
var btn = byId('b1')
btn.click()
should.equal(window.foo, true)
delete window.foo
div.setAttribute('hx-disable', '')
btn.click()
should.equal(window.foo, undefined)
delete window.foo
})
it("can't make egress cross site requests when htmx.config.selfRequestsOnly is true", function(done) {
this.timeout(4000)
// should trigger send error, rather than reject
var listener = htmx.on('htmx:invalidPath', function() {
@@ -135,31 +227,34 @@ describe('security options', function() {
it('can cancel egress request based on htmx:validateUrl event', function(done) {
this.timeout(4000)
htmx.config.selfRequestsOnly = false
// should trigger send error, rather than reject
var pathVerifier = htmx.on('htmx:validateUrl', function(evt) {
evt.preventDefault()
htmx.off('htmx:validateUrl', pathVerifier)
})
var listener = htmx.on('htmx:invalidPath', function() {
htmx.off('htmx:invalidPath', listener)
htmx.off('htmx:validateUrl', pathVerifier)
done()
})
this.server.restore() // use real xhrs
// will 404, but should respond
var btn = make('<button hx-get="https://hypermedia.systems/www/test">Initial</button>')
btn.click()
htmx.config.selfRequestsOnly = true
})
it('can cancel egress request based on htmx:validateUrl event, sameHost is false', function(done) {
this.timeout(4000)
htmx.config.selfRequestsOnly = false
// should trigger send error, rather than reject
var pathVerifier = htmx.on('htmx:validateUrl', function(evt) {
if (evt.detail.sameHost === false) {
evt.preventDefault()
}
htmx.off('htmx:validateUrl', pathVerifier)
})
var listener = htmx.on('htmx:invalidPath', function() {
htmx.off('htmx:validateUrl', pathVerifier)
htmx.off('htmx:invalidPath', listener)
done()
})
@@ -167,6 +262,31 @@ describe('security options', function() {
// will 404, but should respond
var btn = make('<button hx-get="https://hypermedia.systems/www/test">Initial</button>')
btn.click()
htmx.config.selfRequestsOnly = true
})
it('can cancel egress request based on htmx:validateUrl event and then allow a request', function(done) {
htmx.config.selfRequestsOnly = false
var requestCount = 0
var pathVerifier = htmx.on('htmx:validateUrl', function(evt) {
requestCount = requestCount + 1
if (requestCount === 1) {
evt.preventDefault()
}
})
this.server.restore() // use real xhrs
var btn = make('<button hx-get="data:,foo">Initial</button>')
btn.click()
setTimeout(function() {
btn.innerHTML.should.not.equal('foo')
btn.click()
setTimeout(function() {
htmx.off('htmx:validateUrl', pathVerifier)
htmx.config.selfRequestsOnly = true
btn.innerHTML.should.equal('foo')
done()
}, 30)
}, 30)
})
it('can disable script tag support with htmx.config.allowScriptTags', function() {

Some files were not shown because too many files have changed in this diff Show More