Extending ActiveEffectConfig (i.e. custom ActiveEffects sheet based on Svelte)

I want to create a custom ActiveEffects sheet. Advice from Foundry is:
Re4XN — Today at 13:29
Extend ActiveEffectConfig.
Mana — Today at 14:30
Assign your datamodel to the base type if you want it to be used when no type is specified.
CONFIG.ActiveEffect.dataModels.base = yourModel;
You can of course have custom types as much as you want.
Re4XN — Today at 13:29
Extend ActiveEffectConfig.
Mana — Today at 14:30
Assign your datamodel to the base type if you want it to be used when no type is specified.
CONFIG.ActiveEffect.dataModels.base = yourModel;
You can of course have custom types as much as you want.
However, I'm not sure how that ties into the examples for creating svelte-based sheets from esm?
12 Replies
geoidesic
geoidesicOP5w ago
Here's what I have so far:
import FFActiveEffectShell from './FFActiveEffectShell.svelte';
import { SvelteApplication } from "@typhonjs-fvtt/runtime/svelte/application";
import { SYSTEM_ID, SYSTEM_CODE } from "~/src/helpers/constants"

export default class FFActiveEffectSheet extends SvelteApplication {
/**
* Default Application options
*
* @returns {object} options - Application options.
* @see https://foundryvtt.com/api/interfaces/client.ApplicationOptions.html
*/
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: `${SYSTEM_ID}--active-effect-config`,
classes: [SYSTEM_CODE, 'sheet', 'active-effect-config'],
title: 'EFFECT.ConfigTitle',
template: `systems/${SYSTEM_ID}/templates/active-effect-config.html`,
width: 560,
height: 'auto',
tabs: [{navSelector: '.tabs', contentSelector: '.content', initial: 'details'}],
dragDrop: [{dragSelector: null, dropSelector: null}],
svelte: {
class: FFActiveEffectShell,
target: document.body
}
});
}

getData() {
const effect = this.object;
const data = {
effect: effect,
isActorEffect: effect.parent.documentName === 'Actor',
isItemEffect: effect.parent.documentName === 'Item',
submitText: 'EFFECT.Submit',
modes: Object.entries(CONST.ACTIVE_EFFECT_MODES).reduce((obj, e) => {
obj[e[1]] = game.i18n.localize('EFFECT.MODE_' + e[0]);
return obj;
}, {})
};

return data;
}

_updateObject(event, formData) {
const effect = this.object;
return effect.update(formData);
}
}
import FFActiveEffectShell from './FFActiveEffectShell.svelte';
import { SvelteApplication } from "@typhonjs-fvtt/runtime/svelte/application";
import { SYSTEM_ID, SYSTEM_CODE } from "~/src/helpers/constants"

export default class FFActiveEffectSheet extends SvelteApplication {
/**
* Default Application options
*
* @returns {object} options - Application options.
* @see https://foundryvtt.com/api/interfaces/client.ApplicationOptions.html
*/
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: `${SYSTEM_ID}--active-effect-config`,
classes: [SYSTEM_CODE, 'sheet', 'active-effect-config'],
title: 'EFFECT.ConfigTitle',
template: `systems/${SYSTEM_ID}/templates/active-effect-config.html`,
width: 560,
height: 'auto',
tabs: [{navSelector: '.tabs', contentSelector: '.content', initial: 'details'}],
dragDrop: [{dragSelector: null, dropSelector: null}],
svelte: {
class: FFActiveEffectShell,
target: document.body
}
});
}

getData() {
const effect = this.object;
const data = {
effect: effect,
isActorEffect: effect.parent.documentName === 'Actor',
isItemEffect: effect.parent.documentName === 'Item',
submitText: 'EFFECT.Submit',
modes: Object.entries(CONST.ACTIVE_EFFECT_MODES).reduce((obj, e) => {
obj[e[1]] = game.i18n.localize('EFFECT.MODE_' + e[0]);
return obj;
}, {})
};

return data;
}

