3

I'm building a tree component where nodes can be moved around. The component and data structure are similar to the one used in the Tree View example in the Vue.js documentation.

The data structure looks like this (irrelevant properties removed):

[
  {"id": 1, "children": []},
  {"id": 2, "children": []},
  {"id": 3, "children": [
    {"id": 4, "children": [
      {"id": 5, "children": []},
      {"id": 6, "children": []}
    ]}
  ]}
]

The nodes represent "folders" that can be folded or expanded in the view. The FolderNode component looks like this:

export default {
  name: 'FolderNode',
  props: {
    node: { type: Object, required: true },
  },
  data: () => ({
    expanded: true,
  }),

  methods: {
    toggleExpand() {
      this.expanded = !this.expanded;
    },
  },
};

The template is:

<template>
  <li>
    <span class="node-icon">
      <span @click="toggleExpand">[{{ expanded ? '-' : '+' }}]</span>
    </span>
    <span class="node-label">{{ node.id }}</span>
    <ol
      v-if="node.children && node.children.length"
      v-show="expanded"
    >
      <FolderNode
        v-for="child in node.children"
        :key="child.id"
        :node="child"
      />
    </ol>
  </li>
</template>

This part works fine. I added drag and drop functionality to move nodes around (not shown above for simplicity) in the tree. When nodes are removed and inserted elsewhere, Vue.js instantiates new FolderNode components automatically to reflect the changes. These new FolderNode instances are created (when a node is moved to a different parent) with the default expanded state to true. I was hoping the :key property would work across different parents, but it only reuses components (and keeps their expanded state) for children of the same parent FolderNode.

So, how can I keep the folders' expanded state (and other display state) when they are moved?

The tree object will be used by other parts of the application so I can't add an expanded property directly to its nodes which would be the easiest solution. Besides, expanded is strictly "display state" and has nothing to do with the tree data.

I've thought of 2 possible solutions which I don't find really appealing:

  1. Create a parallel tree structure mirroring the data tree that holds the display state. Pass this to the folder components and have them query that to know their state.
  2. Use a map to map nodes of the data tree to a state object. The problem is that maps are not reactive in Vue.js so I'd have to resort to ugly hacks to make it work.

Any other ideas?

10
  • I take it node.id is a :key? It doesn't change while moving node? Commented Jan 22, 2019 at 8:29
  • @Styx That's right :key="child.id" refers to a node.id property Commented Jan 22, 2019 at 8:31
  • @Styx and it doesn't change when moving nodes. The node objects themselves are also kept when they are moved. That is I'm not creating new objects when nodes are moved. Commented Jan 22, 2019 at 8:40
  • This contradicts with what you said in question: New FolderNode instances are created when a node is moved to a different parent with the default expanded state to true. Commented Jan 22, 2019 at 8:43
  • Ah, I see, your expanded is not stored in node object. Why, though? Commented Jan 22, 2019 at 8:44

2 Answers 2

1

We faced a similar problem when we build a tree component. We had the exact same thought.

Initially, we created a parallel data structure and with the help of Ramda, we would merge the internal data model external tree structure. But this proved not very elegant. And, tree conciliation is very difficult to achieve.

But, if you take another perspective on this approach, then it makes sense to make expanded as part of the tree Node object. There are multiple use cases for this:

  • Sometimes you might want to show the first Node as expanded
  • During the search operation, all the Nodes with specific name must expand the way it happens in most of the code editors.
  • When used as a file explorer, nodes containing specific files must be expanded by default.

And there are many other cases where it proves useful. We are using TypeScript, we simply declare that property as optional like:

export interface TreeNode<T extends any> {
    label: string;
    expanded: boolean;
    children: Array<TreeNode<T>>;

    isDisabled?: boolean;

    // Holds the state if tree/node selected or not
    select?: boolean;

    // Unique key which identifies each node
    _id?: string;

    // Any other data that needs to be stored
    context?: T;
}

As far as API is concerned, they need not be bothered about these additional keys as they are optional. Thus, it becomes trivial to support drag-n-drop with expanded state

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for your insight. You are right about the useful use cases. I'd like to be able to have multiple independent "component trees" that display the same tree though. Each one would have a separate set of expanded states per node. Maybe I can live without that however.
Since you faced a similar situation, would you spare 2 minutes to look at the other solution I just posted? It seems like it solves the separate state per tree issue neatly, but I might have missed something.
0

Another possibility is to go with option 2 without using a map. In fact the map, while semantically correct, turns out to not be needed at all!

The "root" component creates a "tree view state" object that holds the state of expanded of each node. Something like:

const treeViewState = {};
// Visit each node and create its initial view state object to make it reactive
walkNodes(tree, (node, level) => {
  treeViewState[node.id] = {
    expanded: level < 1, // Simple logic to only expand the first level by default
  };
});

The "root" component then provides the treeViewState to its children components through Vue's injection mechanism.

Each FolderNode then accesses its display state through its node's unique id. This is used to index the treeViewState object.

2 Comments

This looks promising. While I don't see a problem with Vue provide/inject mechanism, I prefer that it be used only for injecting functions or constants rather than actual data. But it is just my historical context that forces me to have this prejudice. Also, if the tree changes, do you plan to drop all the keys from treeViewState object? Due to Vue provide/inject, you cannot create a new object. Maybe consider injecting ES Proxy instead of an actual treeViewState object.
@HarshalPatil Regarding the reactivity, I think it can work if I inject an accessor function instead of the treeViewState object directly. This is what @chekmare did in his demo. Regarding dropped nodes, I have two options: 1. ignore it and take the (small) memory leak hit. 2. Remove the state of a node when it is removed from the tree.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.