From 6f34e30d24b0840ac72c1b31e1fdaf12eb13f7fd Mon Sep 17 00:00:00 2001 From: codecalm Date: Tue, 20 Oct 2026 01:30:38 +0200 Subject: [PATCH] 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 --- .changeset/alert-dialog.md | 2 +- core/js/src/alert-dialog.ts | 8 +++- core/js/tests/unit/alert-dialog.spec.ts | 12 ++++++ docs/content/ui/components/alert-dialog.mdx | 46 ++++++++++++++++----- preview/pages/alert-dialogs.astro | 29 ++++++++++++- 5 files changed, 83 insertions(+), 14 deletions(-) diff --git a/.changeset/alert-dialog.md b/.changeset/alert-dialog.md index 526a3b845..82736192a 100644 --- a/.changeset/alert-dialog.md +++ b/.changeset/alert-dialog.md @@ -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. diff --git a/core/js/src/alert-dialog.ts b/core/js/src/alert-dialog.ts index eb86de8dd..9afd08d83 100644 --- a/core/js/src/alert-dialog.ts +++ b/core/js/src/alert-dialog.ts @@ -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) { diff --git a/core/js/tests/unit/alert-dialog.spec.ts b/core/js/tests/unit/alert-dialog.spec.ts index 2bfd78fa3..35fcb0dcc 100644 --- a/core/js/tests/unit/alert-dialog.spec.ts +++ b/core/js/tests/unit/alert-dialog.spec.ts @@ -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 = '' + 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('[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() diff --git a/docs/content/ui/components/alert-dialog.mdx b/docs/content/ui/components/alert-dialog.mdx index d3c931ea7..08cf4e11e 100644 --- a/docs/content/ui/components/alert-dialog.mdx +++ b/docs/content/ui/components/alert-dialog.mdx @@ -114,18 +114,44 @@ The message is optional. A dialog with only a title is fine for a short question +## 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 = ` +

Choose who takes over the projects of this user.

+ +` +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 +} +``` + + + + + ## 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 Enter does not confirm it. diff --git a/preview/pages/alert-dialogs.astro b/preview/pages/alert-dialogs.astro index 6cc99f81b..5e96f51ef 100644 --- a/preview/pages/alert-dialogs.astro +++ b/preview/pages/alert-dialogs.astro @@ -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' +
+ + + Custom content + Pass a DOM element as message to put a form field in the dialog and read it after the promise resolves. + + +
+