T
TyphonJS•2y ago
geoidesic

How to reset filter for Embedded collection?

I'm trying to allow the items list to be filtered by tabs / buttons for the type of item, rather than via a text field. My tabs are two separate components. So I have registered the typeFilter in both Comp1 (shows all items)
const typeSearch = createFilterQuery("type");
onMount(async () => {
console.log("inventory onMount");
typeSearch.set("");
});
const typeSearch = createFilterQuery("type");
onMount(async () => {
console.log("inventory onMount");
typeSearch.set("");
});
Comp2 (shows only items of type trait):
const typeSearch = createFilterQuery("type");
onMount(async () => {
console.log("inventory onMount");
typeSearch.set("trait");
});
const typeSearch = createFilterQuery("type");
onMount(async () => {
console.log("inventory onMount");
typeSearch.set("trait");
});
This works for only the first tab clicked and thereafter is non-reactive. I had a go at writing it to the documentStore
const doc = getContext("#doc");

const nameSearch = createFilterQuery("name");
$doc.system.filters.inventory.typeSearch = createFilterQuery("type");
$doc.system.filters.inventory.typeSearch.set("");

$: console.log(nameSearch);
$: console.log($doc.system.filters.inventory.typeSearch);

const input = {
store: nameSearch,
efx: rippleFocus(),
placeholder: "*",
type: "search",
};

/** @type {import('@typhonjs-fvtt/runtime/svelte/store').DynMapReducer<string, Item>} */
const wildcard = doc.embedded.create("Item", {
name: "wildcard",
filters: [nameSearch, $doc.system.filters.inventory.typeSearch],
sort: (a, b) => a.name.localeCompare(b.name),
});
const doc = getContext("#doc");

const nameSearch = createFilterQuery("name");
$doc.system.filters.inventory.typeSearch = createFilterQuery("type");
$doc.system.filters.inventory.typeSearch.set("");

$: console.log(nameSearch);
$: console.log($doc.system.filters.inventory.typeSearch);

const input = {
store: nameSearch,
efx: rippleFocus(),
placeholder: "*",
type: "search",
};

