grapoza / vue-tree

Tree components for Vue 3
MIT License
95 stars 12 forks source link

How to access and modify the parent state of a node? #285

Closed monfortm closed 1 year ago

monfortm commented 1 year ago

On v3.0.5

Similar to #260, but I ask because I could not really work with the solution proposed there.

I want to have a custom behavior on checking nodes, based on multistate checkboxes (e.g. with an integer as checkbox input value in place of a boolean). Part of this behavior should recursively set state on the parent chain when a child is checked.

I was so far working with a trick where I inject the parent into the child (I obtained the parent from the loadAsyncChildren function), and was then recursively accessing node?.parent to set needed state. This was working surprisingly well. However, this is causing a bug when involving vuex in there...

I am developing some sort of wizard where users first use the tree to select some nodes (i.e. file system like) before they navigate to the next page. I need vuex to keep the state of the tree in memory regardless of the lifecycle of the tree component itself (which gets destroyed when navigating away). I want users to be able to navigate back and be presented with the tree as they left it (certain children expanded, certain node selected, etc). Problem is I think these circular reference cause a bug, i.e. I get a weird vuex bug which disappears as soon as I drop the parent key of a node, but then I lose the wanted behavior.

First, a suggestion: If the parent is not meant to be accessed as I was doing it, maybe make a copy of it before injecting it as parameters, so that one cannot successfully modify parent.treeNodeSpec.state.input.value and get away with it! Well you might also as well keep it like this since it works...

How are we expected to access and modify a node parent state, recursively?

monfortm commented 1 year ago

One way: Store parent ID in node and use getMatching( (id) => parentID === id ), set state and repeat all along the parent chain. Not so satisfying in terms of resource usage

grapoza commented 1 year ago

TL;DR, the tree has always only had one-way navigation, downward. There's not much of a reason for that except that I hadn't yet had a need for parent hierarchy references (ironically, until last Friday). As part of The Great Refactoring I'm going to add more/better iteration over the tree and expose those methods, but for now I've had to take an approach of:

1) Add the parentId to the nodes (it's actually already in my data set, and it looks like you have that under control for yours) 2) Operate directly against the tree model object to find the hierarchy to the desired node (I think I based the work off of these SO answers, but the implementation I'm thinking of is on my work device and not close at hand). So basically handle the checked event, traverse the hierarchy to find the node, and do work on the ancestors on the way back up.

As you saw, getMatching is only so much help and ends up repeating work, though yeah it can technically do the job. The good(?) news is that anything I write and expose for 5.x can easily go into 3.x as well since the actual data structures haven't changed in a significant way.

First, a suggestion: If the parent is not meant to be accessed as I was doing it, maybe make a copy of it before injecting it as parameters, so that one cannot successfully modify parent.treeNodeSpec.state.input.value and get away with it! Well you might also as well keep it like this since it works...

Nope, go ahead and mess with that data to your heart's content.

Problem is I think these circular reference cause a bug

Yeah, Vuex hates that. :) It's been a while since I worked against Vuex, I can't remember any way to make it so the store contains the model without the parent node references but then rehydrates those when it's read from the store. If you think of one please let me know, now I'm curious.

monfortm commented 1 year ago

Well, if I follow the current best SO answer well, storing the parent ID is not necessarily needed since upon searching a node, the IDs of all traversed nodes are accumulated.

This solution makes total sense once you think about it, lol. Having each node referencing their parent just is more convenient, and avoid top-down navigation each time. I guess for my use case this could work tho. I have yet to try. Thanks for the answer and the SO pointer.

monfortm commented 1 year ago

OK so I close here, I think I will take the "find node keeping traversed path" approach.

  1. Recursively find a node, recording its path (list of parent nodes IDs)
  2. Use list of parent nodes IDs to access each parent - probably with getMatching( (id) => parentID === id ) - to toggle selection

No idea how getMatching is implemented, but I imagine it will traverse again the tree - what is done in (1) already - to find each parent based their IDs. Maybe 1 can store reference to nodes instead of just IDs, then 2 does not need to happen (?) - I will try this.

Also - in v5 - it would be cool to have:

  1. a better node API, like node.expand(), node.reload(), node.check(), node.select(), etc. so that we don't necessarily have to dig into the objects
  2. a tree.getPath(nodeId) that either return nodeIds or node references to use with the node API

But again, not sure how easy it is to provide considering the lib handles more than just the use case I have in mind rn!

Also - unrelated - I love the scoped slot approach to fully customise the node html!

Anyways, thank you again! :v:

grapoza commented 1 year ago

Sounds good. And yeah, part of what I'm working on right now for 5.x is a refactor into composables for a lot of the concerns (selection, focus, etc) which has already simplified a good chunk of the internals from the just-keep-tacking-stuff-onto-mixins approach that existed before. One of the main things I want out of that is a better API both for interacting with nodes and for traversing the tree.

I should also note that there was a 5.x enhancement for getMatching to add a parameter for max matches to allow it to short-circuit the tree traversals, for instance if you were looking for the one parent node by ID. If you want, I can pretty trivially add that to 3.x for you. Just put in an enhancement issue for it if you think it'd be nice to have!

monfortm commented 1 year ago

Thanks for the details. Good to know, I will spawn a new issue when and if needed in this case :)

Yeah, I also can't wait to be using the composition API but the codebase I am working on needs to be migrated... That's not going to be an easy task ^^