Try   HackMD

Foundry VTT Module Making for Beginners - Single Page Edition

tags: module-tutorial foundry-vtt

This page has every other page embedded within it. It's easier to search but some of the formatting is going to be worse, some elements repeated, etc.

Foundry VTT Module Making for Beginners - Introduction

tags: module-tutorial foundry-vtt

So you want to make a Foundry VTT Module but you're not sure where to start?

This tutorial will start at the very beginning and end with a working module, explaining as much as is feasible to explain along the way.

How to read this tutorial

This tutorial works best as a start-to-finish step-by-step journey into module making. It will walk you through each part of the process, including some purposefully wrong steps to demonstrate how to debug things when they do not work. As a result, if you copy and paste directly from some examples without the context, things might not work.

What we'll be building

Here's the elevator pitch:

A module which allows each individual user in a Foundry VTT world to have a to-do list that they can add entries to, mark entries complete, and delete entries from.

What we'll learn about

By the end of this tutorial, you'll have at least touched each of the following core Foundry VTT API concepts:

  • Hooks
  • Flags
  • FormApplication
  • Dialog
  • Settings
  • Handlebars
  • CSS
  • Localization

From my experience, having a working understanding of these Foundry-specific things in addition to a working knowledge of Javascript, HTML, and CSS is a rock solid baseline for starting to create Foundry VTT Modules.

Prerequisites

An activated local FoundryVTT installation on version 0.8.8+ or version 9

All of the pieces this tutorial interacts with are in the Public API for Foundry VTT and thus it is expected to be relevant for a long time, but it is confirmed to be working on Core version 0.8.8 and version 9 stable.

Some working knowledge of HTML, CSS, and JavaScript (ES2020)

If you're completely new to these technologies, this tutorial can still be helpful to you, but you may find yourself overwhelmed.

Style & Organization Notes

Code Samples

Code Samples will be styled like so:

// This is some Code

Ocassionaly there will not be line numbers or syntax highlights.

// This is also a code sample

Errors

Sometimes we'll be debugging errors that our code has, they'll be styled like so:

Error: This is an error.

Programming Concepts

Where relevant, we'll be learning some concepts tangential to Foundry VTT specific work. These will be styled like so:

Programming Concept: Foo

"Foo" is a common placeholder when creating examples.


License Information & Attribution

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution-ShareAlike 4.0 License, and code samples are licensed under the Unlicense (text below).

Provide attribution to Andrew Krigline, aka "Calego".

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.

In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to http://unlicense.org/


Getting Started

tags: module-tutorial foundry-vtt

A lot of this first step is very similar to the official knowledgebase article: Introduction to Module Development

I. Find your userData directory

Navigate to your Foundry Installation's userData directory.

This directory is set during the Foundry installation process and varies depending on how you run Foundry. If you are using the standalone foundry application on Windows (aka the Electron app), you can right click on the taskbar icon and select "Browse User Data" to get there.

II. Create your Module Directory

Create a new directory under Data/modules. Let's name this directory todo-list.

├── Config
├── Data
│   ├── modules
│   │   └── todo-list   
│   ├── systems
│   └── worlds
└── Logs

There is only one file needed to register a module with Foundry and that is the Manifest JSON. This file tells Foundry some information about the module and how to use it.

III. Create a Manifest JSON

todo-list
└── module.json

In your new directory create a file named module.json and add this to it:

{ "name": "my-todo-list", "title": "To Don't", "description": "A simple module which adds a user specific to do list.", "authors": [ { name: "You!" } ], "version": "1", "minimumCoreVersion": "0.8.8", }

There's a more detailed breakdown of what each of these fields means (and a whole lot more fields we'll be adding later) on the official knowledgebase article.

At this point we have created a module! It doesn't do anything yet but let's make sure it's working. Start up your Foundry Instance and go to the "Modules and Addons" tab in the setup screen. We should see a module named "To Don't" in this list.

Narrator. We do not.

IV. Debugging "Why Won't my Module Show Up?"

Our module has exactly 1 file, so there's very few ways it might be broken.

Does our Directory name match our Module name?

In our Manifest JSON we set the name to "my-todo-list". We created a directory named todo-list in our userData/Data/modules. Unless these two things match, Foundry will not recognize the module as valid.

Let's change our Manifest to instead have "name": "todo-list".

Anytime a Manifest JSON is changed, the Foundry Server needs to be restarted to pick up the change.

Restart foundry to see if it fixed things.

Narrator. It did not.

Is our JSON valid?

JSON is a very finnicky way to describe data. We should run our JSON through a validator to make sure it doesn't have any syntax errors.

Error: Parse error on line 5:
...",	"authors": [{		name: "You!"	}],	"
---------------------^
Expecting 'STRING', '}', got 'undefined'

Ah, we failed to put quotation marks around name in authors.

  "authors": [
    {
      "name": "You!"
    }
  ],

Let's fix that and run validation again.

Error: Parse error on line 9:
...eVersion": "0.8.8",}
----------------------^
Expecting 'STRING', got '}'

Ok new error, that's progress. This time it looks like we left a trailing comma at the end of our JSON object. Let's fix that and run again.

  "minimumCoreVersion": "0.8.8"
Valid JSON

Excellent. Make those changes in your file and restart Foundry to see if that fixed it.

Windows likes to hide file extensions sometimes. If your module doesn't show up in Foundry, the file might actually be called module.json.txt (with Windows hiding the .txt). For more information and instructions how to disable this behavior, check this article.

image of module tab

🥳

Locking a Package

Go ahead and lock this package by clicking the "Lock" button next to "Uninstall". This creates a simple .lock file in our module folder and tells Foundry never to update it. Since we are developing locally we definitely don't want to accidentally replace our files with an update we released.

V. Hello World!

Now we need this empty module to do something.

Create a Javascript File

Add a scripts directory with a todo-list.js file to your module directory:

todo-list
├── module.json
├── scripts
│   └── todo-list.js
└── todo-list.lock

Add the following to your js file.

console.log('todo-list | Hello World!');

Add this script to our Manifest

Foundry relies on the Manifest JSON telling it which files in the directory are relevant to the function of your module. To add a script, we'll need to define it under the "scripts" field:

  "scripts": [
    "scripts/todo-list.js"
  ],

Make sure your JSON is valid after adding this!

The path is a relative path from the manifest file itself, which is always in the root of the module directory.

Test!

Now that we've changed our Manifest JSON, we have to restart Foundry again. Open a world with any system (I recommend using the Simple Worldbuilding System) and activate the "To Don't" module.

Once Foundry is done refreshing, we can open our devtools (try F12 or CMD + Option + i) and see our Hello World in the Console!

Image showing the console log of our Hello World!

VI. Setting up the rest of our files.

We know we'll need a bunch of other files before the end of this project so we're going to go ahead and create them all now. We'll revisit what each file does later in this tutorial but if we can set our Manifest up now, we won't have to restart Foundry later.

Localization

It's important to start thinking about localization early in the project, as it is much easier to do it as you go, rather than go back through at the end of a project.

Add a languages directory and en.json:

{ "TODO-LIST": { } }

Register this language in your Manifest:

  "languages": [
    {
      "lang": "en",
      "name": "English",
      "path": "languages/en.json"
    }
  ],

Stylesheets

Create a styles directory and a todo-list.css, then register it in your Manifest:

  "styles": [
    "styles/todo-list.css"
  ],

Templates

Foundry uses Handlebars as a template language. We'll be needing one template and we might as well make it now.

Add a todo-list.hbs to a templates directory in your module directory.

We don't need to make any changes to the Manifest for templates.

VII. Wrapping Up

Once you've added all of these files and directories, restart foundry one last time before moving on.

So far we have:

  • Created a directory for our module to live in
  • Added a Manifest JSON to register our module
  • Debugged why our Module wasn't registering
  • Added a js file and logged something within Foundry
  • Created the skeleton for the rest of our files
Final Manifest JSON

The order of the keys does not matter, I used alphabetical order for simplicity sake.

{ "authors": [ { "name": "You!" } ], "description": "A simple module which adds a user specific to do list.", "languages": [ { "lang": "en", "name": "English", "path": "languages/en.json" } ], "minimumCoreVersion": "0.8.8", "name": "todo-list", "scripts": [ "scripts/todo-list.js" ], "styles": [ "styles/todo-list.css" ], "title": "To Don't", "version": "1" }
Final Directory Tree
todo-list
├── languages
│   └── en.json
├── module.json
├── scripts
│   └── todo-list.js
├── styles
│   └── todo-list.css
├── templates
│   └── todo-list.hbs
└── todo-list.lock

Quick Tip

The League of Extraordinary Foundry VTT Developers has created an Open Source template repository on Github which helps bootstrap the steps we went through in this part of the tutorial. It also has some other nicities built-in including an automation for releases.

Next Step: Planning our Module


License Information & Attribution

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution-ShareAlike 4.0 License, and code samples are licensed under the Unlicense (text below).

Provide attribution to Andrew Krigline, aka "Calego".

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.

In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to http://unlicense.org/


Planning our Module

tags: module-tutorial foundry-vtt

