Agent Skill
2/7/2026fvtt-applications
This skill should be used when creating custom windows with ApplicationV2, building forms with FormApplication, using Dialog for prompts, understanding the render lifecycle, or migrating from Application v1 to ApplicationV2.
I
impropersubset
0GitHub Stars
1Views
npx skills add ImproperSubset/hh-agentics
SKILL.md
| Name | fvtt-applications |
| Description | This skill should be used when creating custom windows with ApplicationV2, building forms with FormApplication, using Dialog for prompts, understanding the render lifecycle, or migrating from Application v1 to ApplicationV2. |
name: fvtt-applications description: This skill should be used when creating custom windows with ApplicationV2, building forms with FormApplication, using Dialog for prompts, understanding the render lifecycle, or migrating from Application v1 to ApplicationV2.
Foundry VTT Applications
Domain: Foundry VTT Module/System Development Status: Production-Ready Last Updated: 2026-01-05
Overview
Applications are the window/dialog system in Foundry. ApplicationV2 (V12+) is the modern framework replacing the legacy Application v1 (deprecated, removed in V16).
When to Use This Skill
- Creating custom UI windows
- Building data entry forms
- Showing confirmation dialogs
- Understanding render lifecycle
- Migrating v1 applications to v2
ApplicationV2 Basics
Minimal Application
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
class MyWindow extends HandlebarsApplicationMixin(ApplicationV2) {
static DEFAULT_OPTIONS = {
id: "my-window",
classes: ["my-module"],
position: { width: 400, height: "auto" },
window: {
title: "My Window",
icon: "fas fa-gear"
}
};
static PARTS = {
main: {
template: "modules/my-module/templates/window.hbs"
}
};
async _prepareContext(options) {
return {
message: "Hello World"
};
}
}
// Usage
new MyWindow().render(true);
Form Application
class MyForm extends HandlebarsApplicationMixin(ApplicationV2) {
static DEFAULT_OPTIONS = {
id: "my-form",
tag: "form", // CRITICAL for form handling
window: { title: "Settings" },
position: { width: 500 },
form: {
handler: MyForm.#onSubmit,
submitOnChange: false,
closeOnSubmit: true
}
};
static PARTS = {
form: {
template: "modules/my-module/templates/form.hbs"
}
};
async _prepareContext() {
return {
settings: this.settings
};
}
static async #onSubmit(event, form, formData) {
const data = foundry.utils.expandObject(formData.object);
console.log("Submitted:", data);
// Process form data
}
}
DialogV2
Confirmation Dialog
const confirmed = await foundry.applications.api.DialogV2.confirm({
window: { title: "Confirm" },
content: "<p>Delete this item?</p>"
});
if (confirmed) {
await item.delete();
}
Input Prompt
const name = await foundry.applications.api.DialogV2.prompt({
window: { title: "Enter Name" },
content: "<p>What is your character's name?</p>",
label: "Submit"
});
Custom Buttons
const result = await foundry.applications.api.DialogV2.wait({
window: { title: "Choose Action" },
content: "<p>What would you like to do?</p>",
buttons: [{
icon: "fas fa-check",
label: "Accept",
action: "accept"
}, {
icon: "fas fa-times",
label: "Decline",
action: "decline"
}]
});
if (result === "accept") {
// Handle accept
}
Form Dialog
const data = await foundry.applications.api.DialogV2.prompt({
window: { title: "Configure" },
content: `
<div class="form-group">
<label>Name</label>
<input type="text" name="name" value="">
</div>
<div class="form-group">
<label>Level</label>
<input type="number" name="level" value="1">
</div>
`,
ok: {
callback: (event, button, dialog) => {
const form = dialog.querySelector("form");
return new FormDataExtended(form).object;
}
}
});
Render Lifecycle
Key Methods
class MyApp extends HandlebarsApplicationMixin(ApplicationV2) {
// Prepare data for template
async _prepareContext(options) {
return { items: this.items };
}
// Prepare data for specific part
async _preparePartContext(partId, context) {
if (partId === "list") {
context.sortedItems = this.sortItems(context.items);
}
return context;
}
// After first render only
async _onFirstRender(context, options) {
this.setupInitialState();
}
// After every render
async _onRender(context, options) {
this.attachEventListeners();
}
}
Lifecycle Order
render(true) called
→ _preRender()
→ _prepareContext()
→ _preparePartContext() (per part)
→ Template rendering
→ _onFirstRender() (first time only)
→ _onRender()
→ Hook: renderMyApp
Event Handling
Static Actions (Recommended)
class MyApp extends HandlebarsApplicationMixin(ApplicationV2) {
static DEFAULT_OPTIONS = {
actions: {
delete: MyApp.#onDelete,
edit: MyApp.#onEdit
}
};
static async #onDelete(event, target) {
const itemId = target.dataset.itemId;
await this.deleteItem(itemId);
}
static #onEdit(event, target) {
const itemId = target.dataset.itemId;
this.editItem(itemId);
}
}
Template:
<button type="button" data-action="delete" data-item-id="{{item.id}}">
Delete
</button>
Manual Listeners in _onRender
async _onRender(context, options) {
this.element.querySelector(".custom-button")
?.addEventListener("click", this._onCustomClick.bind(this));
}
_onCustomClick(event) {
event.preventDefault();
// Handle click
}
Multi-Part Templates
PARTS Configuration
static PARTS = {
header: {
template: "modules/my-mod/templates/header.hbs"
},
tabs: {
template: "templates/generic/tab-navigation.hbs"
},
content: {
template: "modules/my-mod/templates/content.hbs",
scrollable: [""] // Enable scroll preservation
},
footer: {
template: "modules/my-mod/templates/footer.hbs"
}
};
Tab Navigation
static PARTS = {
tabs: { template: "templates/generic/tab-navigation.hbs" },
details: { template: "templates/details.hbs" },
inventory: { template: "templates/inventory.hbs" }
};
static TABS = {
primary: {
tabs: [
{ id: "details", label: "Details" },
{ id: "inventory", label: "Inventory" }
],
initial: "details"
}
};
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.tabs = this._prepareTabs();
return context;
}
async _preparePartContext(partId, context) {
if (["details", "inventory"].includes(partId)) {
context.tab = context.tabs[partId];
}
return context;
}
Legacy FormApplication (V1)
For maintenance of existing code only:
class LegacyForm extends FormApplication {
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "legacy-form",
title: "Legacy Form",
template: "modules/my-mod/templates/form.hbs",
width: 400,
closeOnSubmit: true
});
}
getData() {
return { data: this.object };
}
async _updateObject(event, formData) {
await this.object.update(formData);
}
activateListeners(html) {
super.activateListeners(html);
html.find(".button").click(this._onClick.bind(this));
}
}
Common Pitfalls
1. Missing tag: "form"
// WRONG - form submission won't work
static DEFAULT_OPTIONS = {
form: { handler: MyApp.#onSubmit }
};
// CORRECT
static DEFAULT_OPTIONS = {
tag: "form",
form: { handler: MyApp.#onSubmit }
};
2. Button Submits Form
<!-- WRONG - triggers form submission -->
<button>Click Me</button>
<!-- CORRECT - won't submit form -->
<button type="button">Click Me</button>
3. DialogV2 Re-render
// WRONG - DialogV2 cannot re-render
const dialog = new DialogV2({...});
await dialog.render(true);
await dialog.render(true); // Error!
// CORRECT - use ApplicationV2 for re-renderable windows
class MyDialog extends HandlebarsApplicationMixin(ApplicationV2) {}
4. Async in _prepareContext
// Always await async operations
async _prepareContext(options) {
const data = await this.loadData(); // OK
return { data };
}
5. Losing this Context
// WRONG - loses context
this.element.addEventListener("click", this._onClick);
// CORRECT - bind context
this.element.addEventListener("click", this._onClick.bind(this));
// Or use arrow function
this.element.addEventListener("click", (e) => this._onClick(e));
6. Form Spacing Issues
{{!-- Use standard-form class --}}
<div class="standard-form">
<div class="form-group">
<label>Field</label>
<input type="text" name="field">
</div>
</div>
Implementation Checklist
ApplicationV2
- Extend
HandlebarsApplicationMixin(ApplicationV2) - Define
static DEFAULT_OPTIONSwith id, classes, position - Define
static PARTSfor templates - Implement
_prepareContext()for template data - Set
tag: "form"for form applications - Define
form.handlerfor form submission - Use
data-actionattributes with static actions - Use
type="button"on non-submit buttons
DialogV2
- Use
DialogV2.confirm()for yes/no prompts - Use
DialogV2.prompt()for text input - Use
DialogV2.wait()for custom buttons - Remember DialogV2 cannot re-render
References
Last Updated: 2026-01-05 Status: Production-Ready Maintainer: ImproperSubset
Skills Info
Original Name:fvtt-applicationsAuthor:impropersubset
Download