diff options
author | matiboy <mathieu.chauvinc@gmail.com> | 2023-10-27 04:43:41 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-10-26 14:43:41 -0600 |
commit | 712ee759f17ceac51df6c4c8542401a83c35abee (patch) | |
tree | 74bb392d1c22914dcf5329a4e081972c02e7cce5 | |
parent | 1040ace09377abb453ceff56cafa9931f4e097a4 (diff) | |
download | htmx-712ee759f17ceac51df6c4c8542401a83c35abee.tar.gz htmx-712ee759f17ceac51df6c4c8542401a83c35abee.zip |
Fix `confirmed` being ignored in htmx:confirm event (#1610)
* Current behavior testing
Testing current library behavior
* Test should remove correct handler
* Add question in htmx:confirm event detail
* Allow skipping window.confirm
* Additional test without hx-confirm value
* Wrap htmx.off in finally
* More correct assertion in case of no calls to confirm
* Remove erroneously added formatting
* Remove erroneously added formatting
* Documentation, fix loop
---------
Co-authored-by: mat <matt@techspace.asia>
-rw-r--r-- | src/htmx.js | 12 | ||||
-rw-r--r-- | test/attributes/hx-confirm.js | 126 | ||||
-rw-r--r-- | test/core/events.js | 2 | ||||
-rw-r--r-- | test/index.html | 1 | ||||
-rw-r--r-- | www/content/attributes/hx-confirm.md | 10 | ||||
-rw-r--r-- | www/content/examples/confirm.md | 82 |
6 files changed, 210 insertions, 23 deletions
diff --git a/src/htmx.js b/src/htmx.js index 74bf5a79..62afd334 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -2975,12 +2975,13 @@ return (function () { } } + var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm"); // allow event-based confirmation w/ a callback - if (!confirmed) { - var issueRequest = function() { - return issueAjaxRequest(verb, path, elt, event, etc, true); + if (confirmed === undefined) { + var issueRequest = function(skipConfirmation) { + return issueAjaxRequest(verb, path, elt, event, etc, !!skipConfirmation); } - var confirmDetails = {target: target, elt: elt, path: path, verb: verb, triggeringEvent: event, etc: etc, issueRequest: issueRequest}; + var confirmDetails = {target: target, elt: elt, path: path, verb: verb, triggeringEvent: event, etc: etc, issueRequest: issueRequest, question: confirmQuestion}; if (triggerEvent(elt, 'htmx:confirm', confirmDetails) === false) { maybeCall(resolve); return promise; @@ -3081,8 +3082,7 @@ return (function () { } } - var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm"); - if (confirmQuestion) { + if (confirmQuestion && !confirmed) { if(!confirm(confirmQuestion)) { maybeCall(resolve); endRequestLock() diff --git a/test/attributes/hx-confirm.js b/test/attributes/hx-confirm.js new file mode 100644 index 00000000..7f8516d7 --- /dev/null +++ b/test/attributes/hx-confirm.js @@ -0,0 +1,126 @@ +describe("hx-confirm attribute", function () { + var confirm + beforeEach(function () { + this.server = makeServer(); + confirm = sinon.stub(window, "confirm"); + clearWorkArea(); + }); + afterEach(function () { + this.server.restore(); + confirm.restore() + clearWorkArea(); + }); + + it('prompts using window.confirm when hx-confirm is set', function () { + this.server.respondWith("GET", "/test", "Clicked!"); + confirm.returns(true); + var btn = make('<button hx-get="/test" hx-confirm="Sure?">Click Me!</button>') + btn.click(); + confirm.calledOnce.should.equal(true); + this.server.respond(); + btn.innerHTML.should.equal("Clicked!"); + }) + + it('stops the request if confirm is cancelled', function () { + this.server.respondWith("GET", "/test", "Clicked!"); + confirm.returns(false); + var btn = make('<button hx-get="/test" hx-confirm="Sure?">Click Me!</button>') + btn.click(); + confirm.calledOnce.should.equal(true); + this.server.respond(); + btn.innerHTML.should.equal("Click Me!"); + }) + + it('uses the value of hx-confirm as the prompt', function () { + this.server.respondWith("GET", "/test", "Clicked!"); + confirm.returns(false); + var btn = make('<button hx-get="/test" hx-confirm="Sure?">Click Me!</button>') + btn.click(); + confirm.firstCall.args[0].should.equal("Sure?"); + this.server.respond(); + btn.innerHTML.should.equal("Click Me!"); + }) + + it('should prompt when htmx:confirm handler calls issueRequest', function () { + try { + var btn = make('<button hx-get="/test" hx-confirm="Surely?">Click Me!</button>') + var handler = htmx.on("htmx:confirm", function (evt) { + evt.preventDefault(); + evt.detail.issueRequest(); + }); + btn.click(); + confirm.calledOnce.should.equal(true); + } finally { + htmx.off("htmx:confirm", handler); + } + }) + + it('should include the question in htmx:confirm event', function () { + var stub = sinon.stub(); + try { + var btn = make('<button hx-get="/test" hx-confirm="Surely?">Click Me!</button>') + var handler = htmx.on("htmx:confirm", stub); + btn.click(); + stub.calledOnce.should.equal(true); + stub.firstCall.args[0].detail.should.have.property("question", "Surely?"); + } finally { + htmx.off("htmx:confirm", handler); + } + }) + + it('should allow skipping built-in window.confirm when using issueRequest', function () { + this.server.respondWith("GET", "/test", "Clicked!"); + try { + var btn = make('<button hx-get="/test" hx-confirm="Sure?">Click Me!</button>') + var handler = htmx.on("htmx:confirm", function (evt) { + evt.detail.question.should.equal("Sure?"); + evt.preventDefault(); + evt.detail.issueRequest(true); + }); + btn.click(); + confirm.called.should.equal(false); + this.server.respond(); + btn.innerHTML.should.equal("Clicked!"); + } finally { + htmx.off("htmx:confirm", handler); + } + }) + it('should allow skipping built-in window.confirm when using issueRequest', function () { + this.server.respondWith("GET", "/test", "Clicked!"); + try { + var btn = make('<button hx-get="/test" hx-confirm="Sure?">Click Me!</button>') + var handler = htmx.on("htmx:confirm", function (evt) { + evt.detail.question.should.equal("Sure?"); + evt.preventDefault(); + evt.detail.issueRequest(true); + }); + btn.click(); + confirm.called.should.equal(false); + this.server.respond(); + btn.innerHTML.should.equal("Clicked!"); + } finally { + htmx.off("htmx:confirm", handler); + } + }) + + + it('should allow htmx:confirm even when no hx-confirm is set', function () { + this.server.respondWith("GET", "/test", "Clicked!"); + try { + var btn = make('<button hx-get="/test">Click Me!</button>') + var handler = htmx.on("htmx:confirm", function (evt) { + evt.detail.should.have.property("question", null); + evt.preventDefault(); + evt.detail.issueRequest(); + }); + btn.click(); + confirm.called.should.equal(false); // no hx-confirm means no window.confirm + this.server.respond(); + btn.innerHTML.should.equal("Clicked!"); + } finally { + htmx.off("htmx:confirm", handler); + } + }) + + +});
\ No newline at end of file diff --git a/test/core/events.js b/test/core/events.js index 261a407f..7c1c1596 100644 --- a/test/core/events.js +++ b/test/core/events.js @@ -659,7 +659,7 @@ describe("Core htmx Events", function() { this.server.respond(); div.innerHTML.should.equal("updated"); } finally { - htmx.off("htmx:load", handler); + htmx.off("htmx:confirm", handler); } }); diff --git a/test/index.html b/test/index.html index 50892993..b0841d25 100644 --- a/test/index.html +++ b/test/index.html @@ -70,6 +70,7 @@ <!-- attribute tests --> <script src="attributes/hx-boost.js"></script> +<script src="attributes/hx-confirm.js"></script> <script src="attributes/hx-delete.js"></script> <script src="attributes/hx-ext.js"></script> <script src="attributes/hx-get.js"></script> diff --git a/www/content/attributes/hx-confirm.md b/www/content/attributes/hx-confirm.md index 5a8ceaa5..9cb3ced4 100644 --- a/www/content/attributes/hx-confirm.md +++ b/www/content/attributes/hx-confirm.md @@ -13,6 +13,16 @@ Here is an example: </button> ``` +## Event details + +The event triggered by `hx-confirm` contains additional properties in its `detail`: + +* triggeringEvent: the event that triggered the original request +* issueRequest(skipConfirmation=false): a callback which can be used to confirm the AJAX request +* question: the value of the `hx-confirm` attribute on the HTML element + ## Notes * `hx-confirm` is inherited and can be placed on a parent element +* `hx-confirm` uses the browser's `window.confirm` by default. You can customize this behavior as shown [in this example](@/examples/confirm.md). +* a boolean `skipConfirmation` can be passed to the `issueRequest` callback; if true (defaults to false), the `window.confirm` will not be called and the AJAX request is issued directly diff --git a/www/content/examples/confirm.md b/www/content/examples/confirm.md index fde399f0..d1f00c49 100644 --- a/www/content/examples/confirm.md +++ b/www/content/examples/confirm.md @@ -8,32 +8,78 @@ action. This uses the default `confirm()` function in javascript which, while t applications UX. In this example we will see how to use [sweetalert2](https://sweetalert2.github.io) and the [`htmx:confirm`](@/events.md#htmx:confirm) -event to implement a custom confirmation dialog. +event to implement a custom confirmation dialog. Below are two examples, one with `hyperscript` using a click+custom event method, and one in vanilla JS and the built-in `hx-confirm` attribute. + +## Hyperscript, on click+custom event ```html <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> -<button hx-get="/confirmed" - _="on htmx:confirm(issueRequest) - halt the event - call Swal.fire({title: 'Confirm', text:'Do you want to continue?'}) - if result.isConfirmed issueRequest()"> +<button hx-get="/confirmed" + hx-trigger='confirmed' + _="on click + call Swal.fire({title: 'Confirm', text:'Do you want to continue?'}) + if result.isConfirmed trigger confirmed"> Click Me </button> ``` We add some hyperscript to invoke Sweet Alert 2 on a click, asking for confirmation. If the user confirms -the dialog, we trigger the request by invoking the `issueRequest()` function, which was destructured from the event -detail object. +the dialog, we trigger the request by triggering the custom "confirmed" event +which is then picked up by `hx-trigger`. Note that we are taking advantage of the fact that hyperscript is [async-transparent](https://hyperscript.org/docs/#async) and automatically resolves the Promise returned by `Swal.fire()`. A VanillaJS implementation is left as an exercise for the reader. :) -{{ demoenv() }} +## Vanilla JS, hx-confirm +```html <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> +<script> + document.addEventListener("htmx:confirm", function(e) { + e.preventDefault() + Swal.fire({ + title: "Proceed?", + text: `I ask you... ${e.detail.question}` + }).then(function(result) { + if(result.isConfirmed) e.detail.issueRequest(true) // use true to skip window.confirm + }) + }) +</script> + +<button hx-get="/confirmed" hx-confirm="Some confirm text here"> + Click Me +</button> +``` + +We add some javascript to invoke Sweet Alert 2 on a click, asking for confirmation. If the user confirms +the dialog, we trigger the request by calling the `issueRequest` method. We pass `skipConfirmation=true` as argument to skip `window.confirm`. + +This allows to use `hx-confirm`'s value in the prompt which is convenient +when the question depends on the element e.g. a django list: +```html +{% for row in clients %} +<button hx-post="/delete/{{client.pk}}" hx-confirm="Delete {{client.name}}??">Delete</button> +{% endfor %} +``` + +{{ demoenv() }} + +<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> +<script> + document.addEventListener("htmx:confirm", function(e) { + e.preventDefault() + Swal.fire({ + title: "Proceed?", + text: `I ask you... ${e.detail.question}`, + showCancelButton: true + }).then(function(result) { + if(result.isConfirmed) e.detail.issueRequest(true) + }) + }) +</script> <script> //========================================================================= @@ -51,13 +97,17 @@ A VanillaJS implementation is left as an exercise for the reader. :) // templates function initialUI() { - return `<button hx-trigger='confirmed' - hx-get="/confirmed" - _="on click - call Swal.fire({title: 'Confirm', text:'Do you want to continue?'}) - if result.isConfirmed trigger confirmed"> - Click Me - </button>`; + return `<button hx-get="/confirmed" + _="on htmx:confirm(issueRequest) + halt the event + call Swal.fire({title: 'Confirm', text:'Do you want to continue?'}) + if result.isConfirmed issueRequest()"> + Click me (hyperscript click & custom event) +</button><br><br> + <button id="confirmButton" hx-get="/confirmed" hx-confirm="Some confirm text here"> + Click Me (vanilla JS, hx-confirm) +</button> +`; } </script> |