Advanced Templates Module (PF1)

A thread to discuss Svelte and the Advanced Templates module.
23 Replies
david aka claudekennilol
p.s. I'm on lunch while at Jury duty and I don't know when we'll be called back in, so if I stop responding that's why 😅
TyphonJS (Michael)
TyphonJS (Michael)OP•10mo ago
Yeah.. I'll take a look at that branch now. What is the action prop? Does this data change or is it static? This could also be moved to a context quite likely. It also is reasonable to move the context setting to the component constructor here https://github.com/dmrickey/ckl-advanced-templates-pf1/blob/ui_update/src/module/view/show-template-settings.js The Svelte component constructor accepts props, but also a context Map where you can set the context data in the constructor and don't have to do that inside the component itself. See the constructor options
const _show = (parent, sibling, app, action = {}) => {
return new TemplateSettings({
target: parent,
anchor: sibling,
props: { TemplateApplication: app },
context: new Map([['action', action], ['updates', writable({})]])
});
}
const _show = (parent, sibling, app, action = {}) => {
return new TemplateSettings({
target: parent,
anchor: sibling,
props: { TemplateApplication: app },
context: new Map([['action', action], ['updates', writable({})]])
});
}
What is the current state of the code branch? Is it working and you want to refine it? Or is it a current work in progress. I haven't installed the module to try running it and there are several aspect that can potentially be improved. Just from looking at the code in template-settings.svelte it is not clear how the reactive block executes except for when the first time the component is instantiated. I gather this block runs immediately and subsequently isn't triggered:
$: {
updateAction(action, updates);
console.log("applying updates", updates);
}
$: {
updateAction(action, updates);
console.log("applying updates", updates);
}
I'd need to install this module to examine it more, but let me know the status of this branch. In all of the settings components and the shared component I see that you directly bind / modify the updates object. This shouldn't trigger reactivity or the reactive block quoted above. Is there a reason you don't wrap the settings components in a form and have an on:submit handler to then process all of the form data? For example:
<script>
function saveData(event) {
// Note: FormDataExtended is from Foundry and might be useful vs `FormData` as it coerces types based on input type.
const fd = new FormDataExtended(event.target);

// `fd.object` is the processed / coerced data.
const data = fd.object;

// You can further sanitize / process it here.

foundryDoc.update(data, { render: false });
}
</script>

<form on:submit|preventDefault={saveData} autocomplete=off>
<svelte:component this={TemplateApplication} />
<SharedSettings />
</form>
<script>
function saveData(event) {
// Note: FormDataExtended is from Foundry and might be useful vs `FormData` as it coerces types based on input type.
const fd = new FormDataExtended(event.target);

// `fd.object` is the processed / coerced data.
const data = fd.object;

// You can further sanitize / process it here.

foundryDoc.update(data, { render: false });
}
</script>

<form on:submit|preventDefault={saveData} autocomplete=off>
<svelte:component this={TemplateApplication} />
<SharedSettings />
</form>
Doing it this way also doesn't require each of the TemplateApplication components to accept a slot for the shared settings. In this case you set the name field of each input to the desired key properties for the resulting data like: Inside one of the template components...
<script>
import { CONSTS, MODULE_NAME } from "../consts";
</script>

<input type=text name={`flags.${MODULE_NAME}.${CONSTS.flags.circle.areaType}`} />
<script>
import { CONSTS, MODULE_NAME } from "../consts";
</script>