Let's take a step back and think about the structure of what our todo-list module should look like when it's done.

I. ESModules vs Scripts

There are two ways we can tell Foundry Core about a module's javascript files in its Manifest: "scripts" and "esmodules". There's a lot of philosophical difference between these approaches but we're going to focus on the practical here.

Practically speaking, the difference comes down to the scope in which your script is run.

Programming Concept: Scope

Think of "scope" in this context like a room in an office building where a script is allowed to work.

Scripts

Since the beginning of javascript, scripts have been loaded all together and all at once and expected to co-exist in a single global 'scope'.

Putting all of the scripts in the same scope has both benefits and drawbacks.

  • For simple cases (like a website which needs only 1 or 2 scripts) it's simpler and faster.
  • For cases where scripts want to work together, working in the same scope is easy.
  • The more scripts you add, the more likely a conflict will arise where Script F overrides something Script B was using, etc.

Office Example: When there's only one or two people working together in a room things work great. They can share a whiteboard, table space, etc. without getting in eachother's way.

When there's a lot of people working together in a single room, things get complicated. Conflicts might arise where multiple individuals need the whiteboard or the table at once. And that one person comes along who starts eating other peoples' lunches.

ES Modules

Recently there has been a shift in javascript development away from putting all scripts in the same scope and instead introducing "modules" which can import eachother to work together. A single "module" in this context is a single "script." Each "script" in this setup works within its own scope with no overlap between them.

This solves a lot of problems, but also has some drawbacks:

  • For simple cases, this is probably overkill.
  • For cases where scripts want to work together, they must explicitly define which parts of themselves are exported to be importable by other scripts.
  • It doesn't matter how many scripts you add, they will never conflict with eachother.

Office Example: ES Modules are like cubicles. The office room got so hectic that to make things easier, every person gets their own mini-room within the larger room in which to work.

When someone wants to interact with another person, they have to go to that other person and ask for stuff from the cubicle entrace. Only the things that the person inside the cubicle wants to can leave it.

There's a lot more to unpack about this topic and I'll refer you to this article if you want to know more.

Why Scripts?

I chose to leverage scripts for this project because it is a simpler concept to grasp.

Since we're not making an overly complex module, we're going to stick to scripts with some Object Oriented principles to help protect ourself and the other modules around us.

II. Object Oriented Javascript?

There's a lot of very philosophical talk out there among professional javascript developers on frontend, backend, and anywhere in-between about topics like this. Mozilla has an excellent tutorial going into more details if you are interested. I will leave you with why I chose to write this tutorial as following some basic Object Oriented principles:

  1. It's a way to keep your code safe from other peoples' code.

All of the modules active in a Foundry instance are sharing the same "scope." This can lead to unexpected problems if your module is installed alongside another module.

  1. This methodology draws inspiration from how Core is laid out.

By working in a way that echos how Core works, you can get a better feeling for how to read Core code when you need to.

The Basics

Everything you care about should be grouped into Classes. A Class governs one 'concern'. These Classes are namespaced in such a way that they are easy to find and differentiate.

Classes have methods/properties defined on their namespace. Some of these are static, which means they are accessible from outside the scope of the Class (most are not).

Programming Concept: Terminology

There's a lot of overlapping teminology in Javascript-land. It's easy to get lost when the words switch half-way through a tutorial and I'm sure I'll end up doing that here.

A method is a function which belongs to a Class (aka is a "property" of that Class).


III. Moving Parts

At the most fundamental level, our TODO module needs two things:

  • A place to persist our data in the database.
  • A UI to interact with that data.

We can break this task up and do things one at a time. We'll handle our data problem first, then make a UI later.

Programming Concept: Separation of Concerns

From a theoretical standpoint, how we store the data should not care about the user interface. We should instead build an API for our data 'layer' which can be used by any number of consumers.

For example, if done correctly a todo could be created, updated, or deleted by a macro, or by another module.


IV. Data Layer

There's a few ways modules can handle data in Foundry, which you use depends on your needs. Here's our assumptions:

  • Any user can create, edit, or delete a todo.
  • A todo can only be viewed/edited/deleted by its creator.
  • A todo should follow the user to whichever machine they log into.

A 'client' scoped setting almost fills these requirements and is a good option. However, since we want these todos to follow the user when the log in on a different machine, that won't work. Instead, we'll use a flag on the User Document to store todos.

What is a ToDo?

/** * A single ToDo in our list of Todos. * @typedef {Object} ToDo * @property {string} id - A unique ID to identify this todo. * @property {string} label - The text of the todo. * @property {boolean} isDone - Marks whether the todo is done. * @property {string} userId - The user's id which owns this ToDo. */

This is a jsdoc @typedef which defines the structure of our ToDo object. It notes that the ToDo is an object with four keys (id, label, isDone, userId) as well as what each of these do. An IDE like Visual Studio Code has built-in tooling to make use of this kind of in-code documentation.

What do we need to be able to do with it?

Description Operation
User should be able to make new ToDos Create
User should be able to view one of their ToDos Read
An existing ToDo's label should be editable Edit Label
An existing ToDo should be mark-able as "Done" Toggle Done
User should be able to delete ToDos. Delete

Programming Concept: CRUD

Create, Read, Update, and Delete is often abbreviated as simply "CRUD" In our table of operations above, "Edit Label" and "Toggle Done" are both really the same data operation: "Update and existing ToDo"


V. Recap

We want a User Interface which allows Users to Create, Read, Update, and Delete ToDos which are objects consisting of some Text for a label and a Boolean for isDone stored on User flags.

Next Step: Setting Up


License Information & Attribution

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution-ShareAlike 4.0 License, and code samples are licensed under the Unlicense (text below).

Provide attribution to Andrew Krigline, aka "Calego".

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.

In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to http://unlicense.org/


Setting Up

tags: module-tutorial foundry-vtt

There's a bit of up-front work we can do to make our lives easier going forward.

I. Create a Class for our Module

There's some base logic and constants which we can define in a central location that will make our lives easier.

In our todo-list.js file, we should define a ToDoList class:

class ToDoList { static ID = 'todo-list'; static FLAGS = { TODOS: 'todos' } static TEMPLATES = { TODOLIST: `modules/${this.ID}/templates/todo-list.hbs` } }

Programming Concept: static

You'll notice we're putting static in front of each of these things. Classes have two kinds of properties: static and not static Marking a property as static basically means that it can be accessed by functions outside the class where it is defined.

ID

There's a few places where ID will be useful in our module's code:

  • Getting and Setting Flags
  • Getting and Setting Settings
  • Registering our Templates

Programming Concept: Constant

Generically, a constant is something which should never change. Our module's id is an example of this. It's convention to define these with all caps names.

FLAGS

This little object is where we define what flags our module is going to use when we try to get or set flags later while we're setting up our data layer.

Since we use this 'todos' string in several places, we're hoping that by defining it in this little object, we can avoid bugs caused by typos later.

TEMPLATES

Similar to our FLAGS, this is where we define which handlebars template files our project includes. Again we hope to avoid typos by defining it in object form, which we will then re-use later while we're creating our UI.

II. Create our ToDo typedef

We're going to be using JSDoc to document our code as we go during this tutorial. This is entirely optional but is a very nice thing to do for future us as you return later to try and read your old code.

There's a concept of a typedef in JSDoc which lets us define a complicated type which can be reused. Since we already know the shape of a single ToDo, we might as well define it.

/** * A single ToDo in our list of Todos. * @typedef {Object} ToDo * @property {string} id - A unique ID to identify this todo. * @property {string} label - The text of the todo. * @property {boolean} isDone - Marks whether the todo is done. * @property {string} userId - The user who owns this todo. */

III. [Optional] Register a DevMode debug flag

Installing and using the Developer Mode module will allow us to have a few quality of life tools as we move forward. One of these is a way to register a debug flag that ensures we won't release code that has debug logs enabled.

Add a log helper to ToDoList

static log(force, ...args) { const shouldLog = force || game.modules.get('_dev-mode')?.api?.getPackageDebugValue(this.ID); if (shouldLog) { console.log(this.ID, '|', ...args); } }

Register the module with Developer Mode

Developer Mode uses a custom hook to serve an API to other modules which allows them to register as soon as possible.

Hooks.once('devModeReady', ({ registerPackageDebugFlag }) => { registerPackageDebugFlag(ToDoList.ID); });

Using the log helper

Anytime you want to log something to the console, instead of doing console.log('todo-list | ', 'foo') and risking that be in a release, do ToDoList.log(false, 'foo') instead.

IV. Wrapping Up

We've set up some things for our convienience in the next steps.

  • Created a namespaced class for our module's general information to protect ourselves from other modules (and them from us).
  • Added a typedef for our ToDo object.
  • Created a logger helper to use Developer Mode's features.
