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.
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.
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.
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.
By the end of this tutorial, you'll have at least touched each of the following core Foundry VTT API concepts:
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.
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.
If you're completely new to these technologies, this tutorial can still be helpful to you, but you may find yourself overwhelmed.
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
Sometimes we'll be debugging errors that our code has, they'll be styled like so:
Error: This is an error.
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.
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/
module-tutorial
foundry-vtt
A lot of this first step is very similar to the official knowledgebase article: Introduction to Module Development
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.
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.
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.
Our module has exactly 1 file, so there's very few ways it might be broken.
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.
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.
🥳
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.
Now we need this empty module to do something.
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!');
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.
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!
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.
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"
}
],
Create a styles
directory and a todo-list.css
, then register it in your Manifest:
"styles": [
"styles/todo-list.css"
],
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.
Once you've added all of these files and directories, restart foundry one last time before moving on.
So far we have:
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"
}
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
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/
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.
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.
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.
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.
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:
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.
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.
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:
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.
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.
Everything you care about should be grouped into Class
es. A Class
governs one 'concern'. These Class
es 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).
At the most fundamental level, our TODO module needs two things:
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.
There's a few ways modules can handle data in Foundry, which you use depends on your needs. Here's our assumptions:
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.
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.
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"
We want a User Interface which allows Users to Create, Read, Update, and Delete ToDo
s which are objects consisting of some Text for a label
and a Boolean for isDone
stored on User
flags
.
Next Step: Setting Up
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/
module-tutorial
foundry-vtt
There's a bit of up-front work we can do to make our lives easier going forward.
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.
There's a few places where ID
will be useful in our module's code:
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.
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.
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.
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.
*/
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.
static log(force, ...args) {
const shouldLog = force || game.modules.get('_dev-mode')?.api?.getPackageDebugValue(this.ID);
if (shouldLog) {
console.log(this.ID, '|', ...args);
}
}
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);
});
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.
We've set up some things for our convienience in the next steps.
/**
* 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
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/
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 ToDo
s. In our scripts/todo-list.js
file, define a new class: ToDoListData
.
class ToDoListData {
}
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:
game.users
Collection.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);
}
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);
}
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
"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!
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.
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;
}
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);
}
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' });
Success!
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);
}
Same as before, get a todo from ToDoListData.allToDos
and then use our new method to delete it.
ToDoListData.deleteToDo('56r58vm2v197prr8');
Success!
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 updateToDo
s 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);
}
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 },
});
Success!
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.
/**
* 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
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/
module-tutorial
foundry-vtt
Here's what we're hoping to end up with:
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
.
We also see what arguments are being passed to the Hook as it's fired:
PlayerList
class - we're not very interested in this#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.
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.
Our goal is clear at this point:
renderPlayerList
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.
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>"
);
});
:partying-face:
We should add a tooltip on this button so people don't wonder what it is, and we should ensure this tooltip is localized.
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"
}
}
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>`
);
Refresh and verify that when hovering over the button, the value we assigned in the languages file shows up.
Narrator. It does not.
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.
There's a lot of room for improvement on how our button looks right now.
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>`
);
Better already. For the next steps, we'll need to open our own styles/todo-list.css
file.
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;
}
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.
Perfect.
In this section we…
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!');
});
});
.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
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/
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:
FormApplication
subclass which gathers our data for a Template, and which handles the logic for interacting with that template.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.
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 {
}
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:
todo-list
.ToDoList
class.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.
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.
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>
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?
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:
- 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.
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
.
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>
...
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>
Success!
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>
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;
}
Simply put, we're telling them the values from the object populating the template (the one we defined in ToDoListConfig#getData
).
checked
to display the proper state of the ToDo based on the isDone
propertyvalue="{{label}}"
to know what its contents should bename
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.
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.
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>
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.
In this section we:
ToDoListConfig
subclass of FormApplication
with:
defaultOptions
getData
method which populates the ToDos for a given user
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)
}
}
}
<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>
{
"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
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/
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.
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();
});
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.
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.
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
.
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});
});
});
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.
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 ToDo
s 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.
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.
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!
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 button
s, 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).
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.
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.
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.
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:
ToDoListData.createToDo
method to create a blank new ToDo on the user whose list we are looking at.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:
ToDoListData.deleteToDo
method to delete the appropriate ToDo.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).
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
.
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.
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.
The console gives us some useful information to help debug this.
HTMLButtonElement._handleButtonClick
todo-list.js
: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 });
We expect this
to be an instance of ToDoListConfig
, but for some reason it's an HTML element button
.
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.
We accomplished a lot in this section:
FormApplication._updateObject
and how to make data persist on editFormApplication.activateListeners
and how to use it to make buttons workactivateListeners
method stopped our data persistenceCannot read property X of undefined
errors
static initialize() {
this.toDoListConfig = new ToDoListConfig();
}
/**
* Once the game has initialized, set up our module
*/
Hooks.once('init', () => {
ToDoList.initialize();
});
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);
}
}
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
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/
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:
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:
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.
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'
}
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:
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.
Refresh, then go to Configure Settings > Module Settings and you should see your module's settings there.
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;
}
// ...
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.
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
.
Once more, refresh, then open Settings and toggle the setting. Now the button appears and disappears correctly as the setting changes!
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 await
ed 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."
}
}
}
}
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.
Some finishing touches were applied in this section, we:
Dialog
and Dialog.confirm
Dialog.confirm
to confirm deletion of ToDos
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()
});
}
}
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 });
});
});
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);
}
}
{
"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
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/
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:
As a bonus you also interacted with another Module's exposed API (Development Mode).
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:
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.
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.
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: A FormApplication subclass which gathers our data for a Template, and which handles the logic for interacting with that template. A handlebars template which displays the information and controls that the FormApplication gathers/handles. 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.
Apr 22, 2022Congratulations! 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: Hooks Flags FormApplication Dialog Settings Handlebars
Dec 22, 2021We 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
Dec 22, 2021So 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:
Dec 22, 2021or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up