Feature 318 Overlays

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

#CustomerPlanSeatsCity
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

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.' });
});

Are you sure?

Rename

↑↓ moveEnter openEsc close