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>
This commit is contained in:
matiboy 2023-10-27 04:43:41 +08:00 committed by GitHub
parent 1040ace093
commit 712ee759f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 210 additions and 23 deletions

View File

@ -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()

View File

@ -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);
}
})
});

View File

@ -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);
}
});

View File

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

View File

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

View File

@ -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. :)
## 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>