Foundry Modules 🤝 Svelte - Building FVTT modules using Svelte

Svelte is a radical new approach to building user interfaces. Whereas traditional frameworks like React and Vue do the bulk of their work in the browser, Svelte shifts that work into a compile step that happens when you build your app. - svelte.dev

Why do I need a framework?

You probably don’t. The Foundry interface is built with Handlebars to render the HTML components. There are two ways to update the data on screen - direct DOM updates (often through jQuery) or re-rendering the entire application and replacing it in the DOM. This is fine for most simple use cases, but once your application becomes more complex. Re-rendering everything is slow, but manually updating the DOM and keeping track of state changes can become a real headache.

A modern component framework can atomically update the DOM without re-rendering everything — you simply need to update the data inputs and only the changed elements will update.

Why Svelte?

A Foundry module exists in a peculiar landscape. The interface is already there, and you will build a component on top of that.

Adding an entire front-end framework on top of this, just to support your module (where many users will run tens to hundreds of modules) can easily lead to bloat. Your module would probably only use a fraction of the React or Vue.js frameworks.

This is where Svelte comes in. Svelte is not a front-end framework. It is a component framework with its own compiler. Instead of creating the datastructures that will be executed by the framework code, you write code that will be compiled into the minimal needed operations for updating the DOM.

See also Svelte 3: Rethinking reactivity

Getting started with Svelte in Foundry

Before you can start building Svelte components and using them in your Foundry module, you need add the Svelte compiler as part of your build, and you need to grasp the basics of using Svelte.

Setup the Svelte compiler

Before you can run your Svelte application, you need to compile the .svelte files to regular JavaScript. I recommend using rollup.js, but webpack should be just as easy to set up.

Learning Svelte

I recommend you start with a cursory look at the Tutorial and Examples while the Docs will give you detailed knowledge about Svelte.

Add to Foundry

// main.js
import App from "./App.svelte";
class MyApp extends Application {
  activateListeners(html) {
    this.component = new App({
      target: html.get(0),
      props: {
        place: "Foundry"
      },
    });
  }
}

Hooks.on("ready" () => {
  const app = new MyApp()
  app.render();
});

Complete Example

Here is a simple example of the minimal code needed to add a Svelte “Hello World” to your Foundry module.

<!-- App.svelte -->
<script>
  export let name = "world";
</script>

<div>Hello {name}!</div>

To add the App.svelte component to your Foundry module, simply attach it to a regular Foundry Application once it has been rendered.

// main.js
import App from "./App.svelte";
class MyApp extends Application {
  activateListeners(html) {
    this.component = new App({
      target: html.get(0),
      props: {
        name: "Foundry",
      },
    });
  }
}

Hooks.on("ready", () => {
  const app = new MyApp()
  app.render();
});

App.svelte will be compiled into the JS below. The imported functions are mostly transparent wraps of native DOM element creation/updates.

/* App.svelte generated by Svelte v3.31.0 */
import {
	SvelteComponent,
	append,
	detach,
	element,
	init,
	insert,
	noop,
	safe_not_equal,
	set_data,
	text
} from "svelte/internal";

function create_fragment(ctx) {
	let div;
	let t0;
	let t1;
	let t2;

	return {
		c() {
			div = element("div");
			t0 = text("Hello ");
			t1 = text(/*place*/ ctx[0]);
			t2 = text("!");
		},
		m(target, anchor) {
			insert(target, div, anchor);
			append(div, t0);
			append(div, t1);
			append(div, t2);
		},
		p(ctx, [dirty]) {
			if (dirty & /*place*/ 1) set_data(t1, /*place*/ ctx[0]);
		},
		i: noop,
		o: noop,
		d(detaching) {
			if (detaching) detach(div);
		}
	};
}

function instance($$self, $$props, $$invalidate) {
	let { place = "world" } = $$props;

	$$self.$$set = $$props => {
		if ("place" in $$props) $$invalidate(0, place = $$props.place);
	};

	return [place];
}