Search code examples
vue.jsrenderreactiveslots

How to update tree view with VUE


I have a problem on a tree components structure managerment. Like a tree have many nodes, when I delete a node, the render function is run , but the page does not display the correct info according to the tree data.

I attached the source code here, pls anyone can help me out of this confuing situations. Many thanks in advance.

Code sample: This is the tree render component, the tree data is defined in store.state.tree, and I mark it as reactive here, every time I delete a node in the tree, the render function is called, I saw from the console.log('render')

import Node from "./Node";
import {reactive, h} from 'vue';
import store from '@/store';

export default {
  name: 'Tree',
  components: {Node},
  data(){
    return reactive({
      tree: store.state.tree
    });
  },
  render(){
    console.log('render');
    let tree = this.tree;
  
    let renderChildNodes = (node)=>{      
      let vnodes = [];
      node.nextnodes.forEach(node => {
        vnodes.push(renderChildNodes(node));
      });
      let vnode = h(Node,{node: node}, 
        {
          default: ()=>{
            return vnodes;
          }
        }
      );
      return vnode;
    };
    return h('ul', null, renderChildNodes(tree));
  }  
}
</script>

This is the node code, I get the node from default slot's props, and also mark it as reactive. Then I just delete the node from the store.state.tree. I hope the tree will refresh the view, but it's seems that the node is actually deleted, but the deleted node in the view is not updated.

<template>
<li>
  <div class="node">
    {{state.node.id}}
    <input v-model="state.node.msg"/>
    <button @click="add(state.node)">add</button>
    <button @click="del(state.node)">del</button>
  </div>
  <ul>
    <li>
      <slot :node="node"></slot>
    </li>
  </ul>
</li>
</template>

<script>
import {onMounted, reactive} from 'vue';
import store from '@/store';

export default {
  name: 'Node',
  props: {
    node: {
      type: Object,
      required: true
    }
  },
  setup(props){
    console.log('node setup');
    const state = reactive({
      node: props.node,
    })
    onMounted(()=>{
      console.log('node onMounted');
    })
    function add(node){
      let sub_node = JSON.parse(JSON.stringify(node));
      sub_node.id = node.id + "-" + (node.nextnodes.length+1);
      sub_node.nextnodes = [];  
      node.nextnodes.push(sub_node);
    }
    function del(node){
      findAndRemoveNode(store.state.tree,node);
      console.log(store.state.tree.nextnodes);
    }
    return{
      state,
      add,
      del
    }
  }
}
function findAndRemoveNode(root, target){
  if(!root.nextnodes.findremove(target)){
    for(let i=0; i<root.nextnodes.length; i++){
      if(findAndRemoveNode(root.nextnodes[i], target))
        return true;
    }
  }else{
    console.log(`removed node:` + target.id);
    return true;
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="scss" scoped>
.node{
  background-color: #e6e4e4;
  text-align:left;
  &:hover{    
    background-color: #c0bdbd;
  }
}
li{
  margin: .2em;
}
</style>

This is the store code:

import { createStore } from 'vuex';

export default createStore({
    state:{
        tree: {
            msg: "this is root.",
            nextnodes: [{
                id:"1",
                msg: "node 1",
                nextnodes: []
            },{
                id:"2",
                msg: "node 2",
                nextnodes: []
            },{
                id:"3",
                msg: "node 3",
                nextnodes: []
            },{
                id:"4",
                msg: "node 4",
                nextnodes: []
            },{
                id:"5",
                msg: "node 5",
                nextnodes: []
            },{
                id:"6",
                msg: "node 6",
                nextnodes: []
            },{
                id:"7",
                msg: "node 7",
                nextnodes: []
            }]
        }
    },
    mutations: {},
    actions: {},
    modules: {
    }
});

This is the main.js, I put remove node function here.

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App);
app.mount('#app')

//app.config.warnHandler = () => null;
Array.prototype.findone = function(val) {
    for (var i = 0; i < this.length; i++) {
//        if (this[i].id == val.id) return i;
        if (this[i] === val) return i;
    }
    return -1;
};
Array.prototype.findremove = function(val) {
    var index = this.findone(val);
    if (index > -1) {
        this.splice(index, 1);
        return true;
    }
    return false;
};

If I delete 2nd node, You can see that 7nd node is disappeared, but the store.state.tree is actually removed 2nd node. The view is not updated, only removed the last node, reflecting the list size only delete 2nd node View updated


Solution

  • I ran into a similar problem, though my implementation is a little bit different. My implementation works by enumerating nodes with a v-for loop. Adding child nodes worked as expected, but when nodes were removed, the DOM would not be updated even though the underlying data changed.

    Vue uses the key of the for-each loop to trigger a mutation, so I addressed this by adding a property to the root of the tree VM called "updateKey" (which is just a timestamp). Each node in my model already had a unique ID, so the list key for the children is a concatenation of its ID and its parent's ID.

    When graphChanged bubbles all the way up to the tree/graph component, the updateKey is set to a new timestamp.

    Tree Root

        <div v-for="node in root.children" :key="node.item.id+updateKey" >
          <graph-node :node="node" @graphChanged="graphChanged"  />
        </div>
    

    Tree Node (graph-node)

    <div class="graph-block">
        <div class="card">{{node.item.text}</div> 
    
    
        <div v-if="node.children && node.children.length">  
            <template v-for="child in node.children" > 
                <graph-node :node="child" :key="`${node.item.id}-${child.item.id}`" @graphChanged="$emit('graphChanged')"  />
            </template>
        </div>
    </div>
    

    For more info: https://v2.vuejs.org/v2/guide/list.html