/** @type {import('@typhonjs-fvtt/runtime/svelte/store').DynMapReducer<string, Item>} */
const wildcard = doc.embedded.create("Item", {
name: "wildcard",
filters: [nameSearch, $doc.system.filters.inventory.typeSearch],
sort: (a, b) => a.name.localeCompare(b.name),
});
But no change in behaviour. Another problem is that even if I just use the type field as per the EmbeddedDocAppShell sample, and put it on one of my tabs... it only works when that tab is first viewed. Any time I click to a different tab and then back to that tab, the filter no longer works and will be stuck with whatever it's previous search results provided. Typing into the search field after that point yields no change. Even if I close and re-open the Actor Sheet after that, the filter will still be broken. Only a page refresh fixes it.
10 Replies
geoidesic
geoidesicOP•2y ago
The original sample requires the Actor to be dropped onto the doodad, thus creating a new document store from the Actor data. Since I wanted to use that for inventory, I changed it so that it reads the document store using getContext but that's what seems to cause this breaking behaviour. It's as if the filter gets stuck in the actor store and then can no longer be changed. I literally copied the EmbeddedDocAppShell.svelte code and pasted it into my Inventory component (which works fine but requires a drag and drop of the Actor to show the inventory list). Then I just replaced const doc = new TJSDocument(); with const doc = getContext("Actor"); which populates the list from the current actor, but also causes the problem described. I'm not sure why nor how to prevent fix the behaviour. Well now, now then, then now! I managed to fix it with this little bit of magic:
const Actor = getContext("#doc");
const doc = new TJSDocument($Actor);
const Actor = getContext("#doc");
const doc = new TJSDocument($Actor);
Wonderful. I don't understand it properly but I have a working search filter that let's me discombobulate the items list at will!
TyphonJS (Michael)
TyphonJS (Michael)•2y ago
There is a large difference between getting something to work while not understanding it versus clean code that won't cause a maintenance problem or other side effects. I gather you are still doing things incorrectly and at best inefficiently despite it seemingly working.
geoidesic
geoidesicOP•2y ago
On this same topic. How can I get the filter to do exclusions instead of inclusions? And / or sets including items from multiple type 's?
TyphonJS (Michael)
TyphonJS (Michael)•2y ago
Provide a function that takes in a document / object and returns true or false depending on whether it is filtered or not. The exclusion / inclusion logic is up to you to create. This is pretty much the same mechanism as Array.filter. You can add multiple filter functions, but just one sort function. Internal details: Instead of creating a new array / collection upon filtering the dynamic reducers library non-destructively creates an index over an associated array or Map such that the iterators returned use this index to provide the filtered / sorted order. IE the underlying embedded collection in Foundry which ultimately is a Map is not modified, but the dynamic reducer API provides a window over that data depending on the sort / filter functions you are applying. createFilterQuery is a helper method that returns one such function, but does other fancy stuff like also making that function a Svelte store, so that you can directly modify the query from any component that takes a store. I'd suggest working with direct functions at first before you try and duplicate what createFilterQuery is doing. General warning though you are treading into an area where preciseness matters in the filter / sorting code you provide. ----- Sans documentation here is the location of the dynamic reducers library. The code does have adequate comments, but no README overview. It does have 100% test coverage. https://github.com/typhonjs-node-utils/dynamic-reducer Although probably generally confusing examining the tests will show the full extent of what is possible. The dynamic-reducer library is exported from @typhonjs-fvtt/runtime/store. Imports like DynArrayReducer / DynMapReducer can be used with any array or Map. However, there is a bridge to hooking it up through TJSDocument and Foundry embedded collections that you are currently using in the "adhoc" manner. There is an even cooler mechanism to provide a custom derived reducer implementation that you have more control over and can create your own derived data API. That will have to be saved to discuss another day.
geoidesic
geoidesicOP•2y ago
Sweet! I was able to get it working. Copied your createFilterQuery utility and just edited the filterQuery.set and filterQuery functions. Works 🔥
TyphonJS (Michael)
TyphonJS (Michael)•2y ago
So, a small "homework" assignment. In your own words can you describe what you created? This little bit of code uses a lesser known feature of JS (what do you think that is?) and is instructive on a primary contract for adding reactivity in Svelte; what is that and what specifically makes it so?
geoidesic
geoidesicOP•2y ago
Well.. I don't know what you have in mind, since I don't read minds 🙂 so I won't play the guessing game but it all looks like pretty standard JavaScript to me. All I was missing was context, which you provide amply above, so thanks for that. The only uncommon thing I noticed in createFilterQuery that I could point out might be the use of assigning properties to a function. As for what I created – a working inventory for the character sheet 🙂 But feel free to share what you had in mind. I know ya wanna!
TyphonJS (Michael)
TyphonJS (Michael)•2y ago
I think a good stepping stone is being able to describe what you create particularly if copying / tweaking others example code is part of the process. Indeed the "lesser known feature" is recognizing that in JS functions are "first class objects". The continuation of this concept is realizing that you can create custom stores from any construct that is like an object where you can add at minimum a subscribe function to make a "readable" store and a set function making it a "writable" store. In Svelte the readable / writable helper functions from import { readable, writable } from 'svelte/store'; are just helper or basic implementations. Another aspect is that this is a basic example of a "higher order function" and that is a function that returns a function. This works out great as you can create a filter or sort function that also is a store encapsulating the state used internally allowing it to serve a dual purpose where you don't have to have any external glue / management code to connect the function to dynamic reducers and any mechanism in Svelte templates / components to update that internal state controlling the operation at hand. A follow on to this is that internally the dynamic reducers detect if the functions added for filtering / sorting are Svelte stores by checking for a subscribe function / attribute. That is how when the internal state of the filter / sort functions changes the dynamic reducer automatically updates. The dynamic reducers are also readable Svelte stores. When you use the TJSDocument.embedded API what you are manipulating / hooking up is a DynMapReducer instance connected to a Foundry embedded collection which is a Map. More or less from an architecture point of view you may find this technique used and useful on the periphery of TRL. It's not a technique (making optional functions stores) that I use internally to TRL in other areas, but a useful mechanism to hook together different APIs and your own logic code in your projects. A good article that delves into custom stores in Svelte: https://monad.fi/en/blog/svelte-custom-stores/ The store contract is the basic glue for reactivity in Svelte and beyond allowing you to create stores with custom logic and / or hook up other state management APIs as well making them Svelte compatible. It essentially is a stripped down Observable pattern. You can create a Svelte store with pure JS code without importing anything from the Svelte library by implementing this contract. A Svelte store can be used from JS , but you do have to manage the subscribe / unsubscribe process. The "magic" the Svelte compiler adds is that when you use $ preceding a store in a Svelte component is that it handles the subscribing / unsubscribing automatically for you for the lifecycle of that component. When that component is destroyed under the hood any stores that automatically have been subscribed to are also automatically unsubscribed from without needing to manage this process explicitly.
geoidesic
geoidesicOP•2y ago
I'm looking at this again because I need to create a new filter and I realised that I don't understand where the data comes from for the filter? This is the function. Where does the data come from?
function filterQuery(data) {
if (keyword === '' || !regex) { return true; }

if (isIterable(properties)) {
for (const property of properties) {
if (regex.test(normalizeString(data?.[property]))) { return true; }
}
return false;
}
else {
return regex.test(normalizeString(data?.[properties]));
}
}
function filterQuery(data) {
if (keyword === '' || !regex) { return true; }

if (isIterable(properties)) {
for (const property of properties) {
if (regex.test(normalizeString(data?.[property]))) { return true; }
}
return false;
}
else {
return regex.test(normalizeString(data?.[properties]));
}
}
Is it from here?
const wildcard = doc.embedded.create("Item", {
name: "wildcard",
filters: [nameSearch, typeSearch],
sort: (a, b) => a.name.localeCompare(b.name),
});
const wildcard = doc.embedded.create("Item", {
name: "wildcard",
filters: [nameSearch, typeSearch],
sort: (a, b) => a.name.localeCompare(b.name),
});
I guess it must be.
TyphonJS (Michael)
TyphonJS (Michael)•2y ago
Yes... The last bit above is the adhoc way of setting up a reactive embedded collection for Item documents. data will be an Item document.
Want results from more Discord servers?
Add your server