Dialogs are most useful for quick prompts and simple forms which do not re-render their contents as inputs or other data changes (as compared to FormApplication
which is well-suited for more advanced forms).
The API documentation for the DialogData and DialogButton types are useful references throughout this.
Dialog extends the Application
class.
Unlike the other Application
ancestors, Dialog
s have a hardcoded handlebars template and getData
method.
It is expected that the content
of the Dialog be passed to the Dialog
constructor in the form of a static HTML string. Dialogs do not re-render if their content
changes.
Dialogs have an API for handling user input in the form of buttons. It is recommended to use these buttons instead of including your own in your content
.
There are essentially two ways to interact with the Dialog api: by wrapping the Dialog in a Promise, or by leveraging the callbacks of the buttons.
Neither way is more correct, both are useful patterns in different circumstances.
Useful for long-running processes which return promises themselves that other API functionality awaits.
async function someDangerousProcess() {
const confirmation = await Dialog.prompt({
content: "Are you sure?"
});
if (!confirmation) {
return;
}
// something like a Document update happens
}
More examples of wrapping Custom Dialogs with promises in the Specific Use Cases section below.
Useful for operations that are not awaited by other API areas and may be easier to follow without understanding the inner workings of Dialog
.
function informUser() {
Dialog.confirm({
content: "These are the results of your inquiry: 42",
callback: () => {
// do something when button is clicked
// Like all callbacks, you can provide a function here
// instead of doing this inline with an anonymous arrow function
},
})
}
The actual template used for dialogs is quite simple:
<div class="dialog-content">
{{{content}}}
</div>
<div class="dialog-buttons">
{{#each buttons as |button id|}}
<button class="dialog-button {{cssClass}}" data-button="{{id}}" {{disabled button.disabled}}>
{{{button.icon}}}
{{{button.label}}}
</button>
{{/each}}
</div>
There are three ways to display a Dialog:
data
and options
to the Dialog
class constructor for fully custom DialogsDialog.prompt
factory method for simple notificationsDialog.confirm
factory method for Yes/No confirmationsThe Dialog constructor takes two arguments:
data
which is passed to the Dialog's rendered templateoptions
which is passed up to the parent Application rendering optionsdata
FieldsThese fields are configurable on the first argument (data
) for the Dialog
constructor.
data.title
A static string that will be displayed in the Application
's title
area. Defaults to "Dialog".
data.content
A static string which is rendered in the content area of the Dialog. HTML tags will not be escaped when rendering content
of a Dialog as it is rendered with Handlebars' built-in 'triple-stash' expression.
data.buttons
(required)To create a dialog with no buttons, provide an empty object.
Dialogs allow buttons to be registered for display and interactivity. Buttons are registered in the form of an object keyed by button "name" with values being an object with these properties:
Name | Type | Attributes | Description |
---|---|---|---|
icon | string | optional | An HTML element that preceeds the button label . Convention is to use Fontawesome icons |
label | string | The label for the button | |
disabled | boolean | optional | Whether the button is disabled |
callback | function | optional | A callback function that fires when the button is clicked, the Dialog's html at the time of button click is provided as argument |
function handleFoo(dialogHtml) {
// do something
}
const buttons = {
foo: {
icon: '<i class="fas fa-dice-d20"></i>',
label: 'Foo'
callback: handleFoo;
},
bar: {
label: 'Bar'
}
};
By default when any button
is clicked the Dialog's close
method will fire and the Dialog will exit.
data.default
Passing the "name" or key
that identifies of one of the defined buttons
will style that button as the "default" button and make pressing the "enter" keyboard key fire that button's callback.
If the button indicated has no callback, an error will be thrown.
data.render(html)
The provided render
callback is fired immediately after the Dialog
is created during the Application#activateListeners
method. This allows any listeners that the contents might warrant to be registered.
The Dialog's html
at the time of rendering is provided as argument.
data.close(html)
Interrupting the Dialog close process can be done by throwing an error in the close callback but this is not the recommended way. See Preventing Dialog Close for more information.
The provided close
callback is fired while the Dialog is closing before the Application#close
method.
The Dialog's html
at the time of closing is provided as argument.
options
The second argument passed to the Dialog
constructor is passed on to Application
as its constructor options.
new Dialog({
title: "This is a custom Dialog!",
content: "With custom content.",
buttons: {
buttonA: {
label: "A Button",
callback: () => {}
},
no: {
label: "No",
callback: () => {}
}
},
default: 'buttonA',
}).render(true)
The Dialog.prompt
, Dialog.confirm
, and Dialog.wait
methods are helper factory methods which provide abstractions for the two most common Dialog use cases.
Dialog.prompt
The prompt
factory method returns a Promise which resolve
s when the user makes a selection or reject
s if the user closes the dialog (configurable to instead resolve with null
).
The Promise resolves with the value output by the provided callback
function, if defined. If there is no callback defined, an error is thrown.
config
fieldsField | Type | Dialog Data Equivalent | Description |
---|---|---|---|
title |
string | data.title |
The confirmation window title |
content |
string | data.content |
The confirmation message |
label |
string | data.buttons['ok'].label |
The confirmation button text |
callback |
function | data.buttons['ok'].callback |
A callback function to fire when the button is clicked |
render |
function | data.render |
A function that fires after the dialog is rendered |
rejectClose |
boolean (default: true ) |
n/a | Reject the promise if the dialog is closed without confirming the choice, otherwise resolve as null |
options |
ApplicationOptions |
options |
Additional rendering options passed to Application |
Dialog.prompt({
title: "Something Happened",
content: "I hope you're ok with that.",
label: "I am",
callback: (html) => { /* do something or return value */ }
})
callback
The button callback
can be an asynchronous operation. The resolved value from that Promise is bubbled up to the prompt
Promise resolution.
const response = await Dialog.prompt({
title: "Something Happened",
content: "I hope you're ok with that.",
label: "I am",
callback: async (html) => { return 'foo' },
});
console.log('responded', response); // expects 'foo' when the button is pressed
Dialog.confirm
The confirm
factory method returns a Promise which resolve
s when the user makes a selection or reject
s if the user closes the dialog (configurable to instead resolve with null
).
The Promise resolves with the value output by the provided yes
or no
callbacks, if defined. These button callbacks can be asynchronous same as the callback
option in Dialog.prompt
. If there is no callback defined, true
is returned for "yes" and false
for "no".
config
fieldsName | Type | Dialog Data Equivalent | Description |
---|---|---|---|
title |
string | data.title |
The confirmation window title |
content |
string | data.content |
The confirmation message |
yes |
function | data.buttons['yes'].callback |
Callback function upon yes |
no |
function | data.buttons['no'].callback |
Callback function upon no |
render |
function | data.render |
A function to call when the dialog is rendered |
defaultYes |
boolean (default: true ) |
n/a | Make "yes" the default choice? |
rejectClose |
boolean (default: false ) |
n/a | Reject the Promise if the Dialog is closed without making a choice. |
options |
ApplicationOptions |
options |
Additional rendering options passed to the Dialog |
Dialog.confirm({
title: "Confirmation Title",
content: "Are you sure about that?",
yes: (html) => { /* do something or return value */ },
no: (html) => { /* do something or return value */ },
})
Foundry provides a built-in method to handle dialogs as a promise, allowing you to await
the output.
This is useful in situations where a quick prompt for user input is desired in an asynchronous process.
const dialogOutput = await Dialog.wait({
title: "A custom dialog title",
content: "Some content for your dialog.",
buttons: {
foo: { label: "foo", callback: () => ( 'foo' ) },
bar: { label: "bar", callback: () => ( 'bar' ) },
},
close: () => { reject() }
});
console.log(dialogOutput); // logs based on the button clicked
The content
of a Dialog expects a string that can be safely rendered as HTML. It's possible to use handlebars to construct this html string by passing the output of a renderTemplate
call to the Dialog constructor.
Note that this will not re-render with new data like the core DocumentSheet
s do without additional jerry-rigging. It's generally advisable to use a custom Application
or FormApplication
if such interactivity is desired.
const contentHtml = await renderTemplate('path/to/template.hbs', data);
new Dialog({
title: "A custom dialog title",
content: contentHtml,
buttons: {
foo: { label: "foo" },
bar: { label: "bar" },
},
}).render(true);
The callback
of a Dialog button gets the html of the dialog as its argument. From this, it is possible to read the state of form elements within the Dialog content
when it is submitted.
Foundry Core includes the helper class FormDataExtended
to help with this.
// this could be the output of renderTemplate to use handlebars
const form = `<form>
<label>Input 1 <input name="input-1" type="string"/></label>
</form>`;
function handleSubmit(html) {
const formElement = html[0].querySelector('form');
const formData = new FormDataExtended(formElement);
const formDataObject = formData.toObject();
// prints an object: { input-1: 'some value' }
console.log('output form data object', formDataObject);
}
new Dialog({
title: "A custom dialog title",
content: form,
buttons: {
submit: { label: "Submit", callback: handleSubmit },
cancel: { label: "Cancel" },
},
}).render(true);
Here's an alternate method that uses Dialog.wait
and an anonymous arrow function:
// this could be the output of renderTemplate to use handlebars
const form = `<form>
<label>Input 1 <input name="input-1" type="string"/></label>
</form>`;
const formResult = await Dialog.wait({
title: "A custom dialog title",
content: form,
buttons: {
submit: { label: "Submit", callback: (html) => {
const formElement = html[0].querySelector('form');
const formData = new FormDataExtended(formElement);
const formDataObject = formData.toObject();
return formDataObject;
}},
cancel: { label: "Cancel" },
},
});
// If submit was entered, this will print an object of the form { input-1: 'some value' }
// If cancel was entered, there is no callback defined
// Thus this will print the key of the button; in this case, `cancel`
console.log('output form data object', formResult);
In cases involving Dialogs which gather user inputs, it might be desired to prevent the dialog from closing in some circumstances. There are two ways to accomplish this:
close
functionThis is not recommended as it is possible to get the user into a state where there is no way to dismiss the dialog, which is very user-unfriendly.
Additionally, there is no feedback given to the user why the dialog fails to close as the error is only logged to the console.
This does not prevent the button callbacks from firing.
new Dialog({
title: "An Unclosable Dialog",
content: "You can open it, but you can never close it.",
buttons: {
submit: { label: "Submit", callback: () => { console.log('User tried to submit') }},
cancel: { label: "Cancel" },
},
close: () => { throw new Error('You cannot leave here!'); }
}).render(true);
callback
A much more user-friendly way to prevent submission of the dialog is to throw an error in a button callback.
These errors are displayed to the user in the form of UI Notifications by default.
new Dialog({
title: "A custom Dialog",
content: "You cannot submit it, but you can close it.",
buttons: {
submit: { label: "Submit", callback: () => { throw new Error('Submission invalid!') }},
cancel: { label: "Cancel" },
},
}).render(true);
In some cases, it is valuable to register event listeners on the elements within the rendered dialog to provide more interactivity than buttons or native form elements allow.
The render
callback provides the rendered Dialog as a jQuery object as parameter.
const htmlContent = "<div>Click here for more Details: <button type='button' class='custom-button'>Details</button></div>";
function handleRender(html) {
html.on('click', 'button.custom-button', () => { /* do something */ });
}
new Dialog({
title: "A custom Dialog",
content: htmlContent,
buttons: {
done: { label: "Done" },
},
render: handleRender,
}).render(true);
Creating a subclass of Dialog allows for considerably more control over every aspect of the Dialog. This is useful for more complex cases. Since Dialog inherits from Application
, all of the normal ways to interact with an Application are available.
This is also a common method for creating a custom factory method when some default Application options for the Dialog would be easier to override in one place (e.g. classes
to help with styling).
class GreetingDialog extends Dialog {
constructor(dialogData = {}, options = {}) {
super(dialogData, options);
this.options.classes = ['my-custom-class-name'];
}
/**
* A custom dialog factory for our use case.
* @param {object} options
* @param {string} options.name - The name of whoever we are greeting
* @returns {Promise}
*/
static async create(options) {
return new Promise(resolve => {
new this({
title: `A custom dialog named: ${options.name}`,
content: `Hello ${options.name}`,
buttons: {
hello: { label: "Hello!", callback: () => { resolve(true) }}
},
close: () => { resolve(false) }
}).render(true);
});
}
}
// ...
const noticeMe = await GreetingDialog.create({ name: "Steve" });
if (noticeMe) {
console.log("Steve likes me!");
} else {
console.log("Steve doesn't like me.");
}
When creating a custom Dialog, it is important to call the render
method of the instanciated Dialog (inherited from Application#render
) with the true
argument.
const customDialog = new Dialog({ ... });
customDialog.render(true);
The following example demonstrates the following all at once:
<form>
<div class="flexrow">
<div class="flexcol">
<h3>
Presets
</h3>
<button data-preset="apple" type="button">
Apple
</button>
<button data-preset="banana" type="button">
Banana
</button>
<button data-preset="orange" type="button">
Orange
</button>
</div>
<div class="flexcol">
<label>
Name
<input type='text' name='name' />
</label>
<label>
Color
<input type='color' name='color' />
</label>
</div>
</div>
</form>
/**
* Sets the form's inputs based on the selected preset.
*/
function handleFruitPreset(event) {
const targetElement = event.currentTarget;
const presetType = targetElement.dataset?.preset;
const formElement = $(targetElement).parents('form');
const nameInput = formElement?.find('[name="name"]');
const colorInput = formElement?.find('[name="color"]');
if (!presetType || !nameInput || !colorInput) {
return;
}
switch (presetType) {
case 'apple':
nameInput.val('Apple');
colorInput.val('#ff0000');
break;
case 'banana':
nameInput.val('Banana');
colorInput.val('#ffff00');
break;
case 'orange':
nameInput.val('Orange');
colorInput.val('#ff7700');
break;
default:
throw new Error(`Unknown preset: ${presetType}`);
break;
}
}
/**
* Verify that all expected formData fields have values.
*/
function verifyFruitInputs(formData) {
if (!formData?.name) {
throw new Error('Name is required');
}
if (!formData?.color) {
throw new Error('Color is required');
}
}
/**
* Prompt the user for traits they wish the created fruit to have.
*/
async function promptForFruitTraits() {
const htmlContent = await renderTemplate('handlebarsTemplate.hbs');
return new Promise((resolve, reject) => {
const dialog = new Dialog({
title: "Fruit Traits",
content: htmlContent,
buttons: {
submit: {
label: "Create",
icon: '<i class="fas fa-apple-alt"></i>',
callback: (html) => {
const formData = new FormDataExtended(html[0].querySelector('form'))
.toObject();
verifyFruitInputs(formData);
resolve(formData);
},
},
skip: {
label: "Skip",
callback: () => resolve(null),
},
cancel: {
label: "Cancel",
callback: () => reject('User canceled.'),
},
},
render: (html) => {
html.on('click', 'button[data-preset]', handleFruitPreset);
},
close: () => {
reject('User closed dialog without making a selection.');
},
});
dialog.render(true);
});
}
// ...
// create a fruit
try {
const fruitTraits = await promptForFruitTraits();
if (!fruitTraits) {
console.warn('User skipped fruit creation.')
} else {
console.log(fruitTraits); // logs the form output
}
} catch(error) {
console.error(error);
}