What's the best way to control default header items in a TJS application window?

I tried the ESM example:
<svelte:options accessors={true} />

<script>javascript
import { ApplicationShell } from "@typhonjs-fvtt/runtime/svelte/component/core";
import { setContext, getContext, onMount } from "svelte";
import { getActorOwner, ucfirst } from "~/src/helpers/utility";
import { SYSTEM_ID } from "~/src/helpers/constants";
import { localize } from "#runtime/svelte/helper";


export let elementRoot;
export let documentStore;
setContext("#doc", documentStore);

const application = getContext("#external").application;
const headerButtonNoLabel = application.reactive.storeAppOptions.headerButtonNoLabel;

$: if (game.settings.get(SYSTEM_ID, "applicationWindowHeaderIconsOnly") == true) {
alert('on');
$headerButtonNoLabel = true;
} else {
alert('off');
$headerButtonNoLabel = false
}
<svelte:options accessors={true} />

<script>javascript
import { ApplicationShell } from "@typhonjs-fvtt/runtime/svelte/component/core";
import { setContext, getContext, onMount } from "svelte";
import { getActorOwner, ucfirst } from "~/src/helpers/utility";
import { SYSTEM_ID } from "~/src/helpers/constants";
import { localize } from "#runtime/svelte/helper";


export let elementRoot;
export let documentStore;
setContext("#doc", documentStore);

const application = getContext("#external").application;
const headerButtonNoLabel = application.reactive.storeAppOptions.headerButtonNoLabel;

