I Am Ven

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

OR
npm 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

    }

  },

});

Note

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 Template

I 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.