Confirm Dialog ยืนยันก่อนลบ
A promise-based replacement for window.confirm(), with a danger style and type-to-confirm for big deletes.
Live demo
Everything here talks to the real Spring Boot + SQL Server back end.Orders (order_request) 3
| # | Customer | Plan | Seats | City | |
|---|---|---|---|---|---|
| 10 | Kittipong Srisuk kitti@example.com |
BUSINESS | 80 | Bangkok | |
| 9 | Anna Lee anna@example.com |
STARTER | 1 | Chiang Mai | |
| 8 | Somchai Jaidee somchai@example.com |
TEAM | 12 | Bangkok |
No orders. Create some in lesson 322 (Stepper) or use Reset demo data on Home.
A non-destructive confirm
What to notice
- UI.confirm() returns a Promise<boolean>, so code reads like: if (await UI.confirm(...)).
- The safe button has the focus, so Enter never deletes by accident.
- The DELETE request runs only after the user confirms.
Try it
Source code
7 parts, read from the files in this project
src/main/java/com/study/uiux/web/BoardApi.java
DELETE /api/orders/{id} and DELETE /api/orders?confirm=DELETE
@DeleteMapping("/orders/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteOrder(@PathVariable int id) {
orders.delete(id);
}
@DeleteMapping("/orders")
public Map<String, Object> deleteAllOrders(@RequestParam(defaultValue = "") String confirm) {
return Map.of("deleted", orders.deleteAll(confirm));
}
src/main/java/com/study/uiux/service/OrderService.java
The delete only runs after the dialog said yes
@Transactional
public void delete(int id) {
if (!repository.existsById(id)) throw ApiException.notFound("Order " + id);
repository.deleteById(id);
}
@Transactional
public long deleteAll(String confirmText) {
if (!"DELETE".equals(confirmText)) throw ApiException.badRequest("Type DELETE to confirm.");
long count = repository.count();
repository.deleteAllInBatch(); // one DELETE FROM order_request
return count;
}
src/main/resources/db/migration/V1__schema.sql
order_request: written by the stepper, deleted with a confirm dialog
CREATE TABLE order_request (
id INT IDENTITY(1,1) NOT NULL CONSTRAINT pk_order_request PRIMARY KEY,
customer_name NVARCHAR(100) NOT NULL,
email NVARCHAR(150) NOT NULL,
phone NVARCHAR(20) NOT NULL,
plan_code NVARCHAR(20) NOT NULL,
seats INT NOT NULL,
addons NVARCHAR(200) NOT NULL CONSTRAINT df_order_addons DEFAULT N'',
address_line NVARCHAR(200) NOT NULL,
city NVARCHAR(80) NOT NULL,
postal_code NVARCHAR(10) NOT NULL,
notes NVARCHAR(500) NOT NULL CONSTRAINT df_order_notes DEFAULT N'',
created_at DATETIME2(0) NOT NULL CONSTRAINT df_order_created DEFAULT SYSUTCDATETIME(),
CONSTRAINT ck_order_plan CHECK (plan_code IN (N'STARTER', N'TEAM', N'BUSINESS')),
CONSTRAINT ck_order_seats CHECK (seats BETWEEN 1 AND 500)
);
src/main/resources/templates/layout.html
The confirm dialog markup: Cancel has autofocus
<dialog class="modal modal-sm" id="confirmDialog" aria-labelledby="confirmTitle">
<form method="dialog">
<div class="confirm-icon"><svg class="i"><use href="#i-alert"/></svg></div>
<h2 id="confirmTitle">Are you sure?</h2>
<p id="confirmMessage" class="muted"></p>
<label class="field" id="confirmTypeField" hidden>
<span>Type <b id="confirmWord">DELETE</b> to confirm</span>
<input class="input" id="confirmInput" autocomplete="off">
</label>
<div class="modal-actions">
<button class="btn btn-ghost" value="cancel" id="confirmCancel" autofocus>Cancel</button>
<button class="btn btn-danger" value="ok" id="confirmOk">Delete</button>
</div>
</form>
</dialog>
src/main/resources/static/css/app.css
The confirm dialog: a warning icon and a red button
.confirm-icon { width: 3rem; height: 3rem; border-radius: 50%; display: grid; place-items: center; background: var(--danger-soft); color: var(--danger); margin-bottom: 1rem; }
.confirm-icon .i { width: 1.5rem; height: 1.5rem; }
#confirmDialog.safe .confirm-icon { background: var(--primary-soft); color: var(--primary-text); }
#confirmDialog h2 { font-size: 1.15rem; }
#confirmDialog .field { margin-top: .5rem; }
src/main/resources/static/js/core.js
UI.confirm() returns a Promise that resolves to true or false
function confirmDialog({ title = 'Are you sure?', message = '', confirmText = 'Delete', cancelText = 'Cancel', danger = true, typeToConfirm = null } = {}) {
const dlg = $('#confirmDialog');
const ok = $('#confirmOk'), input = $('#confirmInput'), typeField = $('#confirmTypeField');
$('#confirmTitle').textContent = title;
$('#confirmMessage').textContent = message;
ok.textContent = confirmText;
$('#confirmCancel').textContent = cancelText;
ok.className = 'btn ' + (danger ? 'btn-danger' : 'btn-primary');
dlg.classList.toggle('safe', !danger);
typeField.hidden = !typeToConfirm;
input.value = '';
ok.disabled = !!typeToConfirm;
if (typeToConfirm) {
$('#confirmWord').textContent = typeToConfirm;
input.oninput = () => { ok.disabled = input.value.trim() !== typeToConfirm; };
}
return new Promise((resolve) => {
dlg.returnValue = '';
modal.open(dlg);
if (typeToConfirm) input.focus(); else $('#confirmCancel').focus(); // the safe choice has the focus
dlg.addEventListener('close', () => resolve(dlg.returnValue === 'ok'), { once: true });
});
}
src/main/resources/static/js/demos.js
await UI.confirm(...) before the DELETE request
$('#orderRows').addEventListener('click', async (e) => {
const btn = e.target.closest('[data-delete]');
if (!btn) return;
const tr = btn.closest('tr');
const ok = await UI.confirm({
title: 'Delete this order?',
message: `Order #${tr.dataset.id} from ${btn.dataset.name} will be removed from SQL Server. This cannot be undone.`,
confirmText: 'Delete order'
});
if (!ok) return;
await api('/api/orders/' + tr.dataset.id, { method: 'DELETE' });
tr.remove();
count();
toast({ type: 'success', title: 'Order deleted', message: `DELETE FROM order_request WHERE id = ${tr.dataset.id}` });
});
$('#deleteAll').addEventListener('click', async () => {
const ok = await UI.confirm({
title: 'Delete ALL orders?',
message: `All ${$$('#orderRows tr').length} orders will be deleted. Type the word below to unlock the button.`,
confirmText: 'Delete everything', typeToConfirm: 'DELETE'
});
if (!ok) return;
const r = await api('/api/orders?confirm=DELETE', { method: 'DELETE' });
$('#orderRows').innerHTML = '';
count();
toast({ type: 'success', title: 'All orders deleted', message: r.deleted + ' rows removed.' });
});