question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Be able to swap content on errors

See original GitHub issue

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/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:closed
  • Created 3 years ago
  • Reactions:1
  • Comments:6 (3 by maintainers)

github_iconTop GitHub Comments

4reactions
1cgcommented, May 29, 2020

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.

1reaction
1cgcommented, Jun 13, 2020

@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:

<form hx-post="/ajax/contact" _="on error.htmx(errorInfo) call myJavascriptMethod(errorInfo)">

and just use hyperscript as the glue between the events and the javascript logic.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Error while swapping elements in a python list using ...
I need to swap elements in a Python list. It works when I'm using a temporary variable to handle the swapping but doesn't...
Read more >
Evaluating and excluding swap errors in analogue tests of ...
(Note, the term swap error refers to the fact that a non-target feature is “swapped in” for the target feature. It should not...
Read more >
Swap errors in visual working memory are fully explained ...
In cue-based recall from working memory, incorrectly reporting features of an uncued item may be referred to as a “swap” error. One account...
Read more >
Swap Errors in Spatial Working Memory are Informed ...
In a typical visual working memory task participants study an array of colored items and must report the color of an item at...
Read more >
Swap error
Hi folks , when i monitor sap server , it shows swap error ... Erro Details Buffer - >swaps table definitions - >...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found