_updateObject(event, formData) {
const effect = this.object;
return effect.update(formData);
}
}
TyphonJS (Michael)
There are two things in play here. The Foundry API side of things in relation to system setup. Then on the TRL / Svelte side if you are dealing with documents use TJSDocument or TJSDocumentCollection.
geoidesic
geoidesicOP5w ago
And then I tried to assign it via:
CONFIG.ActiveEffect.sheetClass = FFActiveEffectSheet;
CONFIG.ActiveEffect.sheetClass = FFActiveEffectSheet;
But I'm still getting the default sheet
TyphonJS (Michael)
That's the Foundry API side of things. You'll have to ask help from other system devs here for any advice on that matter. I can possibly help you on the TRL / Svelte side after you work out the Foundry stuff.
geoidesic
geoidesicOP5w ago
Looks like I got the Foundry side working with this:
DocumentSheetConfig.registerSheet(ActiveEffect, "foundryvtt-final-fantasy", FFActiveEffectSheet, {
makeDefault: true,
});
DocumentSheetConfig.registerSheet(ActiveEffect, "foundryvtt-final-fantasy", FFActiveEffectSheet, {
makeDefault: true,
});
I'm now getting errors in my attempted sheet:
foundry.js:655 Error: An error occurred while rendering FFActiveEffectSheet 26. Cannot read properties of undefined (reading 'parent')
at Hooks.onError (foundry.js:654:24)
at 🎁call_wrapped [as call_wrapped] (libWrapper-wrapper.js:507:22)
at 🎁Hooks.onError#lib-wrapper (listeners.js:138:11)
at 🎁Hooks.onError#0 (libWrapper-wrapper.js:187:52)
at foundry.js:5795:13
Caused by: TypeError: Cannot read properties of undefined (reading 'parent')
at FFActiveEffectSheet.getData (FFActiveEffectSheet.…1735487202922:33:32)
at FFActiveEffectSheet._render (foundry.js:5838:29)
at FFActiveEffectSheet.render (foundry.js:5793:10)
at editItem (EffectsTab.svelte:77:16)
at HTMLButtonElement.<anonymous> (EffectsTab.svelte:195:1276)
foundry.js:655 Error: An error occurred while rendering FFActiveEffectSheet 26. Cannot read properties of undefined (reading 'parent')
at Hooks.onError (foundry.js:654:24)
at 🎁call_wrapped [as call_wrapped] (libWrapper-wrapper.js:507:22)
at 🎁Hooks.onError#lib-wrapper (listeners.js:138:11)
at 🎁Hooks.onError#0 (libWrapper-wrapper.js:187:52)
at foundry.js:5795:13
Caused by: TypeError: Cannot read properties of undefined (reading 'parent')
at FFActiveEffectSheet.getData (FFActiveEffectSheet.…1735487202922:33:32)
at FFActiveEffectSheet._render (foundry.js:5838:29)
at FFActiveEffectSheet.render (foundry.js:5793:10)
at editItem (EffectsTab.svelte:77:16)
at HTMLButtonElement.<anonymous> (EffectsTab.svelte:195:1276)
TyphonJS (Michael)
Maybe... Just maybe... "Do weird shit / win weird prizes?" Are you just copy / pasting random code from places together? It looks like you took some code from some example based on core AppV1 / FormApplication and dropped it into SvelteApplication and expected it to work. SvelteApplication isn't a FormApplication. In getData there is no this.object hence it will be undefined and effect is undefined and the error is pretty clear why this doesn't work. You've probably used TRL / SvelteApplication correctly at some point. Is this how you've done things in the past?
geoidesic
geoidesicOP5w ago
Good question. I'll have another go. I wasn't thrilled with my weird prize.
TyphonJS (Michael)
I haven't worked with active effects, but if Foundry v12 is using the FormApplication constuctor format then it's a document followed by the app options. Create a TJSDocument from the doc sent into the constructor and attach it as a context to the app shell / component you are loading. Remove the getData / _updateObect as that is copy / pasted code not relevant. Also take out the template/ tabs / dragDrop options being set in defaultOptions. I can't write the code for you this time or at least test it. The below is pseudo-code I just typed out. I'm assuming Foundry is following the FormApplication constructor. It looks like this though:
import { SvelteApplication } from "#runtime/svelte/application";
import { TJSDocument } from '#runtime/svelte/store/fvtt/document';