Our Script so Far
/** * A single ToDo in our list of Todos. * @typedef {Object} ToDo * @property {string} id - A unique ID to identify this todo. * @property {string} label - The text of the todo. * @property {boolean} isDone - Marks whether the todo is done. * @property {string} userId - The user who owns this todo. */ /** * A class which holds some constants for todo-list */ class ToDoList { static ID = 'todo-list'; static FLAGS = { TODOS: 'todos' } static TEMPLATES = { TODOLIST: `modules/${this.ID}/templates/todo-list.hbs` } /** * A small helper function which leverages developer mode flags to gate debug logs. * * @param {boolean} force - forces the log even if the debug flag is not on * @param {...any} args - what to log */ static log(force, ...args) { const shouldLog = force || game.modules.get('_dev-mode')?.api?.getPackageDebugValue(this.ID); if (shouldLog) { console.log(this.ID, '|', ...args); } } } /** * Register our module's debug flag with developer mode's custom hook */ Hooks.once('devModeReady', ({ registerPackageDebugFlag }) => { registerPackageDebugFlag(ToDoList.ID); });

Next Step: Writing the Data Layer


License Information & Attribution

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution-ShareAlike 4.0 License, and code samples are licensed under the Unlicense (text below).

Provide attribution to Andrew Krigline, aka "Calego".

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.

In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to http://unlicense.org/


Writing the Data Layer

tags: module-tutorial foundry-vtt

This is the basic shape of what we're going to end up with.

class ToDoListData { // all todos for all users static get allToDos() {} // get all todos for a given user static getToDosForUser(userId) {} // create a new todo for a given user static createToDo(userId, toDoData) {} // update a specific todo by id with the provided updateData static updateToDo(todoId, updateData) {} // delete a specific todo by id static deleteToDo(todoId) {} }

Our "Data Layer" should be a self-contained class which has methods and properties for accessing and editing our ToDos. In our scripts/todo-list.js file, define a new class: ToDoListData.

class ToDoListData { }

I. Read

At most basic, we need a method which accepts a userId and returns our toDos flag.

Since we decided to put ToDos as flags on the User document, we need to do two things in this method:

