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('') + 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('') + 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('') + 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('') + 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('') + 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('') + 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('') + 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('') + 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 @@ + 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: ``` +## 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 - ``` 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. :) +## Vanilla JS, hx-confirm + +```html + + + + +``` + +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 %} + +{% endfor %} +``` + {{ demoenv() }} - +