Let AlertDialog message take a DOM element

- form fields can live in the dialog and be read after the promise resolves
- docs section, demo card and unit test
This commit is contained in:
codecalm committed 2026-10-20 01:30:38 +02:00
1 parent a3f55df601
commit 6f34e30d24
5 files changed
+83 -14

No files matched your search

+1 -1
View File
@@ -4,4 +4,4 @@
"@tabler/docs": minor
---
Added `AlertDialog` with `confirm()` and `alert()` methods, a modal replacement for `window.confirm()` and `window.alert()`.
Added `AlertDialog` with `confirm()` and `alert()` methods, a modal replacement for `window.confirm()` and `window.alert()`. `message` takes a string or a DOM element, so a confirmation can hold a form field.
+6 -2
View File
@@ -4,7 +4,7 @@ const VARIANTS = ['primary', 'secondary', 'success', 'info', 'warning', 'danger'
export type AlertDialogOptions = {
title?: string
message?: string
message?: string | Element
confirmText?: string
cancelText?: string
variant?: (typeof VARIANTS)[number]
@@ -80,7 +80,11 @@ const show = (input: string | AlertDialogOptions, withCancel: boolean): Promise<
root.querySelector(selector)!.textContent = text
}
set('h3', title)
set(`#${id}-description`, message)
if (message instanceof Element) {
root.querySelector(`#${id}-description`)!.append(message)
} else {
set(`#${id}-description`, message)
}
set('[data-confirm]', confirmText)
root.querySelector('[data-confirm]')!.classList.add(`btn-${color}`)
if (withCancel) {
+12
View File
@@ -83,6 +83,18 @@ describe('AlertDialog', () => {
await promise
})
it('accepts a message element and keeps the reference', async () => {
const field = document.createElement('select')
field.innerHTML = '<option value="a">A</option><option value="b">B</option>'
const promise = AlertDialog.confirm({ title: 'Delete user?', message: field })
const el = await shown()
expect(el.querySelector('[id$="-description"] select')).toBe(field)
field.value = 'b'
el.querySelector<HTMLElement>('[data-confirm]')!.click()
expect(await promise).toBe(true)
expect(field.value).toBe('b')
})
it('does not put an unknown variant into the markup', async () => {
const promise = AlertDialog.confirm({ title: 'Hi', icon: 'trash', variant: 'x" onmouseover="alert(1)' as never })
const el = await shown()
+36 -10
View File
@@ -114,18 +114,44 @@ The message is optional. A dialog with only a title is fine for a short question
</button>
</Example>
## Custom content
`message` also takes a DOM element, for example a form field the user has to fill in before the action goes ahead. The element is moved into the dialog as it is, so you keep the reference and can read its value after the promise resolves. The dialog is removed from the page when it closes, so build a fresh element for
each call.
```js
const field = document.createElement('div')
field.innerHTML = `
<p>Choose who takes over the projects of this user.</p>
<select class="form-select">
<option value="1">Jane Doe</option>
<option value="2">John Smith</option>
</select>
`
const ok = await tabler.AlertDialog.confirm({ title: 'Delete user?', message: field, confirmText: 'Delete', variant: 'danger', align: 'start' })
if (ok) {
const owner = field.querySelector('select').value
}
```
<Example centered>
<button type="button" class="btn btn-danger" onclick="const field = document.createElement('div'); field.innerHTML = '<p>Choose who takes over the projects of this user.</p><select class=&quot;form-select&quot;><option>Jane Doe</option><option>John Smith</option></select>'; tabler.AlertDialog.confirm({ title: 'Delete user?', message: field, confirmText: 'Delete', variant: 'danger', align: 'start' })">
Delete user
</button>
</Example>
## Options
| Option | Default | Description |
| ------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `title` | `Are you sure?` for `confirm()`, `Notice` for `alert()` | The heading of the dialog. |
| `message` | none | A line of text under the title. It is set as plain text, so HTML is not rendered. |
| `confirmText` | `OK` | The label of the confirm button. |
| `cancelText` | `Cancel` | The label of the cancel button. `confirm()` only. |
| `variant` | `primary` | The color of the confirm button. |
| `icon` | none | A built-in icon name or a DOM element, shown in a round badge in the color of the variant. |
| `align` | `center` | `center` or `start`. See [Layout](#layout). |
| `focus` | `confirm` | Which button gets focus when the dialog opens: `confirm` or `cancel`. `confirm()` only. |
| Option | Default | Description |
| ------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `title` | `Are you sure?` for `confirm()`, `Notice` for `alert()` | The heading of the dialog. |
| `message` | none | A line of text under the title, or a DOM element. Text is set as plain text, so HTML in a string is not rendered. |
| `confirmText` | `OK` | The label of the confirm button. |
| `cancelText` | `Cancel` | The label of the cancel button. `confirm()` only. |
| `variant` | `primary` | The color of the confirm button. |
| `icon` | none | A built-in icon name or a DOM element, shown in a round badge in the color of the variant. |
| `align` | `center` | `center` or `start`. See [Layout](#layout). |
| `focus` | `confirm` | Which button gets focus when the dialog opens: `confirm` or `cancel`. `confirm()` only. |
For a destructive action, set `focus: 'cancel'` so a stray <kbd>Enter</kbd> does not confirm it.
+28 -1
View File
@@ -21,6 +21,10 @@ import ButtonList from '@ui/ButtonList.astro'
document.querySelectorAll('[data-alert-dialog]').forEach((button) => {
button.addEventListener('click', () => {
const options = JSON.parse(button.dataset.alertDialog)
const template = button.dataset.alertDialogTemplate && document.getElementById(button.dataset.alertDialogTemplate)
if (template) {
options.message = template.content.firstElementChild.cloneNode(true)
}
if (options.alert) {
tabler.AlertDialog.alert(options).then(() => {
result.textContent = 'Alert closed'
@@ -28,7 +32,8 @@ import ButtonList from '@ui/ButtonList.astro'
return
}
tabler.AlertDialog.confirm(options).then((ok) => {
result.textContent = ok ? 'Confirmed' : 'Cancelled'
const select = options.message instanceof Element && options.message.querySelector('select')
result.textContent = ok ? (select ? `Confirmed, reassigned to ${select.value}` : 'Confirmed') : 'Cancelled'
})
})
})
@@ -64,6 +69,28 @@ import ButtonList from '@ui/ButtonList.astro'
</Card>
</div>
<div class="col-md-6 col-lg-4">
<Card>
<CardBody>
<CardTitle>Custom content</CardTitle>
<CardSubtitle>Pass a DOM element as <code>message</code> to put a form field in the dialog and read it after the promise resolves.</CardSubtitle>
<template id="alert-dialog-reassign">
<div>
<p>Choose who takes over the projects of this user.</p>
<select class="form-select">
<option>Jane Doe</option>
<option>John Smith</option>
<option>Mary Jones</option>
</select>
</div>
</template>
<ButtonList>
<Button color="danger" text="Delete user" data-alert-dialog-template="alert-dialog-reassign" data-alert-dialog={JSON.stringify({ title: 'Delete user?', confirmText: 'Delete', variant: 'danger', align: 'start' })} />
</ButtonList>
</CardBody>
</Card>
</div>
<div class="col-md-6 col-lg-4">
<Card>
<CardBody>