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)
Comp2 (shows only items of type trait
):
This works for only the first tab clicked and thereafter is non-reactive.
I had a go at writing it to the documentStore
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
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:
Wonderful. I don't understand it properly but I have a working search filter that let's me discombobulate the items list at will!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.
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?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.Sweet! I was able to get it working. Copied your
createFilterQuery
utility and just edited the filterQuery.set
and filterQuery
functions. Works 🔥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?
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!
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.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?
Is it from here?
I guess it must be.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.