$: if (game.settings.get(SYSTEM_ID, "applicationWindowHeaderIconsOnly") == true) {
alert('on');
$headerButtonNoLabel = true;
} else {
alert('off');
$headerButtonNoLabel = false
}
The alerts trigger every time the actor sheet opens, but the headers only change if I close and open the sheet again for a second time. 1. Start Foundry 2. Open the Actor sheet – see header button labels are on (and alert = "off") 3. Close Actor sheet 4. Change setting and save 5. Open Actor sheet – header button labels are still on (but alert = "on") 6. Close Actor sheet and re-open it – now header button labels are gone (with alert = "on")
No description
14 Replies
TyphonJS (Michael)
if (game.settings.get(SYSTEM_ID, "applicationWindowHeaderIconsOnly") == true)
if (game.settings.get(SYSTEM_ID, "applicationWindowHeaderIconsOnly") == true)
Is not reactive. If you use a game setting store from TJSGameSettings instead it can be made reactive by using the settings store. You should be able to do:
$: application.reactive.headerButtonNoLabel = $gameSettingNoLabel;
$: application.reactive.headerButtonNoLabel = $gameSettingNoLabel;
I also recommend always using === as == does type coercion in JS and shouldn 't be used unless you know exactly what you are doing. You are going to save yourself from potentially subtle bugs / issues in the future.
geoidesic
geoidesic3mo ago
Ok tryng that but mystefied by $gameSettingNoLabel – where's that coming from? I tried this in my ActorSheetShell.svelte
const application = getContext("#external").application;
const applicationWindowHeaderIconsOnly = gameSettings.getStore('applicationWindowHeaderIconsOnly');
const headerButtonNoLabel = application.reactive.storeAppOptions.headerButtonNoLabel;
$: $headerButtonNoLabel = $applicationWindowHeaderIconsOnly;
const application = getContext("#external").application;
const applicationWindowHeaderIconsOnly = gameSettings.getStore('applicationWindowHeaderIconsOnly');
const headerButtonNoLabel = application.reactive.storeAppOptions.headerButtonNoLabel;
$: $headerButtonNoLabel = $applicationWindowHeaderIconsOnly;
But it's still not reactive 😦 Or let me qualify that... it is reactive in the sense of: if I open the character sheet twice, then save a setting change, the header reacts. But it's not reactive in the sense that I still have to open the character sheet twice. It ignores the setting on first render. Specifically, it seems if the setting is changed while the character sheet is closed and then I open the character sheet, it will still have the old setting. I then have to click open the sheet again to get it to change to the correct setting value.
geoidesic
geoidesic3mo ago
Video showing the reactive issue.
geoidesic
geoidesic3mo ago
I have a similar problem when trying to update the header via buttons. From my ActorSheet.js
export default class FF15ActorSheet extends SvelteApplication {
_getHeaderButtons() {
const buttons = super._getHeaderButtons();
console.log('_getHeaderButtons'); //- this prints twice when I open the actor sheet.
buttons.unshift({
label: localize("FF15.Types.Actor.HeaderButtons.Edit"),
class: "edit-sheet",
icon: "fas " + (this.reactive.document.system.isEditing ? "fa-toggle-on" : "fa-toggle-off"),
// onclick: (ev) => this._onToggleEdit(ev),
onPress: (stuff) =>
{
this.reactive.document.update({'system.isEditing': !this.reactive.document.system.isEditing})
console.log('isEditing', this.reactive.document.system.isEditing);
}
})
return buttons;
}
export default class FF15ActorSheet extends SvelteApplication {
_getHeaderButtons() {
const buttons = super._getHeaderButtons();
console.log('_getHeaderButtons'); //- this prints twice when I open the actor sheet.
buttons.unshift({
label: localize("FF15.Types.Actor.HeaderButtons.Edit"),
class: "edit-sheet",
icon: "fas " + (this.reactive.document.system.isEditing ? "fa-toggle-on" : "fa-toggle-off"),
// onclick: (ev) => this._onToggleEdit(ev),
onPress: (stuff) =>
{
this.reactive.document.update({'system.isEditing': !this.reactive.document.system.isEditing})
console.log('isEditing', this.reactive.document.system.isEditing);
}
})
return buttons;
}
Clicking the Edit button updates the reactive value but not reactively in the header, I have to re-open the actor sheet to get it to show there. Also weird is that _getHeaderButtons runs twice when I launch the sheet. One more odd thing... in the ESM example:
onPress: ({ button }) =>
{
const newThemeDarkMode = storage.swapItemBoolean(sessionConstants.themeDarkMode);

button.title = newThemeDarkMode ? 'Dark Node disable' : 'Dark Mode enable';
button.styles = newThemeDarkMode ? { color: 'lightblue' } : { color: 'white' };
}
onPress: ({ button }) =>
{
const newThemeDarkMode = storage.swapItemBoolean(sessionConstants.themeDarkMode);

button.title = newThemeDarkMode ? 'Dark Node disable' : 'Dark Mode enable';
button.styles = newThemeDarkMode ? { color: 'lightblue' } : { color: 'white' };
}
But in my system, button here evaluates to 0, so I'm not sure how that works. I don't have access to the ESM code so I can't debug this. Or rather, I can't build it locally, is what I mean.
TyphonJS (Michael)
As things go I did not properly fork TRL or essential-svelte-esm this development cycle, so the demo code in essential-svelte-esm will have some changes. Keep the same onclick code for defining your buttons. We don't have to discuss the particular changes to callback handling at this time. For TJSGameSettings you have to set it up correctly to register your settings. I can't see how you did that. The above psuedocode that I provided is what the reactive statement should be if you have TJSGameSettings setup correctly and retrieve a store from it. While AutomatedAnimations is on an older TRL version and the import location changed this should give you some code for setup: https://github.com/otigon/automated-jb2a-animations/blob/main/src/gameSettings.js. Like always don't just copy the code. The import statement / location has changed. Here is the API docs for TJSGameSettings. Perhaps try and provide a link to your code in future posts if you have anything committed to review.
geoidesic
geoidesic3mo ago
This is how I register settings:
import { TJSGameSettings } from '#runtime/svelte/store/fvtt/settings';
import { SYSTEM_ID } from '~/src/helpers/constants';
export const gameSettings = new TJSGameSettings(SYSTEM_ID);
import { TJSGameSettings } from '#runtime/svelte/store/fvtt/settings';
import { SYSTEM_ID } from '~/src/helpers/constants';
export const gameSettings = new TJSGameSettings(SYSTEM_ID);
import { SYSTEM_CODE, SYSTEM_ID } from "~/src/helpers/constants";
import { localize } from "#runtime/svelte/helper";
import { gameSettings } from '~/src/config/gameSettings';

export function registerSettings() {
/** User settings */
dontShowWelcome();
applicationWindowHeaderIconsOnly()
}

function dontShowWelcome() {
gameSettings.register({
namespace: SYSTEM_ID,
key: 'dontShowWelcome',
options: {
name: localize(`${SYSTEM_CODE}.Setting.DontShowWelcome.Name`),
hint: localize(`${SYSTEM_CODE}.Setting.DontShowWelcome.Hint`),
scope: 'user',
config: true,
default: false,
type: Boolean
}
});
}
function applicationWindowHeaderIconsOnly() {
gameSettings.register({
namespace: SYSTEM_ID,
key: 'applicationWindowHeaderIconsOnly',
options: {
name: localize(`${SYSTEM_CODE}.Setting.applicationWindowHeaderIconsOnly.Name`),
hint: localize(`${SYSTEM_CODE}.Setting.applicationWindowHeaderIconsOnly.Hint`),
scope: 'user',
config: true,
default: false,
type: Boolean
}
});
}
import { SYSTEM_CODE, SYSTEM_ID } from "~/src/helpers/constants";
import { localize } from "#runtime/svelte/helper";
import { gameSettings } from '~/src/config/gameSettings';

export function registerSettings() {
/** User settings */
dontShowWelcome();
applicationWindowHeaderIconsOnly()
}

function dontShowWelcome() {
gameSettings.register({
namespace: SYSTEM_ID,
key: 'dontShowWelcome',
options: {
name: localize(`${SYSTEM_CODE}.Setting.DontShowWelcome.Name`),
hint: localize(`${SYSTEM_CODE}.Setting.DontShowWelcome.Hint`),
scope: 'user',
config: true,
default: false,
type: Boolean
}
});
}
function applicationWindowHeaderIconsOnly() {
gameSettings.register({
namespace: SYSTEM_ID,
key: 'applicationWindowHeaderIconsOnly',
options: {
name: localize(`${SYSTEM_CODE}.Setting.applicationWindowHeaderIconsOnly.Name`),
hint: localize(`${SYSTEM_CODE}.Setting.applicationWindowHeaderIconsOnly.Hint`),
scope: 'user',
config: true,
default: false,
type: Boolean
}
});
}
geoidesic
geoidesic3mo ago
GitHub
GitHub - geoidesic/foundryvtt-final-fantasy: A Foundry VTT system f...
A Foundry VTT system for Final Fantasy TTRPG by Square Enix - geoidesic/foundryvtt-final-fantasy
TyphonJS (Michael)
Now looking at your video. Applications when they are created aren't destroyed and a new one is created, so likely what is happening here is that the reactive bindings in the Svelte content component won't be run when the app is not displayed. The content component is destroyed between showing the app and closing it. So placing the reactive code that changes when the setting changes in the content component is not going to work in this case. You can change it to your custom Svelte application, but you'll need to manually subscribe / unsubscribe for now in the child SvelteApplication / outer JS code. Do this carefully to avoid multiple subscriptions and you must manually unsubscribe in the close method of the application. --- To test that you have things correctly setup though you can have the app / actor sheet open, but keep it open when you open the core game settings panel to make a change. If your open app updates with the setting change then TJSGameSettings is setup correctly. From your video it looks like it is working.
geoidesic
geoidesic3mo ago
Yes, you'll see that the header does change with the setting changes. So that's working. But I'm still not quite understanding... based on what you've said, why would re-opening the app sync the store? Incidentally, the other weird thing I noticed is that _getHeaderButtons gets called twice when the app opens. I couldn't see a reason for why. Not sure if that's having an impact or not but I noticed that if I put a log message in _getHeaderButtons it logs twice on opening the SvelteApplication.
TyphonJS (Michael)
If the only place where the syncing is occurring is a reactive statement in the content component that is not live / running when the app is not displayed. I can only posit a guess on why _getHeaderButtons is called twice, but likely from the reactive handling baked into SvelteApplication and when Foundry initially tries to render its app frame which is not used. That shouldn't be a problem per se. It could also be called twice because the eventual assignment via a reactive statement can trigger the update process twice. As a next step you can at least put a debug log statement in the reactive statement to verify when it runs.
$: {
application.reactive.headerButtonNoLabel = $gameSettingNoLabel;

console.log(`!!! ActorSheet - changed headerButtonNoLabel: ${$gameSettingNoLabel}`);
}
$: {
application.reactive.headerButtonNoLabel = $gameSettingNoLabel;

console.log(`!!! ActorSheet - changed headerButtonNoLabel: ${$gameSettingNoLabel}`);
}
The likely solution though to cover the case of changing the app headerButtonNoLabel option is handing the reactive store subscribe / unsubscribe in your child SvelteApplication implementation. As mentioned though this is a manual registration and needs explicit unsubscribing as this is outside of a Svelte component.
geoidesic
geoidesic3mo ago
So the reactive statement is triggering and logging, as you've suggested Note that I'm using the value twice: once in the template (which is written out at the top of the sheet in the video) and again to set the header store. So it's running when the sheet launches... but while the correct value is being output to the HTML of the body content of the Application, the value one affects the header if I re-open the application, as shown in the video. As a result – I don't think it's to with the fact that the app is not displaying? Unless I'm misunderstanding something. You can see what I mean in this screenshot. The value is displaying true in the HTML and in the logs, but the header is unaffected, unless I re-open the app. Anyhow, it may just be that I'm not understanding what you're saying 😅 not sure.
No description
TyphonJS (Michael)
There is always a possibility that this is something I'll have to take a look at for this specific scenario in regard to something else internal to TRL.
geoidesic
geoidesic3mo ago
Found a solution. In the .js file for the app, in defaultOptions
headerButtonNoLabel: game.settings.get(SYSTEM_ID, 'applicationWindowHeaderIconsOnly') || false,
headerButtonNoLabel: game.settings.get(SYSTEM_ID, 'applicationWindowHeaderIconsOnly') || false,
It doesn't require reactivity here; can just read from settings. I'm getting something weird when trying to animate the header button though.
<svelte:options accessors={true} />
<script>
import { ApplicationShell } from "@typhonjs-fvtt/runtime/svelte/component/core";
import { setContext, getContext, onMount, tick } from "svelte";
import { getActorOwner, ucfirst } from "~/src/helpers/utility";
import { SYSTEM_ID } from "~/src/helpers/constants";
import { localize } from "#runtime/svelte/helper";
import { gameSettings } from '~/src/config/gameSettings';
import Tabs from "~/src/components/molecules/Tabs.svelte";
import Abilities from "~/src/components/pages/actor/Abilities.svelte";
import Attributes from "~/src/components/pages/actor/Attributes.svelte";
import Profile from "~/src/components/pages/actor/Profile.svelte";
import Inventory from "~/src/components/pages/actor/Inventory.svelte";
export let elementRoot;
export let documentStore;
setContext("#doc", documentStore);
const defaultTabs = [
{ label: localize("FF15.Tabs.Attributes"), id: "attributes", component: Attributes },
{ label: localize("FF15.Tabs.Abilities"), id: "abilities", component: Abilities },
{ label: localize("FF15.Tabs.Inventory"), id: "inventory", component: Inventory },
{ label: localize("FF15.Tabs.Profile"), id: "profile", component: Profile },
];
let stylesApp;
let activeTab = "attributes";
$: tabs = defaultTabs;
</script>
<ApplicationShell bind:elementRoot bind:stylesApp>
<!-- NB: don't delete this pre! -->
<pre style="display: none" />
<Tabs {tabs} bind:activeTab />
</ApplicationShell>
<style lang="sass">
:global(.edit-sheet)
color: var(--border-color)
display: flex
align-items: center
:global(.edit-sheet i)
font-size: larger
:global(.edit-sheet.active)
color: var(--border-highlight)
animation: pulse 1.2s infinite ease-out
@keyframes pulse
0%
opacity: 1
25%
opacity: 0.5
100%
opacity: 1
</style>
<svelte:options accessors={true} />
<script>
import { ApplicationShell } from "@typhonjs-fvtt/runtime/svelte/component/core";
import { setContext, getContext, onMount, tick } from "svelte";
import { getActorOwner, ucfirst } from "~/src/helpers/utility";
import { SYSTEM_ID } from "~/src/helpers/constants";
import { localize } from "#runtime/svelte/helper";
import { gameSettings } from '~/src/config/gameSettings';
import Tabs from "~/src/components/molecules/Tabs.svelte";
import Abilities from "~/src/components/pages/actor/Abilities.svelte";
import Attributes from "~/src/components/pages/actor/Attributes.svelte";
import Profile from "~/src/components/pages/actor/Profile.svelte";
import Inventory from "~/src/components/pages/actor/Inventory.svelte";
export let elementRoot;
export let documentStore;
setContext("#doc", documentStore);
const defaultTabs = [
{ label: localize("FF15.Tabs.Attributes"), id: "attributes", component: Attributes },
{ label: localize("FF15.Tabs.Abilities"), id: "abilities", component: Abilities },
{ label: localize("FF15.Tabs.Inventory"), id: "inventory", component: Inventory },
{ label: localize("FF15.Tabs.Profile"), id: "profile", component: Profile },
];
let stylesApp;
let activeTab = "attributes";
$: tabs = defaultTabs;
</script>
<ApplicationShell bind:elementRoot bind:stylesApp>
<!-- NB: don't delete this pre! -->
<pre style="display: none" />
<Tabs {tabs} bind:activeTab />
</ApplicationShell>
<style lang="sass">
:global(.edit-sheet)
color: var(--border-color)
display: flex
align-items: center
:global(.edit-sheet i)
font-size: larger
:global(.edit-sheet.active)
color: var(--border-highlight)
animation: pulse 1.2s infinite ease-out
@keyframes pulse
0%
opacity: 1
25%
opacity: 0.5
100%
opacity: 1
</style>
The animation stops working if I remove the <pre> tag. It works as long as there's a sibling prior to <Tabs> element, otherwise not. The class is set when defining the button in the js controller:
_getHeaderButtons() {
log.d('_getHeaderButtons')
const buttons = super._getHeaderButtons();

const canEdit = game.user.isGM || (this.reactive.document.isOwner);
if (canEdit) {
buttons.unshift({
label: localize("FF15.Types.Actor.HeaderButtons.Edit"),
class: "edit-sheet" + (this.reactive.document.system.isEditing ? " active" : ""),
icon: "fas " + (this.reactive.document.system.isEditing ? "fa-toggle-on" : "fa-toggle-off"),
// onclick: (ev) => this._onToggleEdit(ev),
onPress: (ev) => {
this._onToggleEdit(ev)
}
})
}
return buttons;
}
_getHeaderButtons() {
log.d('_getHeaderButtons')
const buttons = super._getHeaderButtons();

const canEdit = game.user.isGM || (this.reactive.document.isOwner);
if (canEdit) {
buttons.unshift({
label: localize("FF15.Types.Actor.HeaderButtons.Edit"),
class: "edit-sheet" + (this.reactive.document.system.isEditing ? " active" : ""),
icon: "fas " + (this.reactive.document.system.isEditing ? "fa-toggle-on" : "fa-toggle-off"),
// onclick: (ev) => this._onToggleEdit(ev),
onPress: (ev) => {
this._onToggleEdit(ev)
}
})
}
return buttons;
}
Any clues? Am I doing it wrong?
TyphonJS (Michael)
I don't have a clue about what is wrong with the CSS animation as that is beyond TRL. I do warn you about being judicious on the use of global CSS because anything running in the browser with the .edit-sheet class is targetted. It does look like the animation directive is wrongly formatted with pulse out of order from what is expected or at least recommended. Review the documentation. I can also not tell if the PUG template for styles is correct. LIke many things you can start to divide and conquer. Setup a basic component that includes all of the styles / template and verify that you can get an animation working without global styles. Then once you can verify that animations work figure out why the global styles are not targetting the correct buttons in the TRL header.
Want results from more Discord servers?
Add your server