<input type=text name={`flags.${MODULE_NAME}.${CONSTS.flags.circle.areaType}`} />
It basically looks like your current approach is overcomplicated / not using Svelte for its strengths.
david aka claudekennilol
What is the action prop?
This is an Item-like object defined by the Pathfinder system. The action itself could change, but the specific data I'm reading off it basically won't change unless someone is doing that manually. Outside of a macro (or some other mod that visualizes Item data), the only way to edit the data I'm specifically using in the svelte component is directly in the component (and few system UI inputs that I'm hiding and providing different inputs for).
What is the current state of the code branch?
Just to recap - previously the mod opened all of my extra settings in a svelte modal and those modifications were saved when the user hit "ok" on the modal. The goal of this branch is to instead inject my settings directly in the system's Item's sheet and save as modifications are made (or more probably, debounced so they're saved shortly after modifications are made). Answer - so far I've refactored it so that my inputs are now instead within the Item's sheet and I'm happy with the layout. I'm still working on the best way to use a store and save the modifications as they're made.
I gather this block runs immediately and subsequently isn't triggered:
That was me testing to see if that was viable, I was guessing it wasn't, but it was just a quick test (and it wasn't a viable option). It did actually run some times, but not in some cases. I thought I had already removed that. looking back now I see that I forgot to push that commit 😅
Is there a reason you don't wrap the settings components in a form and have an on:submit handler to then process all of the form data?
Yes there's a reason, that was the method when it was its own modal, but now that I'm injecting it into the Item's sheet directly, I want to adhere to how users already expect the item sheet to behave (which is that it saves directly as changes are made). Now that I'm injecting it into the system's sheet, it's already contained within a form that I'm not "in charge of"
TyphonJS (Michael)
TyphonJS (Michael)OP•10mo ago
Yeah.. I guess that is true that you can't have a form inside a form. Hmm...
david aka claudekennilol
Lastly, I'm using a slot because this (see image) is how I want the layout. The dark red outline is the shared template. The blue is the circle template--basically there are checkboxes that I want to keep grouped together. So any checkboxes that are just yes/no are all grouped together at the bottom whether they're from shared or from the specific template shapes/types
No description
TyphonJS (Michael)
TyphonJS (Michael)OP•10mo ago
Re: injecting input elements. It's quite possible that if you format the input elements correctly w/ name and targeting the correct flag attribute that Foundry in processing the form will update the underlying document without you having to do anything. IE when it's closed or normally saved by Foundry. Some of the components have name defined in various input elements, but not all. Does this cause a conflict w/ the default Foundry handling of a FormApplication?
david aka claudekennilol
I honestly have no idea what the default way Foundry handles that is (I've never had to write a document sheet from scratch before). Their API isn't exactly the easiest to read if you don't already know what you're looking for. Are you saying that it's probable I can just fill the data path into the name attribute and it may save without me manually intervening?
TyphonJS (Michael)
TyphonJS (Michael)OP•10mo ago
What app are you injecting into? It's likely a FormApplication for sure.
david aka claudekennilol
ItemActionSheet (pf1-specific sheet) I know all the various inputs all already have name filled in (though I don't know how it handles radio buttons because PF1 doesn't have radio buttons on any of its sheets) Oh yeah, I didn't see the second part, I believe it is a FormApplication Yes, I just double checked, it is a FormApplication
TyphonJS (Michael)
TyphonJS (Michael)OP•10mo ago
The name in a standard FormApplication / DocumentSheet, etc. should follow the object access path IE x.y.z. Having not looked at PF1 the default DocumentSheet from Foundry has _updateObject as this:
async _updateObject(event, formData) {
if ( !this.object.id ) return;
return this.object.update(formData);
}
async _updateObject(event, formData) {
if ( !this.object.id ) return;
return this.object.update(formData);
}
You might want to put a debug console log statement there in DocumentSheet or any further overridden _updateObject in the PF1 ItemActionSheet to verify that no additional data from the injection is being picked / potentially serialized. My assumption that anything w/ a name field inside of a form in FormApplication / DocumentSheet may be picked up. The PF1 ItemActionSheet has a _updateObject implementation. It's possible that any of your injected input elements with name defined will be picked up in the normal form submission.
david aka claudekennilol
Yeah I saw that. I've updated everything to have an appropriate name, but something is triggering itself over and it's just saving continuously now..
TyphonJS (Michael)
TyphonJS (Michael)OP•10mo ago
Well.. I would strip things down to a very simple injection with one additional input element and try and divide and conquer from there to find what may cause the loop. It doesn't look like you have the $destroy handling in any render / close hooks for ItemActionSheet for instance.
david aka claudekennilol
I realized what was happening with the data being saved weird. I was calling jq.hide() for the inputs that I was replacing, but since they were still in the dom (just hidden) foundry was saving the values using the same name attribute (mine plus the now-hidden system's inputs) as an array under that property. calling jq.remove() for those system elements resolved it. I am calling $destroy, it's just in a hook outside of that "svelte folder". I haven't looked into memory management yet, but I can say that the svelte component is being re-added every time the render hook is called (which is whenever anything for the Item is being saved) and is only being destroyed once at the end. But I'll do some debugging later and see what the memory snapshots say I've basically got these two methods that are called for the render and close hooks
const injectTemplateSelector = async (sheet, jq, _options) => {
//** is there any reason to not add this? if an existing sheet is re-rendered, its render hook is called with the existing sheet so the injected svelte component is still alive */
if (typeof sheet?._templateSettings?.$destroy === 'function') {
sheet._templateSettings.$destroy();
}
//** end */

// snipped, figure out which sheet to inject
sheet._templateSettings = injected(templateGroupOptions, sibling, action);
}

const destroyTemplateSelector = (sheet, ...args) => {
if (typeof sheet?._templateSettings?.$destroy === 'function') {
sheet._templateSettings.$destroy();
}
}
const injectTemplateSelector = async (sheet, jq, _options) => {
//** is there any reason to not add this? if an existing sheet is re-rendered, its render hook is called with the existing sheet so the injected svelte component is still alive */
if (typeof sheet?._templateSettings?.$destroy === 'function') {
sheet._templateSettings.$destroy();
}
//** end */

// snipped, figure out which sheet to inject
sheet._templateSettings = injected(templateGroupOptions, sibling, action);
}

const destroyTemplateSelector = (sheet, ...args) => {
if (typeof sheet?._templateSettings?.$destroy === 'function') {
sheet._templateSettings.$destroy();
}
}
TyphonJS (Michael)
TyphonJS (Michael)OP•10mo ago
Good good.. You'll soon be the foremost expert around here on embedding Svelte components in stock Foundry FormApplications. 😄
Re: and is only being destroyed once at the end
You can add an onDestroy callback just to log that the $destroy takes affect when desired. In template-settings.svelte add:
<script>
import { onDestroy } from 'svelte';

onDestroy(() => console.log('!! TemplateSettings component destroyed'));
</script>
<script>
import { onDestroy } from 'svelte';

onDestroy(() => console.log('!! TemplateSettings component destroyed'));
</script>
That should provide enough proof without having to get into comparing memory snapshots and such. Of course doing analysis of retained memory is the truly full view of what is going on and a good skill to have in general regardless of programming language. --- In general I'd say it's best to try and not use JQuery wherever possible in ones code by converting or feeding your additional methods with the underlying element from Foundry. Eventually JQuery might dissapear from Foundry, but the modern ES / DOM support makes JQuery essentially irrelevant. IE there is a remove() method on all DOM elements.
david aka claudekennilol
In general I'd say it's best to try and not use JQuery wherever possible in ones code by converting or feeding your additional methods with the underlying element from Foundry. Eventually JQuery might dissapear from Foundry, but the modern ES / DOM support makes JQuery essentially irrelevant. IE there is a remove() method on all DOM elements.
Yeah I totally agree. Most of the time I usually avoid jquery and do straight dom manipluation. I threw that in quickly but thanks for the reminder to go back in and update it.
Good good.. You'll soon be the foremost expert around here on embedding Svelte components in stock Foundry FormApplications. 😄
Yay 😅 lol. This was good practice for my other mod where I'm doing something similar. I'm not sure if the result is similar enough to what I was originally aiming for though. I guess I'll find out ¯\_(ツ)_/¯
TyphonJS (Michael)
TyphonJS (Michael)OP•10mo ago
Yeah.. It would be nice to separate the injected input elements and handle them in a more reactive manner. It likely is possible by omitting the name attribute for injected input elements as they shouldn't get picked up in the Foundry form processing. This does create a lot of additional complexity in coming up with a way to handle things reactively. In the near future with Svelte 5 this can be a bit more viable, but implementing something for Svelte 4 will be a bit more messy and probably isn't worth the hassle. IE just accepting that any changes to your data will go through the normal Foundry form processing.
david aka claudekennilol
I was able to confirm that I could still save my data separate from the sheet handling the form data before I added/fixed all the name attributes. And in this case simply using the name attribute worked--I'm not sure it will in my other case though
TyphonJS (Michael)
TyphonJS (Michael)OP•10mo ago
Maintainability and coming back to this code the next time is the main consideration. It's definitely an interesting use case.
david aka claudekennilol
Yeah using the name attribute relies on the system's implementation of _updatedObject for this FormApplication. It probably won't change in a way that would break this, but there's no guarantee (obviously) At the very least, I'm happy that I was able to get the template injected into the native view seemlessly without having to resort to hbs
TyphonJS (Michael)
TyphonJS (Michael)OP•10mo ago
I'm a bit hesitant to suggest this, but there is a reasonable way to handle this w/ TRL and a 3rd party helper. One thing that you can do now with TRL is create a custom object store using propertyStore which is an additional 3rd party store helper. This file exports createUpdatesStore which you pass the Foundry doc to initialize the store in the top level Svelte component. createUpdatesStore.js:
import { writable } from 'svelte/store';
import { propertyStore } from '#runtime/svelte/store/writable-derived';

import { prepareData } from './prepare-template-data.js';

// `action` is the Foundry doc.
export function createUpdatesStore(action) {
const updatesStore = writable(prepareData(action));

// Creates a custom readable object store, with properties that are writable stores.
const updates = {
subscribe: updatesStore .subscribe,

data: {
measureTemplate: {
customColor: propertyStore(updatesStore, ['data', 'measureTemplate', 'customColor']),
customTexture: propertyStore(updatesStore, ['data', 'measureTemplate', 'customTexture']),
overrideColor: propertyStore(updatesStore, ['data', 'measureTemplate', 'overrideColor']),
overrideTexture: propertyStore(updatesStore, ['data', 'measureTemplate', 'overrideTexture'])
}
},
// Do the same for flags...
};

return updates;
}
import { writable } from 'svelte/store';
import { propertyStore } from '#runtime/svelte/store/writable-derived';

import { prepareData } from './prepare-template-data.js';

// `action` is the Foundry doc.
export function createUpdatesStore(action) {
const updatesStore = writable(prepareData(action));

// Creates a custom readable object store, with properties that are writable stores.
const updates = {
subscribe: updatesStore .subscribe,

data: {
measureTemplate: {
customColor: propertyStore(updatesStore, ['data', 'measureTemplate', 'customColor']),
customTexture: propertyStore(updatesStore, ['data', 'measureTemplate', 'customTexture']),
overrideColor: propertyStore(updatesStore, ['data', 'measureTemplate', 'overrideColor']),
overrideTexture: propertyStore(updatesStore, ['data', 'measureTemplate', 'overrideTexture'])
}
},
// Do the same for flags...
};

return updates;
}
For a clean implementation you might want to move all of the custom store creation above into prepareData and have prepareData return the updates store. You can then do the following in template-settings.svelte:
import {
onDestroy,
setContext } from 'svelte';
import { subscribeIgnoreFirst } from '#runtime/util/store';
import { createUpdatesStore } from './createUpdatesStore.js';

// This is the backing Foundry document.
export let action;

const updates = createUpdatesStore(action);

// This sets a context for the `updates` store that is retrieved by `getContext('updates')` in child components.
setContext('updates', updates);

// In your unsubscribe / destroy handler you need to keep track of unsubscribe.
const unsubscribe = subscribeIgnoreFirst(updates, (updateData) => action.update(updateData.data, { render: false }));

onDestroy(() => unsubscribe());
import {
onDestroy,
setContext } from 'svelte';
import { subscribeIgnoreFirst } from '#runtime/util/store';
import { createUpdatesStore } from './createUpdatesStore.js';

// This is the backing Foundry document.
export let action;

const updates = createUpdatesStore(action);

// This sets a context for the `updates` store that is retrieved by `getContext('updates')` in child components.
setContext('updates', updates);

// In your unsubscribe / destroy handler you need to keep track of unsubscribe.
const unsubscribe = subscribeIgnoreFirst(updates, (updateData) => action.update(updateData.data, { render: false }));

onDestroy(() => unsubscribe());
Then in your child Svelte components:
<script>
import { getContext } from 'svelte`;

const updates = getContext('updates');

// To bind stores you must create a top level variable to use `$` in the binding.
const customColor = updates.data.measureTemplate.customColor;
</script>

<input type=text bind:value={$customColor} />
<script>
import { getContext } from 'svelte`;

const updates = getContext('updates');

// To bind stores you must create a top level variable to use `$` in the binding.
const customColor = updates.data.measureTemplate.customColor;
</script>

<input type=text bind:value={$customColor} />
The above is all pseudo-code I just typed out, so there could be problems, but it is pretty close to how it should be; just haven't tested it in this particular scenario. This is a way forward to create a custom object store that should be more manageable / less maintenance burden than going a more bespoke route. Of course you'd have to know about all of these techniques that goes beyond the standard Svelte store helpers. This is where TRL provides additional flexibility. The bindings in the input elements are actually stores and any changes to the child property stores will receive an update to the main updates store which is subscribed to in template-settings.svelte. subscribeIgnoreFirst does what it says. The callback is not triggered on registration, but only subsequent updates. With Svelte 5 you will be able to create a custom stateful object using $state, but the result will be very similar to the above route that is currently possible. You can read more about svelte-writable-derived as it is a 3rd party library added to TRL: https://github.com/PixievoltNo1/svelte-writable-derived. It is exported in TRL from '#runtime/svelte/store/writable-derived'. Let me know if you are going to try any of the above as I can help you refine things as necessary... I changed the above pseudo-code to be a bit more accurate. I still haven't run / tested this code, but it should be pretty close to the pattern to follow now.
david aka claudekennilol
Thanks, I'll definitely take a look at this as I like the idea of being more in control of saving it rather than relying on the system's FormApplication._updateObject implementation. In your last example, would <input type=text bind:value={$updates.data.measureTemplate.customColor} /> not work? It specifically needs to be a separate top level variable?
TyphonJS (Michael)
TyphonJS (Michael)OP•10mo ago
It should work in Svelte 5 when creating a $state-ful data structure. The Svelte 4 compiler when you do $update.data.store Applies the store to update and not the child property access. I've always thought something like this $(update.data.store) could have been supported, but I never got around to prototyping it and bringing this up. The Svelte 3 & 4 compiler has some limitations. A lot of that goes away with Svelte 5 as things can be designed in a more explicit manner. This comes at the expense of just "magic" happening. IE $: reactive statements are going away. The old way of working with Svelte will still be supported for Svelte 5, but likely deprecated by Svelte 7. You can likely find a bit of info on Svelte 5 now. They are calling the new compiler directives "runes".
Want results from more Discord servers?
Add your server