import FFActiveEffectShell from './FFActiveEffectShell.svelte';
import { SYSTEM_CODE } from "~/src/helpers/constants"

export default class FFActiveEffectSheet extends SvelteApplication {
#doc;

constructor(doc, options) {
super(options);
this.#doc = new TJSDocument(doc, { delete: () => this.close() ));
}

/**
* Default Application options
*
* @returns {object} options - Application options.
* @see https://foundryvtt.com/api/interfaces/client.ApplicationOptions.html
*/
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: [SYSTEM_CODE, 'sheet', 'active-effect-config'],
title: 'EFFECT.ConfigTitle',
width: 560,
height: 'auto',
svelte: {
class: FFActiveEffectShell,
target: document.body
context: function() {
doc: this.#doc;
}
}
});
}
}
import { SvelteApplication } from "#runtime/svelte/application";
import { TJSDocument } from '#runtime/svelte/store/fvtt/document';

import FFActiveEffectShell from './FFActiveEffectShell.svelte';
import { SYSTEM_CODE } from "~/src/helpers/constants"

export default class FFActiveEffectSheet extends SvelteApplication {
#doc;

constructor(doc, options) {
super(options);
this.#doc = new TJSDocument(doc, { delete: () => this.close() ));
}

/**
* Default Application options
*
* @returns {object} options - Application options.
* @see https://foundryvtt.com/api/interfaces/client.ApplicationOptions.html
*/
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: [SYSTEM_CODE, 'sheet', 'active-effect-config'],
title: 'EFFECT.ConfigTitle',
width: 560,
height: 'auto',
svelte: {
class: FFActiveEffectShell,
target: document.body
context: function() {
doc: this.#doc;
}
}
});
}
}
Then in FFActiveEffectShell :
<script>
import { getContext } from 'svelte';
const doc = getContext('#external').doc;

// doc is the TJSDocument from the outer JS class.
</script>

<!-- Do the regular app shell stuff this is demo code -->
<ApplicationShell ...>
Doc name: {$doc?.name}
</ApplicationShell>
<script>
import { getContext } from 'svelte';
const doc = getContext('#external').doc;

// doc is the TJSDocument from the outer JS class.
</script>

<!-- Do the regular app shell stuff this is demo code -->
<ApplicationShell ...>
Doc name: {$doc?.name}
</ApplicationShell>
geoidesic
geoidesicOP5w ago
Thanks for that. I'll try it out. I think the reason I had such a mess is I let Cursor AI run wild with it.
TyphonJS (Michael)
Yeah... Don't do that.. 😮
geoidesic
geoidesicOP5w ago
I seem to have first evidence of success with that. Thanks for your help! A few weird things: 1. For ItemSheet and ActorSheet, the name of the item / actor seems to end up in the application title by default. But not so with ActiveEffect. I'm surprised that it does for item / actor since the sheet is entirely custom. Also don't understand why it then doesn't for ActiveEffect. Any thoughts? 2. It didn't like doc: this.#doc – I don't think one can use this in a static method. 3. I tried adding the title property to the defaultOptions but couldn't figure out how to reference the TJSDocument instance (or for that matter, the class's properties, within defaultOptions). So I can only give it text for title. The examples in essential-svelte-esm don't have dynamic titles either. This is how Cursor AI solved the problem:
import { SvelteApplication } from "#runtime/svelte/application";
import { TJSDocument } from '#runtime/svelte/store/fvtt/document';
import { generateRandomElementId } from "~/src/helpers/util";
import { SYSTEM_CODE, SYSTEM_ID } from "~/src/helpers/constants";
import FFActiveEffectShell from './FFActiveEffectShell.svelte';

