Here is an example of a FormApplication that will be used in this guide.
class TemplateApplication extends FormApplication {
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "foo-form",
title: `My Module: ${game.i18n.localize("FOO.form.title")}`,
template: "./modules/foo/templates/form.hbs",
classes: ["form"],
width: 640,
height: "auto",
closeOnSubmit: true,
});
}
getData() {
const setting = game.settings.get("foo", "config");
return {
setting
}
}
async activateListeners(html) {
super.activateListeners(html);
html.find("input[name=something]").on("click", /* ... */);
html.find("button[name=reset]").on("click", async (event) => {
await game.settings.set("foo", "config", {});
});
}
async _updateObject(event, formData) {
await Promise.all(
Object.entries(formData)
.map(([key, value]) => game.settings.set("foo", key, value));
);
}
}
static get defaultOptions
is replaced by static DEFAULT_OPTIONS = {}
, but not everything that used to be there goes there.
What used to be
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "foo-form",
title: `My Module: ${game.i18n.localize("FOO.form.title")}`,
template: "./modules/foo/templates/form.hbs",
classes: ["form"],
width: 640,
height: "auto",
closeOnSubmit: true,
});
}
Should now become
static DEFAULT_OPTIONS = {
id: "foo-form
form: {
handler: TemplateApplication.#onSubmit,
closeOnSubmit: true,
},
position: {
width: 640,
height: "auto",
},
tag: "form", // The default is "div"
window: {
icon: "fas fa-gear", // You can now add an icon to the header
title: "FOO.form.title"
}
}
get title() {
return `My Module: ${game.i18n.localize(this.options.window.title)}`;
}
The template is now declared in a separate property: static PARTS
. Note that it no longer needs to be a monolithic file.
Here you declare the parts that will be loaded. The order matters.
static PARTS = {
foo: {
template: "./modules/foo/templates/form.hbs"
}
}
getData(options)
is replaced by _prepareContext(options)
. This is just a name change.
_prepareContext(options) {
const setting = game.settings.get("foo", "config");
return {
setting
}
}
activateListeners(html)
is replaced by _onRender(context, options)
. Here, context
is the same data you've returned on _prepareContext(options)
.
Reminder that AppV2 has abandoned jQuery, so, if you still want to use it, you must add the following: const html = $(this.element)
.
_onRender(context, options) {
this.element.querySelector("input[name=something]").addEventListener("click", /* ... */);
// We will deal with reset later
}
On static DEFAULT_OPTIONS
we've declared a handler: TemplateApplication.#onSubmit
, which replaces Application#_updateObject
.
The main difference is that formData is the whole FormDataExtended
instead of the its object
.
#onSubmit(event, form, formData) {
const settings = foundry.utils.expandObject(formData.object);
await Promise.all(
Object.entries(settings)
.map(([key, value]) => game.settings.set("foo", key, value))
);
}
Action
s are functions that automatically get bound as click
listeners to any element that has the appropriate data-action
in its attributes. Importantly, these should be static functions, but their this
value will still point to the specific class instance.
They are declared on the DEFAULT_OPTIONS
.
On this example, we will rewrite the reset listener as an action
:
static DEFAULT_OPTIONS = {
actions: {
reset: TemplateApplication.reset,
},
}
static async reset() {
await game.settings.set("foo", "config", {});
}
Right now you have a complete 1:1 conversion of your original Application, but you will notice that the submit button is reloading the page instead of actually triggering the form's handler
.
Here is how you fix it:
submit
-type button.PARTS
, add the following:static PARTS = {
// ...
footer: {
template: "templates/generic/form-footer.hbs",
},
}
_prepareContext
, add the following:_prepareContext(options) {
return {
// ...
buttons: [
{ type: "submit", icon: "fa-solid fa-save", label: "SETTINGS.Save" }
]
}
}
static DEFAULT_OPTIONS = {
// ...
window: {
contentClasses: ["standard-form"]
}
}
Now, test it. If the issue persists, check if any of templates have a <form>
. If they do, replace it for <div>
.
I don't know why it works like this, but that's how it is.
class="standard-form"
to your HTML template's outer <div>
so your inner <div class="form-group">
get proper spacing.<p class="notes">
for <p class="hint">
.