Foundry VTT + Svelte + TypeScript
Intro
I stumbled across Foundry VTT a few months ago and have been loving the immense amount of customization that it gives you.
Using HTML and Javascript, you can add in any new functionality that your heart desires to this system.
Foundry uses Handlebars as a templating system for HTML.
You might recognize the syntax if you use frameworks such as Vue or Svelte.
<p>{{firstname}} {{lastname}}</p>
What if we wanted to use something else for templating, such as Svelte?
Also, what if we wanted to complicate things further by using Typescript?
Both can be achieved and I’ll walk you through my process on how to do so.
This article won’t go over Foundry specific setup (aside from saving data and images). You can go to the Community Wiki Guide after this article to continue your Foundry development journey.
Table of Contents
Vite and Svelte
Vite provides us with an easy way to get started with a Svelte-Typescript project.
I will generally initiate my vite project within the systems directory for Foundry. There’s probably a better way to do this, but I haven’t come across what it is yet.
C:\Users\{Your PC Name}\AppData\Local\FoundryVTT\Data\systems
This is the path where Foundry stores its systems and where we will be initiating vite from.
Within your IDE of choice, make sure that your directory is set properly and then run the following (from the Vite Guide)
npm create vite@latest
This will take you through a few prompts to set up your project folder
ORnpm create vite@latest your-app-name -- --template svelte-ts
Yes, the — is necessary
Once everything is done, cd into your project directory and run:
npm install
Development Setup
Let’s now go over the setup steps needed to get our project started!
Typescript
There’s debate on whether or not you should use TypeScript in your project. On the one hand, it’s complicated for new users (even I don’t understand half of it) and generally only very useful if you’re working in team. On the other, it’s pretty neat to just learn (yes, I’m a masochist).
To start, you’ll need the @league-of-foundry-developers/foundry-vtt-types
package from npm.
npm i @league-of-foundry-developers/foundry-vtt-types --save-dev
This will help you type Foundry specific stuff, and makes Typescript happy when you’re using global Foundry objects.
Below is my tsconfig.json file. I am no expert on setting up ts.config, but I was able to get TypeScript running by fiddling around with things until I got to the below end result.
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"moduleResolution": "node",
"module": "esnext",
"target": "ES6",
"lib": ["DOM", "ES6", "ES2017", "ES2018"],
"types": ["svelte","@league-of-foundry-developers/foundry-vtt-types","node"],
"allowUmdGlobalAccess": true,
"strictNullChecks": true,
},
"include": ["src/**/*"],
"exclude": ["node_modules/*", "__sapper__/*", "public/*"],
}
Vite Config
This portion of the setup helps with compiling Svelte + Typescript into things that the Foundry system can read.
Below is my vite.config.ts file.
I set the “lib” option for entry to our main file (main.ts). This is where the application starts, I.E. where you’re setting up the game system in your Hooks.once
function.
Formats is set to iife because I believe that’s the format type that Foundry needs. Don’t quote me on that, but it does work.
The filename attribute is the name of our minified file. Everything should be complied under a “dist” folder within your project directory.
Watch -> buildDelay is where you set how often the build command is run when you use the npm run build --watch
command.
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
export default defineConfig({
plugins: [svelte()],
build: {
minify: "terser",
lib: {"entry":"src/module/main.ts","formats":["iife"],
name: "app",fileName: "main.mjs",},
watch: {
buildDelay: 3000
}
},
});
Svelte config should be fine as is.
import sveltePreprocess from 'svelte-preprocess'
export default {
preprocess: sveltePreprocess()
}
Foundry Setup for Svelte
Initial Setup
As a base, I used the below template for setting up Foundry and Svelte.
Svelte Foundry TemplateI would not recommend using npm install
on the package.json file for that template. It uses rollup, and we are using vite (which also uses rollup). Vite is a little easier to configure in my opinion, so we will stick to just the files under the src folder.
I won’t go into great detail with each of these folders/files unless it’s specific to Svelte/TypeScript setup.
Lang
This houses all of the json files for localization.
Module
Here lies the belly of the beast. You’ll find the main file (main.js (main.ts in our case)), preloadTemplates.js, and an actor folder (with files necessary for showing our character sheet).
main.js This has the base code to get your Foundry application up and running. I would recommend using this as is (barring changing things like system name, etc.). At base, it doesn’t need much editing at all for TypeScript.
Here’s an example of what one of my systems looks like:
import { preloadTemplates } from "./preloadTemplates";
import { CharacterSheet } from "./actor/investigator-sheet";
import { Investigator } from "./actor/investigator";
const SYSTEM_NAME = "CallofCthulhuV7";
Hooks.once("init", async () => {
game[SYSTEM_NAME] = { CharacterSheet };
CONFIG.Actor.documentClass = Investigator;
Actors.unregisterSheet("core", ActorSheet);
Actors.registerSheet(SYSTEM_NAME, CharacterSheet, { label: 'Investigator', types: ["investigator"], makeDefault: true });
await preloadTemplates(SYSTEM_NAME);
});
preloadTempaltes.js This can be left as is. You should be able to keep this as a js file, but I changed it to “.ts” just to keep things consistent.
character-sheet.js This is where all the magic happens!
At the top, you will need to import the main .svelte file that houses your character sheet (or other page if you’re creating something else.)
Let’s walk through each of the functions in the CharacterSheet class.
This function, I believe, gets the defaultOptions setup for your Foundry application. Classes are the css classes that are going to be used for styling…I think. I usually use the <style>
section in the svelte components to style my app. So you most likely don’t need to worry about this.
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["character", "sheet", "actor"],
});
}
This gets the handlebars template to use. We’ll visit that in the template section later.
In essence, this is the template that your Svelte application will be injected into.
get template() {
return `systems/svelte/templates/character.hbs`;
}
These two functions work to get your Actor Data setup.
get actorData() {
return this.actor.data;
}
getData() {
return {
actor: this.actorData,
data: this.actorData.toObject().data,
};
}
Here is where the Svelte app is injected into the handlebars template.
I’m not 100% sure what super._injectHTML is doing. I think this loads the handlebars template first.
Next the Svelte app is created and injected into the “form” located on the handlebars template.
The second function I believe works when the application is updated/re-rendered. Again, I’m not 100% certain of that.
// Injects Svelte app when initializing HTML
async _injectHTML(html) {
await super._injectHTML(html);
this.app = new FormApp({
target: html.find("form")[0],
props: {
sheetData: this.getData(),
},
});
}
// Injects Svelte app when replacing innerHTML
async _replaceHTML(element, html) {
await super._replaceHTML(element, html);
this.app = new FormApp({
target: html.find("form")[0],
props: {
sheetData: this.getData(),
},
});
}
character.js and character-sheet.svelte I’m not totally sure of the usage of the character.js file, it’s not something that I generally have needed to use. Most of my javascript is within Svelte itself.
The .svelte file should be self explanatory. This is the main Svelte file where our character sheet will live.
Templates
This is where all of the handlebar templates are housed. I would recommend using a separate template for each window/sheet that you intend to create.
When we create the Svelte app in character-sheet.js, we’re creating an app on the form element of that specific template. So things will get wonky if you use the same template for different things.
You can keep the file as is. I’ve included what it looks like below.
<div class="{{cssClass}} flexcol">
<form autocomplete="off"></form>
</div>
Other notes
I generally place both the lang
and templates
folders in the public
folder in the project directory. This way, Vite will copy those files as is to your dist folder where the minified JS is kept. They won’t copy over at all if you place them in the src
folder.
Saving Data
Saving data is generally a piece of cake with Svelte.
In our character-sheet.svelte
file, we declare our props like this:
export let sheetData: { actor: ActorData; data: Type }; // prop declaration
const { actor, data } = sheetData; // destructured for ease of use
ActorData is the type for the this.actorData
in our character-sheet.ts
file. Type is just a placeholder for this example. It will be the interface you use for your character sheet data.
I.E.
interface CharacterData {
name: string
age: number
}
Next, let’s add an input for name that saves what we type back into Foundry.
<input type="text" id="name" name="data.name" value={data.name} />
The key parts here are “name” and “value”. Name is the attribute used to save the data back into Foundry, and value displays the value of our name. This should be fairly similar to how Foundry does things using handlebars.
Character Image Setup
Getting an image to save for the character sheet was difficult. For some reason it just doesn’t work when you’re using Svelte, so I had to use the global “FilePicker” class to get things working.
This portion was mostly accomplished by looking at the documentation and fiddling around. I can tell you my guess as to what each thing does, though don’t hold me to it!
Type and activeSource I am not sure what those are, but I think they were mentioned in the documentation.
callback
is the function used to save the image path. We set the actorImage
prop to the path
value that is being passed to the callback function.
Both field and button have the same thing going on, I’m not sure if you need both or just one of them (haven’t gotten around to fooling with this again). We are getting the img
element from the html in the same Svelte file. I have the below code snippets separated as two things just for clarity, but they are both in the same Svelte file.
Next we have the html img element. I have an on:click listener to render the FilePicker, which sends “true” to render it.
data-edit
was something from either the Foundry Tutorial I linked above or the docs. Either way, this is what should make everything work, but for some reason it hates Svelte.
Everything else should be self explanatory.
<script lang="ts">
export let actorImage;
export let actorName;
const filePicker = new FilePicker({
type: "image",
activeSource:"public",
callback: (path: string) => {
actorImage = path},
field: document.getElementsByTagName("img")[0],
button: document.getElementsByTagName("img")[0]
});
</script>
<img
class="profile-img"
name="img"
data-edit="img"
src={actorImage}
title={actorName}
alt="Profile"
width="200px"
on:click={() => filePicker.render(true)}
/>
Conclusion
With all of this you should be able to get your TypeScript/Svelte/Foundry project going. All you need to do is run npm run build
to compile everything. You can add the --watch
flag to the end to run the build whenever changes are made to your project directory.