export default class FFActiveEffectSheet extends SvelteApplication {
#doc;
#storeUnsubscribe;

constructor(doc, options) {
super(options);
this.#doc = new TJSDocument(doc, { delete: () => this.close() });
}

static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: `${SYSTEM_ID}--active-effect-sheet-${generateRandomElementId()}`,
classes: [SYSTEM_CODE, 'sheet', 'active-effect-config'],
width: 560,
height: 'auto',
svelte: {
class: FFActiveEffectShell,
target: document.body,
props: {
doc: null
}
}
});
}

/** @override */
get document() {
return this.#doc;
}

/** @override */
async _injectHTML(html) {
this.options.svelte.props.doc = this.document;
return super._injectHTML(html);
}

render(force = false, options = {}) {
if (!this.#storeUnsubscribe) {
this.#storeUnsubscribe = this.#doc.subscribe(this.#handleDocUpdate.bind(this));
}
return super.render(force, options);
}

async close(options = {}) {
if (this.#storeUnsubscribe) {
this.#storeUnsubscribe();
this.#storeUnsubscribe = void 0;
}
return super.close(options);
}

#handleDocUpdate(doc) {
if (doc) {
this.reactive.title = `${doc.name || ''}`;
}
}
}
import { SvelteApplication } from "#runtime/svelte/application";
import { TJSDocument } from '#runtime/svelte/store/fvtt/document';
import { generateRandomElementId } from "~/src/helpers/util";
import { SYSTEM_CODE, SYSTEM_ID } from "~/src/helpers/constants";
import FFActiveEffectShell from './FFActiveEffectShell.svelte';

export default class FFActiveEffectSheet extends SvelteApplication {
#doc;
#storeUnsubscribe;

constructor(doc, options) {
super(options);
this.#doc = new TJSDocument(doc, { delete: () => this.close() });
}

static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: `${SYSTEM_ID}--active-effect-sheet-${generateRandomElementId()}`,
classes: [SYSTEM_CODE, 'sheet', 'active-effect-config'],
width: 560,
height: 'auto',
svelte: {
class: FFActiveEffectShell,
target: document.body,
props: {
doc: null
}
}
});
}

/** @override */
get document() {
return this.#doc;
}

/** @override */
async _injectHTML(html) {
this.options.svelte.props.doc = this.document;
return super._injectHTML(html);
}

render(force = false, options = {}) {
if (!this.#storeUnsubscribe) {
this.#storeUnsubscribe = this.#doc.subscribe(this.#handleDocUpdate.bind(this));
}
return super.render(force, options);
}

async close(options = {}) {
if (this.#storeUnsubscribe) {
this.#storeUnsubscribe();
this.#storeUnsubscribe = void 0;
}
return super.close(options);
}

#handleDocUpdate(doc) {
if (doc) {
this.reactive.title = `${doc.name || ''}`;
}
}
}
Not sure if that's optimal – also not sure wtf is going on in there – but it does work.
TyphonJS (Michael)
You are not going to get anything of value from Cursor AI. Even if you can get something to "work" it's likely shit code like every code snippet you have brought up in this post. I have no time to debug much of what you come up with especially when AI is involved. Crititcal thinking is a foundational and very important part of actually being a programmer and you need to start exercising that muscle. Previously above I wrote out psuedo-code insofar that it should look similar. I did this from Discord quickly. context and props can be defined as a normal function and are invoked with the this of the app instance. What I did not write out correctly is that this function needs to return an object.
svelte: {
class: FFActiveEffectShell,
target: document.body
context: function() {
return { doc: this.#doc; }
}
}
svelte: {
class: FFActiveEffectShell,
target: document.body
context: function() {
return { doc: this.#doc; }
}
}

Did you find this page helpful?