Be able to swap content on errors
See original GitHub issueI’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/htmx.org@0.0.4"></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>
Issue Analytics
- State:
- Created 3 years ago
- Reactions:1
- Comments:6 (3 by maintainers)

Top Related StackOverflow Question
Hi Anthony,
I am working on an approach for exactly this. It is going to take me a bit to get it right, but I think you will be happy with the results. I will keep this issue open until I’m ready to discuss it.
@agraddy totally understand.
Your other options are a simple extension that handles the error.htmx error, or simply a javascript event handler on the body for the same event.
I’m trying to keep htmx as simple as possible and make it open and extensible for all the rest.
Should also mention that you can call out to javascript from a hyperscript handler:
and just use hyperscript as the glue between the events and the javascript logic.