  1. Get the appropriate user from the game.users Collection.
  2. Leverage User#getFlag, providing our module as scope, and our flag name as key.
static getToDosForUser(userId) { return game.users.get(userId)?.getFlag(ToDoList.ID, ToDoList.FLAGS.TODOS); }

II. Create

Creating a ToDo is very similar to the process for reading them. We need a method which accepts a userId and a new ToDo, which then adds the new ToDo to the object on the User flag.

static createToDo(userId, toDoData) { // generate a random id for this new ToDo and populate the userId const newToDo = { isDone: false, ...toDoData, id: foundry.utils.randomID(16), userId, } // construct the update to insert the new ToDo const newToDos = { [newToDo.id]: newToDo } // update the database with the new ToDos return game.users.get(userId)?.setFlag(ToDoList.id, ToDoList.FLAGS.TODOS, newToDos); }

Test!

Now that we have both Create and Read API methods defined, we can test things from the browser console.

ToDoListData.createToDo(game.userId, {label: 'Foo'});

Uncaught (in promise) Error: Invalid scope for flag todos

Debugging "Invalid scope"

"Invalid scope" is what Foundry Core complains about when we try to set or get a flag using a 'scope' argument (the first argument in Document#setFlag) which does not match an active module's name.

Since our module's name is "todo-list", when we User#setFlag we must use setFlag('todo-list'...).

This is why we defined ToDoList.ID as todo-list. By setting this up as a constant, we can make sure we avoid typos and it'll be easy enough to change the name down the line if we need to.

Unfortunately we were still able to introduce a typo at the end of our create method:

game.users.get(userId)?.setFlag(ToDoList.id, ToDoList.FLAGS.TODOS, newToDos);

We accidentally used ToDoList.id instead of ToDoList.ID.

Fix that up, refresh Foundry and try again!

III. Update

Updating an existing ToDo is a little tricky. Ideally we would like to only need to provide the ToDo's id and the update data. But since all our ToDos are split up and stored on different individual Users, we currently would need to provide both userId and ToDo id to update any given ToDo.

allToDos

To fix this, we would need some mapping of ToDo id to User id. Since our ToDo object has userId on it already, this could be accomplished with a big object containing all ToDos for all Users indexed by ToDo id.

We'll leverage reduce for this inside a getter.

static get allToDos() { const allToDos = game.users.reduce((accumulator, user) => { const userTodos = this.getToDosForUser(user.id); return { ...accumulator, ...userTodos } }, {}); return allToDos; }

updateToDo

Now that we have a way to lookup which User a particular ToDo belongs to we can make an update method that accepts only ToDo id.

static updateToDo(toDoId, updateData) { const relevantToDo = this.allToDos[toDoId]; // construct the update to send const update = { [toDoId]: updateData } // update the database with the updated ToDo list return game.users.get(relevantToDo.userId)?.setFlag(ToDoList.ID, ToDoList.FLAGS.TODOS, update); }

Test!

Let's try to edit a ToDo we've already created.

First, use ToDoListData.allToDos to see all of the ToDos we have available. If there are none, use ToDoListData.create to make one.

ToDoListData.updateToDo("56r58vm2v197prr8", { label: 'bar' });

Image of console showing Success!

Success!

IV. Delete

The last piece to our puzzle for an API which handles ToDos is removing one. Deletion of data is a special case in the Foundry data interaction playbook. At first glance it's as simple as setting our flag to be an object without a specific key in it, but this doesn't actually work how you might expect.

Foundry's Document#setFlag is an abstraction around Document#update. As such, it does not treat an undefined object or key as something which should be deleted. Instead, there is a specific syntax which must be used when we want to tell Foundry's database to delete a key.

someDocument.update({ ['-=someKey']: null })

We can abstract that away behind our method.

static deleteToDo(toDoId) { const relevantToDo = this.allToDos[toDoId]; // Foundry specific syntax required to delete a key from a persisted object in the database const keyDeletion = { [`-=${toDoId}`]: null } // update the database with the updated ToDo list return game.users.get(relevantToDo.userId)?.setFlag(ToDoList.ID, ToDoList.FLAGS.TODOS, keyDeletion); }

Test!

Same as before, get a todo from ToDoListData.allToDos and then use our new method to delete it.

ToDoListData.deleteToDo('56r58vm2v197prr8');

Image of the console showing our successful deletion of a ToDo.

Success!

V. Bulk Update

There's one last thing we will want to be able to do and that's edit many todos at once. Effectively, we want to be able to handle a bunch of updateToDos with one call.

To accomplish this, we will make a thin wrapper around the setFlag method we use in updateToDo just as a convience method:

static updateUserToDos(userId, updateData) { return game.users.get(userId)?.setFlag(ToDoList.ID, ToDoList.FLAGS.TODOS, updateData); }

Test!

Similar to the previous updateToDo, but we can affect several toDos at once, as long as they are on the same user.

ToDoListData.updateUserToDos('acfkPFqSPy01uJ3p', {
  "gogdv4qvgydcr7sh": { isDone: true },
  "yndgcuoq147g37nz": { isDone: true },
});

Image of the console showing our successful update of several ToDos.

Success!

V. Wrapping Up

We've powered through and written a class to help us handle our ToDo Data. This "ToDo API" is an abstraction on top of Foundry's own abstracted flags methods, but it will allow both us and others an easy way to interact with ToDos specifically.

Final ToDoListData class
/** * The data layer for our todo-list module */ class ToDoListData { /** * get all toDos for all users indexed by the todo's id */ static get allToDos() { const allToDos = game.users.reduce((accumulator, user) => { const userTodos = this.getToDosForUser(user.id); return { ...accumulator, ...userTodos } }, {}); return allToDos; } /** * Gets all of a given user's ToDos * * @param {string} userId - id of the user whose ToDos to return * @returns {Record<string, ToDo> | undefined} */ static getToDosForUser(userId) { return game.users.get(userId)?.getFlag(ToDoList.ID, ToDoList.FLAGS.TODOS); } /** * * @param {string} userId - id of the user to add this ToDo to * @param {Partial<ToDo>} toDoData - the ToDo data to use */ static createToDo(userId, toDoData) { // generate a random id for this new ToDo and populate the userId const newToDo = { isDone: false, label: '', ...toDoData, id: foundry.utils.randomID(16), userId, } // construct the update to insert the new ToDo const newToDos = { [newToDo.id]: newToDo } // update the database with the new ToDos return game.users.get(userId)?.setFlag(ToDoList.ID, ToDoList.FLAGS.TODOS, newToDos); } /** * Updates a given ToDo with the provided data. * * @param {string} toDoId - id of the ToDo to update * @param {Partial<ToDo>} updateData - changes to be persisted */ static updateToDo(toDoId, updateData) { const relevantToDo = this.allToDos[toDoId]; // construct the update to send const update = { [toDoId]: updateData } // update the database with the updated ToDo list return game.users.get(relevantToDo.userId)?.setFlag(ToDoList.ID, ToDoList.FLAGS.TODOS, update); } /** * Deletes a given ToDo * * @param {string} toDoId - id of the ToDo to delete */ static deleteToDo(toDoId) { const relevantToDo = this.allToDos[toDoId]; // Foundry specific syntax required to delete a key from a persisted object in the database const keyDeletion = { [`-=${toDoId}`]: null } // update the database with the updated ToDo list return game.users.get(relevantToDo.userId)?.setFlag(ToDoList.ID, ToDoList.FLAGS.TODOS, keyDeletion); } /** * Updates the given user's ToDos with the provided updateData. This is * useful for updating a single user's ToDos in bulk. * * @param {string} userId - user whose todos we are updating * @param {object} updateData - data passed to setFlag * @returns */ static updateUserToDos(userId, updateData) { return game.users.get(userId)?.setFlag(ToDoList.ID, ToDoList.FLAGS.TODOS, updateData); } }

Next Steps: Creating our UI


License Information & Attribution

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution-ShareAlike 4.0 License, and code samples are licensed under the Unlicense (text below).

Provide attribution to Andrew Krigline, aka "Calego".

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.

In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to http://unlicense.org/


Starting the UI: HTML Injection

tags: module-tutorial foundry-vtt

Planning the UI

Here's what we're hoping to end up with:

  1. Add a button to the Player List entry for our user which when clicked, opens a window with all of our user's todos.
  2. From that window, allow the user to create, edit, and delete ToDos.

I. Finding the right Hook

Foundry has a system of Hooks which allow modules to safely interact with events in the Foundry ecosystem.

One such hook happens when any given UI element is rendered. We should toggle Hook debugging on and take a look to see if one for the Player List exists. With the Developer Mode module, this is as simple as opening the Dev Mode Settings and enabling CONFIG.debug overrides, and then the Hooks debug flag.

With this log on and the console open, click on the player list size toggle. Watch as we interact with the UI element that there is a hook fired: renderPlayerList.

Image of the console showing renderPlayerList

We also see what arguments are being passed to the Hook as it's fired:

  1. A PlayerList class - we're not very interested in this
  2. Something that looks like an array of HTML elements - this is interesting to us
  3. Some other object - not very interesting to us

#2 here is the element being painted on the page inside a jQuery wrapper. If we can change that element while the hook is running, we can change what is rendered on the page.

How do we change the element?

Taking a look at the DOM of the player list, we can see it's pretty basic. There's a heading and a list. The list has list items inside it, and each list item has an attribute (which looks like it's a custom data attribute): data-user-id which looks like it records the ID of the user that list item represents.

We want to put a button on the player list item which corresponds to the logged in user, and we can use this data-user-id attribute to do so.

Bringing it all together

Our goal is clear at this point:

  1. Register a hook callback for renderPlayerList
  2. Modify that second argument in the callback to include a button
  3. Register a listener on that button to open a window

We'll have to come back to the whole "open a window" part, but we can at least get the button and listener working up front.

Programming Concept: Event Listener

An event listener is a fancy way to say "piece of code that reacts to something happening." In most cases, this 'something' is a user input like a mouse click or typing into a form element.

II. Hooking into renderPlayerList

We're going to use the jQuery bundled with Foundry Core to make our lives a little easier. We're also going to use the FontAwesome icons bundled with Foundry Core to label our button instead of text. There's a tasks icon which would be perfect for this.

In the todo-list.js file, after the ToDoList class definition, add this:

Hooks.on('renderPlayerList', (playerList, html) => { // find the element which has our logged in user's id const loggedInUserListItem = html.find(`[data-user-id="${game.userId}"]`) // insert a button at the end of this element loggedInUserListItem.append( "<button type='button' class='todo-list-icon-button'><i class='fas fa-tasks'></i></button>" ); });

Image showing the button we just added.

:partying-face:

III. Adding a Localized Tooltip

We should add a tooltip on this button so people don't wonder what it is, and we should ensure this tooltip is localized.

Add the title to our language file

Open up languages/en.json that we made a while ago and add a key to it: button-title.

{ "TODO-LIST": { "button-title": "Open To-Do list" } }

Apply localized string to button

Back in our renderPlayerList hook, we need to convert our button element string into a template literal to make use of Foundry Core's built-in i18n methods.

// create localized tooltip const tooltip = game.i18n.localize('TODOS.button-title'); // insert a button at the end of this element loggedInUserListItem.append( `<button type='button' class='todo-list-icon-button' title='${tooltip}'><i class='fas fa-tasks'></i></button>` );

Test!

Refresh and verify that when hovering over the button, the value we assigned in the languages file shows up.

Narrator. It does not.

Debugging Localization Problems

Instead of "Open To-Do list", we see the exact key we told Foundry to localize.

When this happens, it happens because Foundry Core can't find a key in its loaded localization files which matches. Most often, this is because of a typo; we should double check our JSON.

Our button-title key is nested within an object with the key TODO-LIST. We asked Foundry to find TODOS.button-title, instead of TODO-LIST.button-title, and that's why it failed.

Quick fix and test again.

const tooltip = game.i18n.localize('TODO-LIST.button-title');

That seems to have fixed it! Now to make things look pretty.

III. Styling our Button

There's a lot of room for improvement on how our button looks right now.

It's too large.

Inspecting the button, we can see that it's parent element has some classes applied to it, one of which is flexrow. This is a utility class in Foundry Core's style.css which turns the element into a Flexbox row.

Looking at our injected button we can see that since it is a child of a .flexrow element, it gains the style rule: flex: 1. Without going into too much detail, this means the button will grow as much as it can in the space available.

We want the opposite, instead the button should never grow.

Luckily, there are some other utility classes in Foundry's style.css which we can use. Apply flex0 to our button and refresh.

loggedInUserListItem.append( `<button type='button' class='todo-list-icon-button flex0' title='${tooltip}'><i class='fas fa-tasks'></i></button>` );

Image of the icon button

Better already. For the next steps, we'll need to open our own styles/todo-list.css file.

Make it look "better"

Getting caught up in the intricacies of CSS will only make this tutorial longer, so here's a CSS snippet that will make it look 'better'.

This works because we applied a class (.todo-list-icon-button) to our injected button.

.todo-list-icon-button { background: transparent; padding: 0; color: white; line-height: normal; border: 0; align-self: center; } .todo-list-icon-button > i { margin-right: 0; } .todo-list-icon-button:hover, .todo-list-icon-button:focus { box-shadow: none; text-shadow: 0 0 5px red; }
Summary of Changes
  • Remove the background, border, and padding from the button
  • Change the color of the button to be white
  • Remove some defaults that apply to icons in buttons
  • Improve the alignment of the button in the row
  • Replace the hover and focus effects to be text-shadows instead of box-shadows

Image of the styled button

IV. Add an event listener to our button

The last step with the button (for now) is to make it do something. We're not picky for now.

We will be using jQuery's on method to make our lives easier here. Within the callback in the renderPlayerList hook, add this:

html.on('click', '.todo-list-icon-button', (event) => { ToDoList.log(true, 'Button Clicked!'); });

Note I'm using the log helper we created during the optional step of Setting Up. If you chose not to do this step, you would instead do something with the default console.log.

With this in place, we can refresh and try clicking on the button while watching the console.

Image showing the Button Clicked log message.

Perfect.

V. Wrapping Up

In this section we

  • Learned about hooks and how to find them.
  • Registered a Hook callback.
  • Injected some HTML into a Core element with that Hook callback.
  • Localized a string with Foundry Core's i18n implementation.
  • Debugged why our localization string wasn't displaying properly.
  • Styled our injected HTML with Foundry Core provided classes.
  • Further styled our element with custom CSS in our stylesheet file.
  • Added an event listener to make the button interactable.
Final renderPlayerList Hook
Hooks.on('renderPlayerList', (playerList, html) => { // find the element which has our logged in user's id const loggedInUserListItem = html.find(`[data-user-id="${game.userId}"]`) // create localized tooltip const tooltip = game.i18n.localize('TODO-LIST.button-title'); // insert a button at the end of this element loggedInUserListItem.append( `<button type='button' class='todo-list-icon-button flex0' title="${tooltip}"> <i class='fas fa-tasks'></i> </button>` ); // register an event listener for this button html.on('click', '.todo-list-icon-button', (event) => { ToDoList.log(true, 'Button Clicked!'); }); });
Final todo-list.css
.todo-list-icon-button { background: transparent; padding: 0; color: white; line-height: normal; border: 0; align-self: center; } .todo-list-icon-button > i { margin-right: 0; } .todo-list-icon-button:hover, .todo-list-icon-button:focus { box-shadow: none; text-shadow: 0 0 5px red; }

Next Step: Making a FormApplication


License Information & Attribution

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution-ShareAlike 4.0 License, and code samples are licensed under the Unlicense (text below).

Provide attribution to Andrew Krigline, aka "Calego".

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.

In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to http://unlicense.org/


Making a FormApplication

tags: module-tutorial foundry-vtt

Our Module has an API for our data layer and we are injecting a button into an existing UI element. The last (large) piece of the puzzle is our new UI for interacting with ToDo lists.

To make this we'll need three things:

  1. A FormApplication subclass which gathers our data for a Template, and which handles the logic for interacting with that template.
  2. A handlebars template which displays the information and controls that the FormApplication gathers/handles.
  3. Styling, because what's the point if it's not pretty?

It's easiest to work in steps, so let's start by making a rough window which displays the current ToDos for the logged in user. We'll get around to adding and editing them next.

I. FormApplication subclass

Foundry Core provides a rudimentary UI framework to make interacting with data via UI easier for itself and modules. The FormApplication class is the most suitable of the tools available to us for what we are looking at doing, and so we need to make our own subclass of it.

Programming Concept: Subclass and Inheritance

ES2020 (a fancy way to say 'modern javascript') classes have the ability to 'extend' other classes, 'inheriting' the properties and methods from the other class.

There's a lot of nuance explained in detail in other guides, but in short if Class Foo extends Class Bar, all of Class Bar's methods and properties are available on Class Foo.

class ToDoListConfig extends FormApplication { }

defaultOptions

First step in setting up our ToDoListConfig class is to give it some overrides to the FormApplication's defaultOptions getter.

static get defaultOptions() { const defaults = super.defaultOptions; const overrides = { height: 'auto', id: 'todo-list', template: ToDoList.TEMPLATES.TODOLIST, title: 'To Do List', userId: game.userId, }; const mergedOptions = foundry.utils.mergeObject(defaults, overrides); return mergedOptions; }

Programming Concept: super

Like everything else, there's nuance to this. You can think of super as "the parent class". In this example, we're getting the defaultOptions of the parent class (FormApplication) since we know there's some good stuff in there and we don't want to override all of it.

We have some options we know our FormApplication should always have:

  1. It should calculate its own height.
  2. It has the id todo-list.
  3. It uses the handlebars template we defined when we were Setting Up our module's ToDoList class.
  4. It displays the title "To Do List."

Lastly, there is one additional option we are defining that is not one of the normal options for a FormApplication: userId. This is the userId whose ToDos we will display with this FormApplication. We are setting it by default to be the logged in user, but doing it this way leaves it possible to render a different user's ToDo List if we desire to later.

Additionally, we know we want all of the existing defaults from FormApplication, and we only want to override a the few we have defined.

mergeObject

This is a utility function that Foundry Core provides. It allows one to merge two objects like a zipper, with some logic about which definition should overwrite the other in the returned object.

getData

Secondly, we need to override the getData method of our FormApplication to gather the data for our specific template. For our purposes, this is delightfully simple:

getData(options) { return { todos: ToDoListData.getToDosForUser(options.userId) } }

Notice how we're relying on the userId passed to options here. These options are the same options we defined the defaults for above. They also are the options passed to FormApplication#render's second argument.

And that's it! Now we need the todo-list.hbs template to display something.

II. Handlebars Template

Handlebars is the templating engine which powers all of the UI in Foundry Core. It is very simplistic in nature which makes it easy to pick up but sometimes hard to work with.

Our ToDo list needs to display two pieces of information for each ToDo: title and isDone. We're going to go super simple initially and make the whole thing an unordered list.

<ul> {{#each todo-list}} <li>{{label}} | Done: {{isDone}}</li> {{/each}} </ul>

Test!

With our ToDoListConfig defined and our basic Template created, we should be able to see something in-foundry.

We need to manually call the render method of an instance of ToDoListConfig for now since we haven't made our button work. We also need to give that render method an argument of true to force it to paint on the page.

new ToDoListConfig().render(true, { userId: game.userId })

Paste that on into your console and everything should work!

Narrator: It did not work. Well I mean it kind of worked.

A window popped up but we also got a console error, so half credit?

Debugging Cannot set property 'onsubmit' of undefined

TypeError: An error occurred while rendering ToDoListConfig 27: Cannot set property 'onsubmit' of undefined

This is a gotchya when using a FormApplication that is just something you have to know about. In the documentation for FormApplication it details some of the assumptions the class makes, the second of which is:

  1. The template used contains one (and only one) HTML form as it's outer-most element

Our outermost element is a <ul>, not a <form>.

Easy to fix, then lets refresh and try again.

<form> <ul> {{#each todo-list}} <li>{{label}} | Done: {{isDone}}</li> {{/each}} </ul> </form>

Narrator: It still did not work.

Debugging our Empty Window

Is our DOM there at all?

Open DevTools and inspect the popped up window with this tool:

When inspecting the DOM of our window, I see:

<section class="window-content">
  <form>
    <ul>
    </ul>
  </form>
</section>

The section element comes from Foundry Core and everything inside it is from our Template. So that's good, our template is taking effect and we can see our elements. Something must be wrong with our #each.

Is our handlebars getting the data we expect?

There's a number of built-in helpers in handlebars. One of these is {{log}} which acts as a console.log.

Let's go ahead and put a one of those at the top of our template file to see what the data getting to the template looks like.

{{log 'todo-list-template' this}} <form> ...

Image showing the todo-list template data

Ah. There's the problem. We are feeding the template the data in the form of todos (from our ToDoListConfig#getData method), but in our template, we expected to see todo-list.

Simple change to our template and now surely it will work (remember to take out the log).

  <ul>
    {{#each todos}}
      <li>{{label}} | Done: {{isDone}}</li>
    {{/each}}
  </ul>

Image showing our todo list

Success!

III. Adding buttons and inputs

Now that we have things displaying, let's add some buttons, inputs, and css classes to make things look more like our end-game.

<form>
  <ul class="todo-list flexcol">
    {{#each todos}}
    <li class="flexrow" data-todo-id="{{id}}">
      <input type="checkbox" name="{{id}}.isDone" title="Mark Done" data-dtype="Boolean" {{checked isDone}} />

      <input type="text" value="{{label}}" name="{{id}}.label" data-dtype="String" />

      <button type="button" title="Delete To Do" data-action="delete" class="flex0 todo-list-icon-button">
        <i class="fas fa-trash"></i>
      </button>
    </li>
    {{/each}}
  </ul>

  <button type="button" data-action="create"><i class="fas fa-plus"></i> Add To Do</button>
</form>

Styling

We'll use flexrow and flexcol as some more of those built-in Foundry Core classes to make things line up nicely.

Notice we're also re-using our .todo-list-icon-button style to make the button at the end have the same style as the one we injected into the Player List.

Here's the CSS to style those classes.

.todo-list { padding: 0; margin-top: 0; gap: 1em; } .todo-list > li { align-items: center; gap: 0.5em; }

Functionality notes

How do the inputs know what value to display?

Simply put, we're telling them the values from the object populating the template (the one we defined in ToDoListConfig#getData).

  • the checkbox input uses a built in helper checked to display the proper state of the ToDo based on the isDone property
  • the label input uses value="{{label}}" to know what its contents should be

What's this name for?

When we make the inputs actually edit things, the name will be how we tell Foundry which property to edit. To make life easier for future us, we're setting this to be the id of the ToDo plus the property we're displaying/editing.

What are these custom data attributes for?

On the inputs, we define data-dtype to help Foundry know what kind of data to expect from this input. This isn't relevant right now but will be in our next step when we make this form actually do something.

On the buttons, we define data-action to help future us know what the button is supposed to do when clicked. This isn't something Foundry Core does for us, we'll be defining this behavior in the next step.

IV. Localization in Handlebars

The keen eyed among you will have noticed that we have made a blunder. We've got hard-coded English language strings in our UI. If someone speaking a different language wanted to translate our module, they would have an exceptionally difficult time doing so.

Instead, we need to leverage Foundry's i18n implementation like we did while Injecting a Button. Foundry Core provides a Handlebars Helper for localization which makes this easy to do from inside a template.

Add these to our languages/en.json file so they're available. Don't forget to ensure your JSON is valid after adding these.

"add-todo": "Add To-Do", "delete-todo": "Delete To-Do", "mark-done": "Mark Done"

Next use the localize helper in our Handlebars Template instead of our hardcoded strings.

      <input ... title="{{localize "TODO-LIST.mark-done"}}"

      // ...

      <button ... title="{{localize "TODO-LIST.delete-todo"}}" 
      
  // ...
      
  <button ... {{localize "TODO-LIST.add-todo"}}</button>

V. Wrapping Up

If you've refreshed and tested this recently, you've seen that we're looking good. We have everything set up and ready for interactivty.

Image showing our styled to-do list

In this section we:

  • Learned about FormApplications, Subclasses, and Inheritence in ES2020
  • Set up our own ToDoListConfig subclass of FormApplication with:
    • customized defaultOptions
    • a getData method which populates the ToDos for a given user
  • Created a Handlebars template which displays a list of ToDos
  • Debugged why that template wasn't actually displaying anything initially
  • Added some input elements, buttons, and localization to said template
Full ToDoListConfig class
class ToDoListConfig extends FormApplication { static get defaultOptions() { const defaults = super.defaultOptions; const overrides = { closeOnSubmit: false, height: 'auto', id: 'todo-list', submitOnChange: true, template: ToDoList.TEMPLATES.TODOLIST, title: 'To Do List', userId: game.userId, }; const mergedOptions = foundry.utils.mergeObject(defaults, overrides); return mergedOptions; } getData(options) { return { todos: ToDoListData.getToDosForUser(options.userId) } } }
Full todo-list.hbs
<form> <ul class="todo-list flexcol"> {{#each todos}} <li class="flexrow" data-todo-id="{{id}}"> <input type="checkbox" name="{{id}}.isDone" title="{{localize "TODO-LIST.mark-done"}}" data-dtype="Boolean" {{checked isDone}} /> <input type="text" value="{{label}}" name="{{id}}.label" data-dtype="String" /> <button type="button" title="{{localize "TODO-LIST.delete-todo"}}" data-action="delete" class="flex0 todo-list-icon-button"> <i class="fas fa-trash"></i> </button> </li> {{/each}} </ul> <button type="button" data-action="create"><i class="fas fa-plus"></i> {{localize "TODO-LIST.add-todo"}}</button> </form>
Full en.json
{ "TODO-LIST": { "button-title": "Open To-Do list", "add-todo": "Add To-Do", "delete-todo": "Delete To-Do", "mark-done": "Mark Done" } }

Next Step: Interactivity


License Information & Attribution

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution-ShareAlike 4.0 License, and code samples are licensed under the Unlicense (text below).

Provide attribution to Andrew Krigline, aka "Calego".

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.

In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to http://unlicense.org/


Interactivity

tags: module-tutorial foundry-vtt

So far we've created a data layer, injected a non-functioning button, and created a FormApplication which only displays ToDos but does not edit them.

It's time to bring it all together with some interactivity.

I. Make the button open our ToDoListConfig

There's two things we need to do to make ToDoListConfig openable. First we need to instanciate it, which is a fancy way of saying "create in memory." So far we've defined ToDoListConfig but we haven't told the browser to actually use it.

ToDoList.initialize

Back in our ToDoList class, create an initialize static method:

static initialize() { this.toDoListConfig = new ToDoListConfig(); }

What we're doing here is creating a new static property of our ToDoList class named toDoListConfig. This is defined as a 'new' instance of our ToDoListConfig class.

init

There's a Hook that Foundry Core calls when it is ready for modules to start registering things like settings. We don't need settings yet, but we can use this init hook to run our own initialize method.

Outside our ToDoList class, register an init hook callback:

Hooks.once('init', () => { ToDoList.initialize(); });

Make the button work

Since ToDoListConfig is a subclass of FormApplication, we know that to make it appear, we need to call its render method, with true as the first argument.

We also set up ToDoListConfig to accept a second argument in the render method which can define which userId's ToDo list to display.

Inside our renderPlayerList hook callback we created an event listener which simply logs when a user clicks the button. Let's replace that log with something which A) gets the user row clicked on, and B) opens the ToDoListConfig with that user's list.

A. Get the right userId

We'll be using jQuery's parents method to grab the player list item's data-user-id, which is put there by Foundry Core.

const userId = $(event.currentTarget).parents('[data-user-id]')?.data()?.userId;

This snippet takes the currentTarget (the button clicked), and goes up the DOM until it finds an element with data-user-id defined, then it gets that userId from the dataset of the element.

B. Render the instanciated ToDoListConfig

ToDoList.toDoListConfig.render(true, {userId});

Quite simply, take the toDoListConfig static property from our ToDoList class (which we defined during init), and call the render method, providing the userId we got in A.

All together now!

class ToDoList { // ... static initialize() { this.toDoListConfig = new ToDoListConfig(); } } // ... Hooks.once('init', () => { ToDoList.initialize(); }); // ... Hooks.on('renderPlayerList', (playerList, html) => { // ... // register an event listener for this button html.on('click', '.todo-list-icon-button', (event) => { const userId = $(event.currentTarget).parents('[data-user-id]')?.data()?.userId; ToDoList.toDoListConfig.render(true, {userId}); }); });
Success!

Animated GIF showing the ToDo list opening and closing.

II. Make the Form Fields Edit ToDos

Next we want to make our input elements actually persist the changes the user makes inside them.

ToDoListConfig.defaultOptions

There's two more overrides we should set with our ToDoListConfig's defaultOptions now that we're making it editable for real:

closeOnSubmit: false, // do not close when submitted submitOnChange: true, // submit when any input changes

With these two options, our FormApplication will start trying to call its _updateObject method any time an input element is changed. This specifically happens on the blur event (when a user stops focusing) for text fields and on the change event for checkboxes.

ToDoListConfig#_updateObject

Overriding the _updateObject method is how we define what "saving" the form means. The FormApplication class we inherit from does a lot behind the scenes which helps transform user input into our data format automatically.

async _updateObject(event, formData) { const expandedData = foundry.utils.expandObject(formData); await ToDoListData.updateUserToDos(this.options.userId, expandedData); this.render(); }

If that looks too easy, it's because we've done a lot before now to make it easy. Let's break that down.

Argument 2: formData

Go ahead and put a log into this method which prints formData:

async _updateObject(event, formData) { ToDoList.log(false, 'saving', { formData });

Refresh, Open the ToDoListConfig window and toggle a checkbox. In console you should see your log print an object that looks like the one below.

If you don't see any logs, make sure you have Developer Mode on, and you've turned on Debug Mode for "To Don't".

{ "v09cveb05p8gpcl1.isDone": true, "v09cveb05p8gpcl1.label": "Dee", "yndgcuoq147g37nz.isDone": false, "yndgcuoq147g37nz.label": "kolasa", "wkqgdz8obom5pgzx.isDone": false, "wkqgdz8obom5pgzx.label": "goop", "gogdv4qvgydcr7sh.isDone": true, "gogdv4qvgydcr7sh.label": "goopa" }

This object has keys which correlate to the name of every input in our form element and values which are the values of those inputs at the time of being saved. This is useful, but we can make it even more useful with another Foundry Core helper: expandObject.

expandObject

Move the log below our definition for expandedData and include that in the output.

async _updateObject(event, formData) { const expandedData = foundry.utils.expandObject(formData); ToDoList.log(false, 'saving', { formData, expandedData });

Now when things log you should see those keys and values expanded out into an object whose shape should look familiar.

{ "v09cveb05p8gpcl1": { "isDone": true, "label": "Dee" }, "yndgcuoq147g37nz": { "isDone": false, "label": "kolasa" }, "wkqgdz8obom5pgzx": { "isDone": false, "label": "goopsdf" }, "gogdv4qvgydcr7sh": { "isDone": true, "label": "goopa" } }

That's exactly what our ToDos look like when they're returned from ToDoListData.getToDosForUser. It also happens to be exactly what we need to give to ToDoListData.updateUserToDos to update a user's todos in bulk.

Updating the ToDos

Since we designed our Data Layer this way, our life is super easy here, we simply give our ToDoListData.updateUserToDos method the output of foundry.utils.expandObject(formData).

We also need to grab the userId off of the ToDoListConfig's options, which are convienently stored in this.options. Remember this is the id we defined when we opened the ToDoListConfig and that we used to populate the list in the first place.

Put all those moving parts together and it "just works." Foundry's FormApplication is an extremely powerful tool if you get used to working with it.

Test!

To test that all of this works, refresh your page, open the ToDoListConfig, and then make some changes. We haven't wired up the Delete or Add buttons yet, so the changes should only be to the inputs.

Once you've made changes close the ToDoListConfig and re-open it. If your changes persisted you're golden!

III. Make the Buttons work

Foundry Core's FormApplication has a lot of built-in logic to handle traditional form inputs, but when it comes to other interactive elements like our add and delete buttons, we have to do the work ourselves. We need to attach some more event listeners to our buttons.

activateListeners

The FormApplication#activateListeners method is intended to be overriden with our subclass's behavior. We'll again use jQuery's on method to do register our event listeners to make our lives easier here.

activateListeners(html) { html.on('click', "[data-action]", this._handleButtonClick); }

We're going start by attaching an event listener which triggers a seperately defined callback method when any element with a data-action attribute is clicked.

_handleButtonClick

To keep things tidy in our activateListeners method we're going to define a new method on our ToDoListConfig class which is where we'll put the callback to the button click event listener.

async _handleButtonClick(event) { const clickedElement = $(event.currentTarget); const action = clickedElement.data().action; const toDoId = clickedElement.parents('[data-todo-id]')?.data()?.todoId; ToDoList.log(false, 'Button Clicked!', {action, toDoId}); }

Programming Concept: Async

Some of the things we'll be doing in the upcoming steps require the ability to wait for Foundry's backend to finish before moving on. To allow that we'll be using async and await.

These are a convienent way to interact with javascript Promises, which are used extensively when dealing with operations that take time like a network request.

This is a huge topic with very in depth guides out there on the internet if you'd like more details.

From the event argument passed to our callback, we can get the element that was clicked with event.currentTarget. This target element should have a dataset on it, which in turn should have the action we defined with data-action="" in our Handlebars Template.

Additionally, one of the parents of the clicked element might have a data attribute which denotes the id of the relevant ToDo (the delete button).

Test!

We set data-action="create" and data-action="delete" for the add and the delete button respectively. So when we click on those buttons, we should see that being logged appropriately.

Image showing the console for Buttons being clicked.

Perfect! But there's a gotchya here. Test if the form inputs are still persisting. We didn't make any changes to the stuff we did before, so they should right?

Narrator: They do not.

Debugging Form Inputs that Don't Save

Even though none of the code we wrote to make the form inputs work correctly was touched, we did affect that code-path in a subtle way.

By overriding activateListeners we disabled all of Foundry's built-in listeners that save the form when an input changes. We want those listeners but we also want our own, so we need to use super again.

activateListeners(html) { super.activateListeners(html); html.on('click', "[data-action]", this._handleButtonClick); }

With super.activateListeners we are ensuring that the inherited method is still being used, but also adding some extras afterwards.

This is a super common mistake to make and one of the first things you should check if your form inputs worked before, but suddenly stopped working recently.

Using the action

Our activateListeners now has an event handler which knows which button was clicked, it's time to make each kind of button click do the right thing. We can use a switch statement to do this efficiently.

switch (action) { case 'create': { await ToDoListData.createToDo(this.options.userId); this.render(); break; } case 'delete': { await ToDoListData.deleteToDo(toDoId); this.render(); break; } default: ToDoList.log(false, 'Invalid action detected', action); }

A lot is going on here so let's break it down one case at a time.

case 'create'

When the action for the clicked button is 'create', we want to:

  1. Use our ToDoListData.createToDo method to create a blank new ToDo on the user whose list we are looking at.
  2. When that is done, Re-render the form.
  3. break, which prevents the rest of our switch from running.

The await is important here because without it, we would re-render the form before the Foundry Backend was done creating our ToDo.

case 'delete'

Similar to create, when the action for the button is 'delete' we want to:

  1. Use our ToDoListData.deleteToDo method to delete the appropriate ToDo.
  2. When that is done, Re-render the form.
  3. break, which prevents the rest of our switch from running.

Again the await is important because without it, we would re-render the form before the ToDo was done being deleted.

default

In case by some stroke of bad luck something other than create or delete was to trigger this flow, we want to do nothing (and log something).

Test!

You know the drill, refresh, open the ToDoListConfig and click the "Add To-Do" button. We should see a new blank ToDo pop into existence at the end of the list.

Narrator: We do not.

Uncaught (in promise) TypeError: Cannot read property 'userId' of undefined

If you were particularly adventurous and tried deleting a ToDo instead of adding one, you'd see a similar error complaining about render instead of userId.

Debugging Cannot read property X of undefined

This is a very common issue to run into and it happens when we tell javascript to read a property of an object which does not exist.

Example:
const foo = undefined; foo.bar(); // TypeError: Cannot read property 'bar' of undefined

In this particular case we are trying to read the userId property of something which isn't defined.

Image of the Error

The console gives us some useful information to help debug this.

  • What function is erroring? HTMLButtonElement._handleButtonClick
  • Which file is erroring? todo-list.js
  • What line of that file is erroring? :192

So taking a look at our file we can tell that this is what's tripping:

await ToDoListData.createToDo(this.options.userId);

To cause this error, this.options must be undefined. That's wierd, we don't even define this.options because it's part of Foundry Core's FormApplication logic. We also know it works because we use this.options.userId in other methods.

Let's try adding this to our log in the _handleButtonClick method.

ToDoList.log(false, 'Button Clicked!', { this: this, action, toDoId });

Image of the console log.

We expect this to be an instance of ToDoListConfig, but for some reason it's an HTML element button.

jQuery messes with this

We used jQuery to make our lives easier when we made our event listener. The on method jQuery provides overrides this in the provided callback to be the element clicked. That's useful for a lot of reasons, but not what we want here.

To fix this, we need to explicitly bind this when we give on its callback.

html.on('click', "[data-action]", this._handleButtonClick.bind(this));

Doing so ensures that this remains the ToDoListConfig in the callback method.

Once more, with feeling!

Image displaying the new log of button click.

Success!

Animated GIF showing items in the ToDo list being created and deleted.

V. Wrapping Up

We accomplished a lot in this section:

  • Initialized our ToDoListConfig
  • Made our injected button open said ToDoListConfig window
  • Learned about FormApplication._updateObject and how to make data persist on edit
  • Made our form fields persist edited data
  • Learned about FormApplication.activateListeners and how to use it to make buttons work
  • Debugged why a custom activateListeners method stopped our data persistence
  • Learned how to debug Cannot read property X of undefined errors
  • Made our form's buttons function
Final ToDoList `initialize`
static initialize() { this.toDoListConfig = new ToDoListConfig(); }
Final init hook
/** * Once the game has initialized, set up our module */ Hooks.once('init', () => { ToDoList.initialize(); });
Final ToDoListConfig
class ToDoListConfig extends FormApplication { static get defaultOptions() { const defaults = super.defaultOptions; const overrides = { closeOnSubmit: false, height: 'auto', id: 'todo-list', submitOnChange: true, template: ToDoList.TEMPLATES.TODOLIST, title: 'To Do List', userId: game.userId, }; const mergedOptions = foundry.utils.mergeObject(defaults, overrides); return mergedOptions; } async _handleButtonClick(event) { const clickedElement = $(event.currentTarget); const action = clickedElement.data().action; const toDoId = clickedElement.parents('[data-todo-id]')?.data()?.todoId; switch (action) { case 'create': { await ToDoListData.createToDo(this.options.userId); this.render(); break; } case 'delete': { await ToDoListData.deleteToDo(toDoId); this.render(); break; } default: ToDoList.log(false, 'Invalid action detected', action); } } activateListeners(html) { super.activateListeners(html); html.on('click', "[data-action]", this._handleButtonClick.bind(this)); } getData(options) { return { todos: ToDoListData.getToDosForUser(options.userId) } } async _updateObject(event, formData) { const expandedData = foundry.utils.expandObject(formData); await ToDoListData.updateUserToDos(this.options.userId, expandedData); } }
Final `renderPlayerList` hook
Hooks.on('renderPlayerList', (playerList, html) => { // find the element which has our logged in user's id const loggedInUserListItem = html.find(`[data-user-id="${game.userId}"]`) // create localized tooltip const tooltip = game.i18n.localize('TODO-LIST.button-title'); // insert a button at the end of this element loggedInUserListItem.append( `<button type='button' class='todo-list-icon-button flex0' title="${tooltip}"> <i class='fas fa-tasks'></i> </button>` ); // register an event listener for this button html.on('click', '.todo-list-icon-button', (event) => { const userId = $(event.currentTarget).parents('[data-user-id]')?.data()?.userId; ToDoList.toDoListConfig.render(true, { userId }); }); });

Next Step: Finishing Touches


License Information & Attribution

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution-ShareAlike 4.0 License, and code samples are licensed under the Unlicense (text below).

Provide attribution to Andrew Krigline, aka "Calego".

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.

In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to http://unlicense.org/


Finishing Touches

tags: module-tutorial foundry-vtt

We could stop here, we have a pretty solid module and we've touched on the following aspects of the Foundry VTT API:

  • Manifest JSON
  • Hooks
  • Localization
  • Flags
  • FormApplication
  • Handlebars
  • CSS

But there are two more useful things to learn about in the Foundry VTT API and they happen to let us solve some minor problems with our to-do list module:

  • Settings
  • Dialog

I. Settings

Foundry provides us with an API to store and retrieve data that isn't tied to a specific Document by way of Settings. We chose to put our ToDos on the User document with Flags and in doing so we made the decision that "ToDos relate to the User document."

Suppose we want to allow an individual user to not see the ToDoList button in their user list. We can define a setting and use that setting to drive the injection.

Define a SETTINGS constant

Similar to what we did with ToDoList.FLAGS, we should define ToDoList.SETTINGS. This is an optional quality of life thing, but it can help us prevent typos.

In your ToDoList class, add the following:

static SETTINGS = { INJECT_BUTTON: 'inject-button' }

Registering a Setting

Next we need to add actually register the setting with Foundry's API during our module's initialization.

Add the following to the ToDoList.initialize method:

game.settings.register(this.ID, this.SETTINGS.INJECT_BUTTON, { name: `TODO-LIST.settings.${this.SETTINGS.INJECT_BUTTON}.Name`, default: true, type: Boolean, scope: 'client', config: true, hint: `TODO-LIST.settings.${this.SETTINGS.INJECT_BUTTON}.Hint`, });

A lot is going on here so let's break it down one piece at a time.

game.settings.register...

This is a core method needed to register the setting. It takes in three arguments:

  1. Module ID (which we defined a constant for)
  2. Setting ID (which we also defined a constant for)
  3. 'options' (which has a lot of stuff inside it)

name & hint

These are the text displayed to the user in the settings panel. We could simply pass in a string, but that's not localization-friendly. Instead, we pass in a localization key, which Foundry Core then uses automatically.

Incidentally, this means we need to add this to our en.json:

{ "TODO-LIST": { ... "settings": { "inject-button": { "Name": "Display To-Do List button?", "Hint": "Controls whether or not the To-Do List can be opened from the player list." } } } }

default

Controls the default value. We want this to be true by default.

type

Tells Foundry Core this is a boolean value (as opposed to a String or Object).

scope

There are two scopes: world and client. World scoped settings can only be changed by the GM and affect every client at once, client scoped settings are editable by each individual and each person's selection affects only their machine.

Importantly, client scoped settings can't be seen by other clients.

config

This tells Foundry that this particular setting should show up in the configuration menu.

Test!

Refresh, then go to Configure Settings > Module Settings and you should see your module's settings there.

Image showing the Module Settings

Using the setting

Now that we have registered a setting, we can get it in the renderPlayerList hook and prevent the button's insertion if the value is 'false'.

Hooks.on('renderPlayerList', (playerList, html) => { if (!game.settings.get(ToDoList.ID, ToDoList.SETTINGS.INJECT_BUTTON)) { return; } // ...

Test!

Refresh, open your module settings un-check the "Display To-Do List button?" setting, then save.

Narrator: The button's still there.

Since we've already injected the button, the act of setting the setting doesn't usually force a re-render of the Player List. If you click on the heading of the Player List (to expand it), you'll see the button disappear.

Likewise if you have the setting off but turn it on, the same problem affects us.

Setting Callbacks

We can pass a callback to the registered setting with the onChange option:

    game.settings.register(this.ID, this.SETTINGS.INJECT_BUTTON, {
      // ...
      onChange: () => ui.players.render()

In this callback, we're tapping into Foundry Core's PlayerList#render method to force it to re-render when the setting's value changes. This is very similar to what we do in our own ToDoListConfig#_handleButtonClick, except we have to get the instance of the Application from ui.players.

One more test!

Once more, refresh, then open Settings and toggle the setting. Now the button appears and disappears correctly as the setting changes!

II. Dialog

Make sure you have that INJECT_BUTTON setting toggled on for this part.

A Dialog is a sibling of the FormApplication in that they both are part of the Application family. Where the FormApplication excells at handling complex data and live editing, the Dialogs excells at much simpler use-cases.

Let's say we don't like that Deleting a ToDo happens instantly and we'd rather have a confirmation dialog pop up when someone wants to delete one.

Dialog.confirm

That's where the Dialog.confirm factory method comes in handy. This is a simple way to prompt a user about an action without having to set up a lot of overhead.

Programming Concept: Factory Method

Simply put, a "factory method" is a utility function which pre-configures a very common task.

In this case, the Dialog class allows you to make fully custom Dialogs, but "yes/no" prompts are so common that Foundry Core has defined a special short-cut to create those.

In ToDoListConfig#_handleButtonClick change the delete case to look like this:

case 'delete': { const confirmed = await Dialog.confirm({ title: game.i18n.localize("TODO-LIST.confirms.deleteConfirm.Title"), content: game.i18n.localize("TODO-LIST.confirms.deleteConfirm.Content") }); if (confirmed) { await ToDoListData.deleteToDo(toDoId); this.render(); } break; }

The neat thing about Dialog.confirm is that it can be awaited and that will only continue forward when the user selects a choice. It also returns a boolean with the user's choice, where true means "yes".

We again are using game.i18n to ensure our prompts are localized, which means we need some additions to our en.json file:

{ "TODO-LIST": { ... "confirms": { "deleteConfirm": { "Title": "Confirm Deletion", "Content": "Are you sure you want to delete this To-Do? This action cannot be undone." } } } }

Test!

Refresh, open the ToDoListConfig, and try to delete a ToDo. The prompt will appear and your choice will determine if the ToDo is actually deleted.

Success!

Animated GIF showing the Dialog prompt when deleting a ToDo

III. Wrapping Up

Some finishing touches were applied in this section, we:

  • Registered a module setting with Foundry
  • Used that setting's value to decide if we should inject our button
  • Learned about Dialog and Dialog.confirm
  • Used a Dialog.confirm to confirm deletion of ToDos
Final `ToDoList` class
class ToDoList { static ID = 'todo-list'; static FLAGS = { TODOS: 'todos' } static TEMPLATES = { TODOLIST: `modules/${this.ID}/templates/todo-list.hbs` } static SETTINGS = { INJECT_BUTTON: 'inject-button' } /** * A small helper function which leverages developer mode flags to gate debug logs. * * @param {boolean} force - forces the log even if the debug flag is not on * @param {...any} args - what to log */ static log(force, ...args) { const shouldLog = force || game.modules.get('_dev-mode')?.api?.getPackageDebugValue(this.ID); if (shouldLog) { console.log(this.ID, '|', ...args); } } static initialize() { this.toDoListConfig = new ToDoListConfig(); game.settings.register(this.ID, this.SETTINGS.INJECT_BUTTON, { name: `TODO-LIST.settings.${this.SETTINGS.INJECT_BUTTON}.Name`, default: true, type: Boolean, scope: 'client', config: true, hint: `TODO-LIST.settings.${this.SETTINGS.INJECT_BUTTON}.Hint`, onChange: () => ui.players.render() }); } }
Final `renderPlayerList` hook
Hooks.on('renderPlayerList', (playerList, html) => { // if the INJECT_BUTTON setting is false, return early if (!game.settings.get(ToDoList.ID, ToDoList.SETTINGS.INJECT_BUTTON)) { return; } // find the element which has our logged in user's id const loggedInUserListItem = html.find(`[data-user-id="${game.userId}"]`) // create localized tooltip const tooltip = game.i18n.localize('TODO-LIST.button-title'); // insert a button at the end of this element loggedInUserListItem.append( `<button type='button' class='todo-list-icon-button flex0' title="${tooltip}"> <i class='fas fa-tasks'></i> </button>` ); // register an event listener for this button html.on('click', '.todo-list-icon-button', (event) => { const userId = $(event.currentTarget).parents('[data-user-id]')?.data()?.userId; ToDoList.toDoListConfig.render(true, { userId }); }); });
Final `ToDoListConfig#_handleButtonClick`
async _handleButtonClick(event) { const clickedElement = $(event.currentTarget); const action = clickedElement.data().action; const toDoId = clickedElement.parents('[data-todo-id]')?.data()?.todoId; ToDoList.log(false, 'Button Clicked!', { this: this, action, toDoId }); switch (action) { case 'create': { await ToDoListData.createToDo(this.options.userId); this.render(); break; } case 'delete': { const confirmed = await Dialog.confirm({ title: game.i18n.localize("TODO-LIST.confirms.deleteConfirm.Title"), content: game.i18n.localize("TODO-LIST.confirms.deleteConfirm.Content") }); if (confirmed) { await ToDoListData.deleteToDo(toDoId); this.render(); } break; } default: ToDoList.log(false, 'Invalid action detected', action); } }
Final `en.json`
{ "TODO-LIST": { "button-title": "Open To-Do list", "add-todo": "Add To-Do", "delete-todo": "Delete To-Do", "mark-done": "Mark Done", "settings": { "inject-button": { "Name": "Display To-Do List button?", "Hint": "Controls whether or not the To-Do List can be opened from the player list." } }, "confirms": { "deleteConfirm": { "Title": "Confirm Deletion", "Content": "Are you sure you want to delete this To-Do? This action cannot be undone." } } } }

Next Step: Conclusion


License Information & Attribution

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution-ShareAlike 4.0 License, and code samples are licensed under the Unlicense (text below).

Provide attribution to Andrew Krigline, aka "Calego".

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.

In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to http://unlicense.org/


Conclusion

tags: module-tutorial foundry-vtt

Congratulations! You've created a module which can CRUD ToDos for each individual user. This is as far as this tutorial will take you. If you'd like to see/download the entire result of this tutorial's code in one place, you can do so in this repository.

You now have interacted with a lot of the major facets of the Foundry VTT public API which form the backbone of any Module:

  1. Hooks
  2. Flags
  3. FormApplication
  4. Dialog
  5. Settings
  6. Handlebars
  7. CSS
  8. Localization

As a bonus you also interacted with another Module's exposed API (Development Mode).

What's Next?

If you'd like a few tasks to flex your new knowledge, try enhancing the todo-list module you've just created with the following features:

  • Allow the GM to see and edit player ToDos
  • Allow the Players to see but not edit other player ToDos (but not the GM's)
  • Add a description field to the ToDo object which is a long-form text input; adjust the design of the ToDoListConfig accordingly.

Alternatively, take the knowledge you just gained and create something of your own!

Consider joining The League of Extraordinary Foundry VTT Developers discord server and sharing what you've made or want to make. The League is a friendly community dedicated to improving the developer experience while working with Foundry VTT.

Some Credits

This tutorial was written by Andrew Krigline, aka Calego (or ElfFriend-DnD) who was the First-among-equals of the Ring Masters of the League at the time of its writing.

Members of the League, most notably Corporat, provided feedback and testing that helped make it the best it could be.

If this tutorial helped you make something or understand something better, I want to hear about it! You can find me hanging out in the League's Discord server, a screenshot of your own complete To-Do List module would make my day.

Lastly, consider supporting me through Ko-Fi or Patreon. This kind of tutorial doesn't write itself and I expect it will need some updating in the future. Your support will help me spend that time here instead of elsewhere.

Changelog

2021-12-22

Changed

  • Changed introduction language to indicate this tutorial is valid for Foundry VTT v9.
  • Changed some API links to link to the Wiki API pages.