bigskysoftware / htmx Goto Github PK
View Code? Open in Web Editor NEW</> htmx - high power tools for HTML
Home Page: https://htmx.org
License: Other
</> htmx - high power tools for HTML
Home Page: https://htmx.org
License: Other
Morphdom is a mature library for DOM swapping:
https://github.com/patrick-steele-idem/morphdom
One of the differences between htmx/intercooler and unpoly is that unpoly by default assumes that the backend is serving full pages in responses rather than page fragments. As I am using htmx to enhance a site that should otherwise be perfectly functional without JS, the backend always serves full pages and never partial templates.
Additionally, I'm not sure what your typical sites look like, but I find that I almost never want to swap the element that is being clicked but rather some other element (this is also made easier by the fact that the entire page is served up by the backend, so things like buttons that have text that needs to be changed depending on the context are usually swapped out altogether with the element that is being affected by simply swapping a parent of the button and the results). As a result, all my htmx usages end up extremely verbose and redundant (especially because of #23 and #24), e.g. to simply swap a single element when a checkbox is altered:
<label><input type="checkbox" name="hideIncomplete" hx-get="/Registrations" hx-push="true" hx-target=".registrations" hx-swap="outerHTML" hx-select=".registrations" /> Hide incomplete registrations</label>
I wonder if there's a configuration option that could be added or else an alternate syntax that could be used that would bundle all this in one to reduce the boilerplate (and chance for error).
The documentation for hx-push-url
starts with
If you want a given element to push its request URL into the browser navigation bar
and this is both the expected behavior and semantically correct when the intended result of the operation is that after the htmx trigger completes, the contents of the current page will be diffed/swapped/manipulated to match the contents of the requested url, but without actually triggering a page load (e.g. purely progressive enhancement).
However, it seems that in reality it is the value of hx-verb
that gets pushed to the history/navbar and not the url of the request, as demonstrated by the following:
index.html:
<script src="https://unpkg.com/[email protected]/dist/htmx.min.js"></script>
<div>
This text is not in the div.
</div>
<div id="target">
click me
</div>
<input type="checkbox" name="param1" value="foo" hx-get="./contents.html" hx-push-url="true" hx-target="#target" hx-swap="innerHTML" hx-select=".swap" />
contents.html:
<div class="swap">this file contains only the contents of the div</div>
This (correctly) triggers an XHR to /contents.html?param1=foo
but the location pushed to the history API is /contents.html
rather than the true request URL.
I would assume that <input type="checkbox" hx-get name="foo" />
would perform a GET against the current page (like a form without an action), and if not, then <input type="checkbox" hx-get="." name="foo" />
would.
In reality, hx-get
without a value does nothing and hx-get="."
makes a request to /
rather than the current page. The only way I could get the above to toggle between /Registrations?foo=on
and /Registrations
was to hard-code hx-get="/Registrations"
, which is unfortunate.
Someone mentioned morphdom in the chat:
https://github.com/patrick-steele-idem/morphdom
Looks like a cool project and would be neat to make kutty swapping pluggable so you could do something like:
kutty.registerSwap("morphdom", {swap:function(target, content){morphdom(target, elt)}}
And then use it like so:
<div kt-get="/example" kt-swap="morphdom">This is replaced w/ Morphdom</div>
I've just tried to import htmx into our build (webpack), and while it builds fine, in the browser I get the following JS error:
Uncaught TypeError: Cannot read property 'logger' of undefined
It's entirely possible that the JS is being mangled by webpack, perhaps the minification routine, but I'm not importing it any differently than any other package (flickity, alpine, etc).
Any tips would be appreciated!
What do you think about it?
https://github.com/bigskysoftware/htmx/blob/master/TODO.md
I am trying a simple kutty test for oob. The rendered html / dom does not come out correctly with more then one oob. You can view the test page here: http://kutty.nwcomp.com/ - If you click the heading the content is retrieved (see response) and the first oob swap works fine the second one does not.
When using hx-post
I see in the console a POST request to the value, quickly followed by a GET request to the value, the result of which is returned. Is it possible to handle errors that are returned in the POST response? All I see in the console is a 302 Found
status and an empty body, even though my controller is returning a 200
with a body. Any insights into how this works would be much appreciated.
I would like the input box be focused while doing inline validation with keyup trigger.
For example:
<input
class="input is-danger"
name="name"
type="text"
value="Invalid value"
hx-trigger="keyup changed delay:300ms"
hx-target="this"
hx-swap="outerHTML"
hx-post="/validate"
hx-params="name">
This works and the dom gets swapped if the user has not entered anything in 300ms. But once its swapped the input focus is lost, so the user cant keep editing to correct the mistake.
I've tried using the event system, but it seems like some of the event listeners are not getting registered.
htmx.on("afterSettle.htmx", function(e) {
console.log("This did not print");
});
However load.htmx
did work.
Repro:
That just causes another "Signup Form" header to appear.
In some places the htmx documentation claims to support other HTTP methods, but in fact only GET and POST are used. If an htmx client page attempts to use a different method (e.g. PUT or DELETE), htmx uses the POST method instead, and the actually desired method is sent in an HTTP header called X-HTTP-Method-Override
.
It appears that the use of the header is designed as a 'hack' or workaround for a situation in which htmx is a front-end on a back-end service which uses one of these HTTP methods, but which is deployed behind a firewall which blocks those methods. The workaround allows these requests to tunnel through the firewall to another proxy which then changes the method and finally forwards the request to the back end service.
The fact that this tunneling feature is a mandatory part of htmx's core makes it necessary to set up a proxy for any server software which uses methods other than GET and POST, even in circumstances where there's no reason for it.
In order to make the tunneling feature optional, I believe the code should be removed from the htmx core, and re-packaged as an http-method-override
extension (like the json-enc
extension). That way anyone who actually needed to tunnel through an enterprise firewall could include the extension in an architecturally clean manner, but it would not interfere with the general case in which people just want to use HTTP in a normal way.
See also https://twitter.com/conal_tuohy/status/1265926740049715201 for background.
Activate/Deactivate buttons do nothing.
The wikipedia article linked to https://en.wikipedia.org/wiki/HTTP_Verbs redirects to https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods and itself states:
HTTP defines methods (sometimes referred to as verbs, but nowhere in the specification does it mention verb, nor is OPTIONS or HEAD a verb) ...
Currently an SSE event can only trigger a new GET request. This works when the SSE is use to notify the frontend of available data, but often the SSE event itself can contain what is needed. Other targets, swapping, and indicator behaviors with SSE would be wonderful. For example:
<div hx-trigger="sse:thing" hx-swap="innerHTML">Waiting for callback</div>
I came to this from HN last week and the project seemed to be called kutty... is it now htmx?
Already a big fan of intercooler, so I'm excited to see where this leads! That being said, since you're starting clean - is there any reason why just k
as the prefix wouldn't work?
<div k-post="/clicked"
k-trigger="click"
k-target="#parent-div"
k-swap="outerHTML">
Click Me!
</div>
No big deal - just curious. Plus if you ever add a "pop" method then you'd have instant SEO! :) Congrats on the release!
I was a real fan of ic-deps as a way of allowing pages to, effectively, subscribe to changes which occur on the server-side after a form post is processed.
After a bit of back and forth on the gitter channel @chg20 suggested: hx-trigger="subscribe/foo/bar"
as a possible replacement, or hx-trigger="subscribe:POST/foo/bar"
if you only wanted to update for particular verbs. I'd be an enthusiastic supporter of this as it makes what was nice about ic-deps even more explicit.
In intercoolerjs, we can mock some url and test in single page.
How can do mock with htmx?
Attributes like hx-boost
have to have ="true"
would it be possible to have them without a value? Not a huge deal. I just thought it would be a little cleaner. I imagine there is a reason to include the true
value but I didn't see it in the issues.
Thanks everyone for the work on this. I'm excited to use this library on my next project!
Apologies in advance if I've missed something but it looks like the 'changed' trigger doesn't do anything yet?
It looks like the test for triggers is wired for change and click, and that after changing the value in the test a click is performed; so that even if the 'changed' trigger wasn't working it would still pass as the 'click' event would cause it to fire.
htmx/test/attributes/hx-trigger.js
Lines 29 to 43 in f8dfe72
I'm unsure exactly why changed
doesn't work, but I have noticed lastValue
is never stored in htmx-internal-data
(at least from inspecting the DOM properties it never appears, on both Chrome and Firefox).
Usecase scenario: some element needs to make a JSON POST request and then render the response to a template.
I am trying the below, but maybe the syntax is wrong?
<input
type="text"
name="query"
hx-ext="json-enc"
hx-ext="client-side-template"
hx-post="https://localhost:8080/v1/graphql"
hx-trigger="keyup changed delay:500ms"
mustache-template="my-mustache-template"
placeholder="<GRAPHQL QUERY TEXT HERE>"
/>
<div id="search-results" hx-ext="client-side-template"></div>
<script id="my-mustache-template" type="x-tmpl-mustache">
{{#data}}
{{#employees}}
Hello {{email}}
{{/employees}}
{{/data}}
</script>
I also tried hs-ext="json-enc client-side-template"
Edit: The docs use client-side-template
, but extension source uses client-side-templates
, so that was issue there.
Edit 2: The source seems to point to using a comma as a separator in the string between extensions, unlike classes which use spaces:
if (extensionsForElement) {
forEach(extensionsForElement.split(","), function(extensionName){
extensionName = extensionName.replace(/ /g, '');
var extension = extensions[extensionName];
if (extension && extensionsToReturn.indexOf(extension) < 0) {
extensionsToReturn.push(extension);
}
});
When I add a comma, all is well ๐
Maybe good note for extensions docs
I'm having trouble using the json-enc extension, because it appears to not update the Content-Type header. It is always application/x-www-form-urlencoded; charset=UTF-8
, even though I've confirmed with console.log that I'm reaching the code in the extension that is calling xhr.setRequestHeader("Content-Type", "application/json");
.
After some digging around, I found a kind of plot twist. Turns out, the parameter charset
isn't allowed for mime type application/x-www-form-urlencoded
. (For example, see discussion at here and here.) Accordingly, this code in htmx.js:
if (verb !== 'get') {
headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
...
should become
if (verb !== 'get') {
headers['Content-Type'] = 'application/x-www-form-urlencoded';
...
For whatever reason, after this change, the extension json-enc
does update the Content-Type
header -- hurray! (I confirmed this in FF 76.0.1 and Chrome 83.0.4103.6 on Mac.)
Unfortunately, the header value is actually appended to:
Content-Type: application/x-www-form-urlencoded, application/json
And that's because the setRequestHeader("Content-Type", ...)
is called twice, and behavior for setRequestHeader
is documented to be:
If this method is called several times with the same header, the values are merged into one single request header.
Each time you call setRequestHeader() after the first time you call it, the specified text is appended to the end of the existing header's content.
๐ฌ
So... it looks like setting the Content-Type header should be done once for the XMLHttpRequest instance.
One idea is to somehow allow extensions to modify the headers
map, and call this code once immediately before the xhr.send() call:
// request headers
for (var header in headers) {
if (headers.hasOwnProperty(header)) {
if (headers[header] !== null) xhr.setRequestHeader(header, headers[header]);
}
}
What do you think?
Hi I implemented a volume control with <input type="range" min="0" max="100" name="volume" hx-post="/volume" hx-trigger="input changed delay:200ms" hx-target="#display-none">
It would be nice if hx-trigger
accepted a throttle:200ms
argument because like it is now the volume changes only when one stops moving and not while the volume is changed.
The addRule
function can fail, and if it does it halts the entire initialization process. There are at least two ways this can happen:
If there are no stylesheets in a document, document.styleSheets[0]
is undefined
.
If the first stylesheet is provided by a remote server and the proper CORS settings are not set, the Kutty JS is not allowed to access the cssRules
of that stylesheet. As an example,
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
will not work, but
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" crossorigin="anonymous">
will.
There are a number of ways this could be solved. Off the top of my head:
addRule
, and log an explanation of what's gone wrong. People who are not using kutty-indicator
won't care.Very very excited about htmx just from reading about it. I have been thinking about my usual needs in developing backoffice applications, and modal popup form comes to mind.
For example, when a user wants to enter a sales order, but halfway through, it turns out the customer hasnt been entried into the system, or the address might have changed for that customer.
Usually i just open up a modal popup dialog displaying the customer form, enabling the user to create a new customer record or to add the new address, and after that returning to the main form to resume the data entry.
I'm curious on how to implement this approach with htmx ?
Thank you !
The file dist/ext/json-enc.js
doesn't match src/ext/json-enc.js
:
~/code/htmx โนmaster*โบ $ git log | head -1
commit f501ca6f7ed955374149ba4ec8ba39940d8b966a
~/code/htmx โนmaster*โบ $ diff -u src/ext/json-enc.js dist/ext/json-enc.js
--- src/ext/json-enc.js 2020-05-31 00:53:53.000000000 -0700
+++ dist/ext/json-enc.js 2020-05-31 00:59:02.000000000 -0700
@@ -1,7 +1,7 @@
htmx.defineExtension('json-enc', {
encodeParameters : function(xhr, parameters, elt) {
- xhr.setRequestHeader('Content-Type', 'application/json');
+ xhr.requestHeaders['Content-Type'] = 'application/json';
xhr.overrideMimeType('text/json');
return (JSON.stringify(parameters));
}
-});
+});
\ No newline at end of file
Consequently, https://unpkg.com/[email protected]/dist/ext/json-enc.js is missing the commit Fix json-enc extension content-type header set .
Hey, really like HTMX, nice job!
However I can't figure out how to pass my CSRF token in the request headers (or to actually configure headers at all).
It's strange because I used intercooler before, and I never had a missing CSRF token...
EDIT: I'm using Elixir/Phoenix
First, kudos on the successor.
I realize this question is out-of-scope. You've clearly positioned htmlx
as targeting/embracing HTML as the payload.
I'm toying with a static site generator that can save pages and fragments as hyperscript
-style tuples. CSR updates then happen with a simple Preact VDOM diff style update.
I think this would require too big of a scope expansion for htmx
:
Feel free to close this issue and say "we'll never allow that kind of pluggability."
I have a use-case in which I want hx-get
to refresh the contents of a parent div
.
<div hx-target="this">
<input hx-get="/reresh" type="text" name="name1" value="value1">
<input hx-get="/reresh" type="text" name="name2" value="value2">
<input hx-get="/reresh" type="text" name="name3" value="value3">
</div>
When any of the 3 input fields are changed, I would like all 3 field values to be sent in the request. I was hoping that putting hx-params="*"
on the div
element would achieve this, but it does not seem to work that way.
<div hx-params="*" hx-target="this">
<input hx-get="/refresh" type="text" name="name1" value="value1">
<input hx-get="/refresh" type="text" name="name2" value="value2">
<input hx-get="/refresh" type="text" name="name3" value="value3">
</div>
So I am currently solving this as follows, which does work (but feels a bit icky).
<div id="random-id" hx-include="#random-id *" hx-target="this">
<input hx-get="/refresh" type="text" name="name1" value="value1">
<input hx-get="/refresh" type="text" name="name2" value="value2">
<input hx-get="/refresh" type="text" name="name3" value="value3">
</div>
I know that if I was to use a form
and hx-post
then it would automatically send all input field values, but wondering if there is a better way for to achieve this with hx-get
? I'm also aware that I could write an extension that listens for the configRequest.htmx
event and adds all the values and I may well end up doing that, just wanted to ask first.
Hey, really like htmx so far! It's great what you have done with it!
I was wondering if you could help me with a POST request example? I have a server that I send a JSON to and it returns a response. For some reason I cannot seem to get this working on my end.
Would you be able to provide an example?
I would like to propose that there should be a way to indicate within htmx that, after a successful request, another endpoint should be accessed for the new content.
I've been looking at switching an existing project, which uses jQuery to do HTML replacement, over to htmx. This projects has users and groups in a many-to-many relationship, and has an endpoint to delete the association between a user and a group. The user's profile page had a AJAX link to this endpoint that easily switched over to htmx. The old jQuery code would trigger a refresh of that part of the page once the first request succeeded. To model that with htmx, I changed to server to return a redirect to the fragment URL, instead of an empty response. So far, this works great.
However, the group page also had code to hit this endpoint, which should then redirect to another fragment. If I switched that page over, it would load the wrong fragment in when deleting a user from a group. I can see a number of ways to fix this, but none is great:
Instead, I'd like to propose that htmx adds a way to indicate that a second request be fired off when the first succeeds. This would allow us to return the delete endpoint to returning nothing, with the two pages indicating how to reload their fragment after the delete happens. Some decisions would be needed:
What is the syntax for this option?
hx-redirect
or hx-success
(which suggests hx-error
, too).hx-get
and friends.I think the latter could get too complex, but I don't have strong feelings here.
What verb should be used for the follow-up requests. My gut feeling is that they should be GETs, acting like it would for 303 (and de-facto 302) responses.
When is this redirect triggered?
I view this as taking the redirect decision away from the server, so it should happen on any success. The other options would allow for more complex behavior, I guess.
I think #21 would provide another way to accomplish this goal. However, that's attacking a larger problem (if I'm understanding it correctly). It would also introduce more indirection to the behavior -- it wouldn't be obvious that one request triggers another. Having the redirect on the same element as the original request would make the flow more obvious, at the cost of limiting the complexity of flows available.
If you would consider adding such a feature, I'd be happy to take a shot at a first prototype.
I have a form that needs to POST an empty field value. In my context, an empty value denotes removing a coupon code from a user's cart.
However when I make a simple HTML form, and bind htmx to it, if the coupon code field is empty then it is not included in the POST request.
I have tried using hx-params="*"
on the form element, but this has no effect, still only non-empty fields are POSTed.
Interestingly, when I explicitly mentioned the input field in question, e.g. hx-params="couponCode"
, while it did POST that form param, the value sent was undefined
. I tried setting name and id attributes to couponCode
but both yielded undefined
...
Thanks for your help!
htmx version: 0.0.3
https://htmx.org/extensions/include-vals/
While the docs say that the "data-" prefix is supported for all "hx-" attributes, there appear to be several places in the code that do not honor it. This is based on reviewing the code, not running it. The unit tests don't have coverage for "data-" prefixes, and I haven't reproduced any of these issues.
querySelectorAll("[hx-trigger='revealed']")
querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']")
querySelector('[hx-history-elt]')
querySelector("[hx-history-elt],[data-hx-history-elt]")
querySelector('[hx-history-elt]')
querySelector("[hx-history-elt],[data-hx-history-elt]")
I'm getting this warning messages when using htmx. I think they're just saying that your using getResponseHeader
to get headers that aren't there. I can workaround by setting these headers to ''
on my server, but feels like I should be able to opt in to this functionality.
According to this article you can use getAllResponseHeaders
to get the header without warning.
(Thanks for making this library, I'm excited to use it)
ASP.NET's (and ASP.NET Core's) model binder for boolean parameters only supports (case-insensitive) "true" and "false" string values.
For the fragment
<label><input hx-get="/Foo" type="checkbox" name="bar" /> Click me!</label>
when checked, htmx makes a request to either /Foo
or /Foo?bar=on
depending on the checked state. I'm presuming some framework takes on
/off
for checkbox parameters, but I haven't come across it (rails, maybe, seeing as the docs mention it in a few places?).
In order of preference, is there
foo=on
(basically, can we switch to using true
instead of on
because whatever supports on
also supports true
?)foo=false
or foo=true
instead of alternating between sending a value and not sending a value)The link to add the library in the documentation is broken:
https://unpkg.com/[email protected]
And this one has an issue with parameters not being included in POST requests:
https://unpkg.com/[email protected]/dist/kutty.min.js
However, the version loaded at kutty.org works correctly.
Thanks!
Similar to htmx, we were using custom script that replaced JSON api call result in the HTML by data attributes. So it was not necessary to have HTML returned but it was possible to have bulk of the HTML in the place and just replace few values on request.
I am wondering what the idiomatic way is to handle request errors. From what I've been able to gather by searching the reference and this repository, I need to write JS to listen on the responseError.htmx
event and in some way present feedback to the user. I'm thinking there must be a way for the server response to be rendered even if it is a 500?
For example, I have this button:
<div></div>
<button hx-post="/clicked" hx-swap="outerHTML">
Click Me
</button>
And the server responds with something along the lines of:
Status 500
Body:
<div class="error">Sorry, it failed. Try again</div>
<button hx-post="/clicked" hx-swap="outerHTML">
Click Me
</button>
From experimenting, it seems that htmx
does not replace the content in this case. Or perhaps I'm doing things wrong? :)
Hello! I'm trying to use htmx to navigate an app while only a small part of the page doesn't reload (because it contains an <audio> element playing music).
Some events don't seem to be working, here is a small test file to reproduce the issue:
<?php declare(strict_types=1);
$link = $_SERVER['SCRIPT_NAME'] . '?t=' . time();
?>
<html>
<head>
<title>htmx test</title>
<script src="https://unpkg.com/[email protected]"></script>
<meta name="htmx-config" content='{"defaultSwapStyle":"outerHTML"}'>
<script>
htmx.on('beforeOnLoad.htmx', () => console.log('beforeOnLoad'));
htmx.on('beforeRequest.htmx', () => console.log('beforeRequest'));
htmx.on('afterRequest.htmx', () => console.log('afterRequest'));
htmx.on('afterSettle.htmx', () => console.log('afterSettle'));
</script>
</head>
<body>
<audio src="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3" controls></audio>
<div id="content">
<h1>HTMX event test <?= time() ?></h1>
<a href="<?= $link ?>"
hx-boost="true"
hx-target="#content"
hx-select="#content">Click this link!</a>
</div>
</body>
</html>
When I click the link, the #content div reloads correctly (leaving the audio element playing), however only the beforeRequest and beforeOnLoad events get triggered.
I saw this mentioned in #75 , but it didn't seem to have it's own GitHub issue.
This is a related note to this: #26
And I appreciate possibly a hard ask, but...
I understand how the oob
feature lets me return multiple containers that can get injected into the page, but this requires architecting my app such that it only returns fragments in response to an htmx request. However I would prefer to always send a single, full-page HTML response, and leave it to the front-end to pick and choose what to inject or discard.
For example if I'm building a store, and I want a user to be able to add an item to their cart, there are at least 2 DIVs in the response that I'd like to select & inject; a flash/notice confirming the add, and an update to the cart icon along my navigation bar.
I'm curious to hear your thoughts on this scenario. Thanks!
Since checkboxes don't send a value when unchecked, a common pattern when dealing with them in forms is to hide a sentinel value before it like so, and then always pick the last value given in the post body as the actual value of the field (PHP does this by default).
<input type="hidden" name="check" value="false">
<input type="checkbox" name="check" value="true">
However, when adding hx-post
to the element, the resulting post data seems to end up holding the values in opposite order:
{ "check": [ true, false ] }
Get these errors, but googling the error they seems to be a non issue. These guys says there is a fix. https://trackjs.com/blog/refused-unsafe-header/
Despite the RED LETTER ERRORS on the console, program never stops.
kutty.js:1169 Refused to get unsafe header "X-KT-Trigger"
xhr.onload @ kutty.js:1169
load (async)
issueAjaxRequest @ kutty.js:1165
issueRequest @ kutty.js:609
eventListener @ kutty.js:614
kutty.js:1170 Refused to get unsafe header "X-KT-Push"
xhr.onload @ kutty.js:1170
load (async)
issueAjaxRequest @ kutty.js:1165
issueRequest @ kutty.js:609
eventListener @ kutty.js:614
Since you guys are still early on in the rewrite I thought it would be worth while for you to know that the X
prefix on custom headers is deprecated. Just using HX
is enough. Not a huge deal, just makes the headers a little cleaner.
See https://www.keycdn.com/support/custom-http-headers#naming-conventions
which references the RFC Spec:
For a page with extensive DOM, the onload performance is not ideal. I understand it is needed for features like 'hx-boost' but for basic usage it is not needed. For capturing the click events global window listener could be sufficient.
To reproduce: Check Chrome Dev Tools main thread work for page with 4K+ DOM elements on load.
If you consider that hx-select
is going to pretty much always be used when your backend is serving a full response rather than a partial response (i.e. for graceful enhancement ajaxification of a regular document that would normally link to a full page), I believe it makes sense for hx-swap
to automatically default to outerHTML
rather than innerHTML
so that the entirety of the element in the response is swapped out one-for-one rather than the element being embedded within itself.
I'm really impressed with what you have built with this library.
One issue that I'm seeing is the docs say to use the responseError.htmx event to handle errors. It seems to me there should be a way to have error responses handled directly in the HTML.
Basically I would like to write HTML like this:
<form hx-post="/ajax/contact" hx-target-4xx=".messages">
<div class="messages"></div>
<div>
<label>Name</label>
<input type="text" name="name" value="" />
</div>
<div>
<label>Email</label>
<input type="text" name="email" value="" />
</div>
<div>
<label>Message</label>
<textarea name="message"></textarea>
</div>
<div>
<input type="submit" value="Send" />
<span class="loading"></span>
</div>
</form>
If the server responds with:
STATUS: 200
<p>Thank you for contacting us!</p>
The whole form gets replaced.
If the server responds with:
STATUS: 422
<p class="error">All fields are required.</p>
The response would be placed in the .messages
div.
I put together a rough example with an extension showing it working in more detail. It adds the ability to target specific error messages: hx-target-422=".messages"
Or all 400 level messages: hx-target-4xx=".messages"
Or all non-200 errors: hx-target-error=".messages"
It has a lot of duplicate functions at the end because the API does not provide a lot of access to the swap functions (unless I'm missing something).
Here is the roughed out working example (assume the server responds with the example responses above):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Contact Us</title>
</head>
<body>
<form hx-post="/ajax/contact" hx-target-4xx=".messages" hx-ext="hx-target-error">
<div class="messages"></div>
<div>
<label>Name</label>
<input type="text" name="name" value="" />
</div>
<div>
<label>Email</label>
<input type="text" name="email" value="" />
</div>
<div>
<label>Message</label>
<textarea name="message"></textarea>
</div>
<div>
<input type="submit" value="Send" />
<span class="loading"></span>
</div>
</form>
<script src="https://unpkg.com/[email protected]"></script>
<script>
htmx.defineExtension('hx-target-error', {
onEvent : function(name, evt) {
if(name === "responseError.htmx") {
var elt = evt.detail.elt;
var response = evt.detail.xhr.response;
var status = evt.detail.xhr.status;
var targetError = getTargetError(elt, status);
var settleInfo = makeSettleInfo(targetError);
var fragment = makeFragment(response);
if(targetError) {
swapInnerHTML(targetError, fragment, settleInfo);
}
}
}
})
function getTargetError(elt, status) {
var match = getClosestMatchError(elt, status);
if (match && match.target) {
var explicitTarget = match.target;
var attr = match.attr;
var targetStr = getAttributeValue(explicitTarget, attr);
if (targetStr === "this") {
return explicitTarget;
} else if (targetStr.indexOf("closest ") === 0) {
return closest(elt, targetStr.substr(8));
} else {
return getDocument().querySelector(targetStr);
}
}
}
function getClosestMatchError(elt, status) {
var output = {};
output.attr = "hx-target-" + status;
output.target = getClosestMatch(elt, function(e){return getAttributeValue(e,output.attr) !== null});
if(!output.target) {
output.attr = "hx-target-" + Math.floor(status/10) + "x";
output.target = getClosestMatch(elt, function(e){return getAttributeValue(e,output.attr) !== null});
}
if(!output.target) {
output.attr = "hx-target-" + Math.floor(status/100) + "xx";
output.target = getClosestMatch(elt, function(e){return getAttributeValue(e,output.attr) !== null});
}
if(!output.target) {
output.attr = "hx-target-error";
output.target = getClosestMatch(elt, function(e){return getAttributeValue(e,output.attr) !== null});
}
return output;
}
// Duplicate functions because the API is locked down with limited access.
// No need to read further.
function closest(elt, selector) {
do if (elt == null || matches(elt, selector)) return elt;
while (elt = elt && parentElt(elt));
}
function forEach(arr, func) {
if (arr) {
for (var i = 0; i < arr.length; i++) {
func(arr[i]);
}
}
}
function getAttributeValue(elt, qualifiedName) {
return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, "data-" + qualifiedName);
}
function getClosestMatch(elt, condition) {
if (condition(elt)) {
return elt;
} else if (parentElt(elt)) {
return getClosestMatch(parentElt(elt), condition);
} else {
return null;
}
}
function getDocument() {
return document;
}
function getRawAttribute(elt, name) {
return elt.getAttribute && elt.getAttribute(name);
}
function getStartTag(str) {
var tagMatcher = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i
var match = tagMatcher.exec( str );
if (match) {
return match[1].toLowerCase();
} else {
return "";
}
}
function handleAttributes(parentNode, fragment, settleInfo) {
forEach(fragment.querySelectorAll("[id]"), function (newNode) {
var oldNode = parentNode.querySelector(newNode.tagName + "[id=" + newNode.id + "]")
if (oldNode && oldNode !== parentNode) {
var newAttributes = newNode.cloneNode();
cloneAttributes(newNode, oldNode);
settleInfo.tasks.push(function () {
cloneAttributes(newNode, newAttributes);
});
}
});
}
function insertNodesBefore(parentNode, insertBefore, fragment, settleInfo) {
handleAttributes(parentNode, fragment, settleInfo);
while(fragment.childNodes.length > 0){
var child = fragment.firstChild;
parentNode.insertBefore(child, insertBefore);
if (child.nodeType !== Node.TEXT_NODE) {
//settleInfo.tasks.push(makeLoadTask(child));
}
}
}
function makeFragment(resp) {
var startTag = getStartTag(resp);
switch (startTag) {
case "thead":
case "tbody":
case "tfoot":
case "colgroup":
case "caption":
return parseHTML("<table>" + resp + "</table>", 1);
case "col":
return parseHTML("<table><colgroup>" + resp + "</colgroup></table>", 2);
case "tr":
return parseHTML("<table><tbody>" + resp + "</tbody></table>", 2);
case "td":
case "th":
return parseHTML("<table><tbody><tr>" + resp + "</tr></tbody></table>", 3);
default:
return parseHTML(resp, 0);
}
}
function makeLoadTask(child) {
return function () {
processNode(child);
triggerEvent(child, 'load.htmx', {});
};
}
function makeSettleInfo(target) {
return {tasks: [], elts: [target]};
}
function parentElt(elt) {
return elt.parentElement;
}
function parseHTML(resp, depth) {
var parser = new DOMParser();
var responseDoc = parser.parseFromString(resp, "text/html");
var responseNode = responseDoc.body;
while (depth > 0) {
depth--;
responseNode = responseNode.firstChild;
}
if (responseNode == null) {
responseNode = getDocument().createDocumentFragment();
}
return responseNode;
}
function swap(swapStyle, elt, target, fragment, settleInfo) {
switch (swapStyle) {
case "outerHTML":
swapOuterHTML(target, fragment, settleInfo);
return;
case "afterbegin":
swapAfterBegin(target, fragment, settleInfo);
return;
case "beforebegin":
swapBeforeBegin(target, fragment, settleInfo);
return;
case "beforeend":
swapBeforeEnd(target, fragment, settleInfo);
return;
case "afterend":
swapAfterEnd(target, fragment, settleInfo);
return;
default:
var extensions = getExtensions(elt);
for (var i = 0; i < extensions.length; i++) {
var ext = extensions[i];
try {
if (ext.handleSwap(swapStyle, target, fragment, settleInfo)) {
return;
}
} catch (e) {
logError(e);
}
}
swapInnerHTML(target, fragment, settleInfo);
}
}
function swapInnerHTML(target, fragment, settleInfo) {
var firstChild = target.firstChild;
insertNodesBefore(target, firstChild, fragment, settleInfo);
if (firstChild) {
while (firstChild.nextSibling) {
target.removeChild(firstChild.nextSibling);
}
target.removeChild(firstChild);
}
}
</script>
</body>
</html>
Hello and thanks for the great library!
Do you have any plans for adding a support for handling of redirects? I wanted to add htmx to a login page that either returns div with error info or a redirect to the main page on successful authorization, but sadly nothing happens when server returns redirects. Will there be any new features that can solve this problem without adding custom response headers?
The material.min.js can't work after clicking a htmx link
This is my sittings:
htmx.on("load.htmx", function(evt) {
gtag();
md();
main_js();
})
This is the material.min.js:
function md(){"use strict";function e(e //......
//...
md();
And other javascripts work perfectly
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.