Coder Social home page Coder Social logo

bigskysoftware / htmx Goto Github PK

View Code? Open in Web Editor NEW
32.8K 185.0 1.1K 28.2 MB

</> htmx - high power tools for HTML

Home Page: https://htmx.org

License: Other

HTML 7.52% JavaScript 91.60% CSS 0.55% Ruby 0.10% Shell 0.04% TypeScript 0.19%
html javascript hateoas rest htmx hyperscript

htmx's Introduction

</> htmx

high power tools for HTML

Discord Netlify Bundlephobia Bundlephobia

introduction

htmx allows you to access AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext

htmx is small (~14k min.gz'd), dependency-free, extendable & IE11 compatible

motivation

  • Why should only <a> and <form> be able to make HTTP requests?
  • Why should only click & submit events trigger them?
  • Why should only GET & POST be available?
  • Why should you only be able to replace the entire screen?

By removing these arbitrary constraints htmx completes HTML as a hypertext

quick start

  <script src="https://unpkg.com/[email protected]"></script>
  <!-- have a button POST a click via AJAX -->
  <button hx-post="/clicked" hx-swap="outerHTML">
    Click Me
  </button>

The hx-post and hx-swap attributes tell htmx:

"When a user clicks on this button, issue an AJAX request to /clicked, and replace the entire button with the response"

htmx is the successor to intercooler.js

installing as a node package

To install using npm:

npm install htmx.org --save

Note there is an old broken package called htmx. This is htmx.org.

website & docs

contributing

Want to contribute? Check out our contribution guidelines

No time? Then become a sponsor

hacking guide

To develop htmx locally, you will need to install the development dependencies.

Requires Node 15.

Run:

npm install

Then, run a web server in the root.

This is easiest with:

npx serve

You can then run the test suite by navigating to:

http://0.0.0.0:3000/test/

At this point you can modify /src/htmx.js to add features, and then add tests in the appropriate area under /test.

  • /test/index.html - the root test page from which all other tests are included
  • /test/attributes - attribute specific tests
  • /test/core - core functionality tests
  • /test/core/regressions.js - regression tests
  • /test/ext - extension tests
  • /test/manual - manual tests that cannot be automated

htmx uses the mocha testing framework, the chai assertion framework and sinon 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:
longing for a hypertext
already in hand

htmx's People

Contributors

1cg avatar adamchainz avatar adamckay avatar ajusa avatar alexpetros avatar alfonsrv avatar bencroker avatar benpate avatar bopeng avatar croxton avatar danieljsummers avatar dependabot[bot] avatar dz4k avatar gavinray97 avatar gnat avatar gone avatar guettli avatar inouire avatar jreviews avatar jvosloo avatar mayowa avatar renerick avatar rschroll avatar srcerer avatar srkunze avatar tapvt avatar telroshan avatar waldyrious avatar wiverson avatar xhaggi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

htmx's Issues

Best way to keep focus in the input box that was just swapped

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.

Specify multiple targets to swap

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!

Multiple extensions on one element (EG: json-enc & client-side-template)

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

How can I reload material.min.js

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

Be able to swap content on errors

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>

Install via npm and `import`?

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!

Modal popup div ?

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 !

Rendering hyperscript-style responses

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:

  • Intercept clicks and getting JSON, possibly storing as such in cache
  • Allow overriding the DOM updates, to use something like Preact

Feel free to close this issue and say "we'll never allow that kind of pluggability."

Move X-HTTP-Method-Override tunneling feature into an extension

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.

Checkbox behavior is not compatible with ASP.NET MVC/Razor

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

  • some value that is supported by both whatever backend you have in mind and ASP.NET's default boolean model binder that could be used instead of foo=on (basically, can we switch to using true instead of on because whatever supports on also supports true?)
  • some way to globally alter the value that is used for checkboxes when present (and perhaps when absent as well, e.g. always sending a value whether it's foo=false or foo=true instead of alternating between sending a value and not sending a value)

'changed' trigger not implemented yet?

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.

var input = make('<input hx-trigger="click changed" hx-target="#d1" hx-get="/test" value="foo"/>');
var div = make('<div id="d1"></div>');
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 1");
input.value = "bar";
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 2");
input.click();
this.server.respond();
div.innerHTML.should.equal("Requests: 2");

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).

Empty inputs are not being sent via POST, or `undefined` if explicitly sent

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

json-enc extension does not update Content-Type

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?

Why not just 'k' instead of 'kt'?

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!

Cannot use current page url for hx-get

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.

Idiomatic way of handling HTTP errors

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? :)

Kutty fails to initialize if it cannot add rules to the first stylesheet

The addRule function can fail, and if it does it halts the entire initialization process. There are at least two ways this can happen:

  1. If there are no stylesheets in a document, document.styleSheets[0] is undefined.

  2. 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:

  1. Kutty could insert its own stylesheet and stick the rules in that.
  2. Kutty could catch errors from addRule, and log an explanation of what's gone wrong. People who are not using kutty-indicator won't care.
  3. This could be dropped from JS, and a CSS file, or even snippet, could be provided for users to include themselves. While this is a little more work, it would make it easier for users to override the default CSS behavior.

Incomplete support for "data-" prefix

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.

  • L27: comment mentions "kt and data-kt prefixes"
    • should become "hx and data-hx prefixes"
  • L53: getClosestAttributeValue uses getRawAttribute
    • should use getAttributeValue
  • L293 & L295: getTarget uses getRawAttribute for "hx-target"
    • should use getAttributeValue
  • L631: initScrollHandler uses querySelectorAll("[hx-trigger='revealed']")
    • could be querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']")
  • L801: getHistoryElement uses querySelector('[hx-history-elt]')
    • could be querySelector("[hx-history-elt],[data-hx-history-elt]")
  • L858: loadHistoryFromServer uses querySelector('[hx-history-elt]')
    • could be querySelector("[hx-history-elt],[data-hx-history-elt]")
  • L1095: issueAjaxRequest uses getRawAttribute for "hx-target"
    • should use getAttributeValue

How to handle POST request errors?

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.

hx-swap should default to outerHTML if hx-select is set?

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.

add hx-trigger throttle

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.

Replacement of ic-deps?

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.

Ergonomics when always serving full-page responses

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).

hx-push-url does not include GET parameters

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.

Flag Attributes without including "true"

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!

Feature request: Client-side redirect

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:

  • We could listen for events and trigger this refresh from JS. But this is what I'd like to get away from.
  • We could create two delete endpoints, differing only in where they redirect. This seems needlessly duplicative.
  • We could pass the redirect URL along with the initial request in one of several ways. This seems better, but it still feels clunky.

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:

  1. What is the syntax for this option?

    • A new attribute like hx-redirect or hx-success (which suggests hx-error, too).
    • Multiple URLs in hx-get and friends.

    I think the latter could get too complex, but I don't have strong feelings here.

  2. 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.

  3. When is this redirect triggered?

    • On any successful response for the primary request.
    • Only when the primary response is empty.
    • Only when the primary response is 204.
    • Only when the primary response is 30x. (Can we convince the XHR to not follow these automatically?)

    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.

How to best send all values of `this` element

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.

Onload script performance

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.

Redirects support

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?

afterRequest, afterSettle events never trigger

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.

Refused to get unsafe header "X-HX-Trigger" and "X-HX-Push"

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)

Feature: Custom response handler

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.

ext/json-enc.js not updated in dist

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 .

Minor issue, scary console error on chromium

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

Out-of-order post data

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 ] }

SSE trigger variety

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>

hx-post with body example?

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?

Passing a CSRF token in the request header

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

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.