Search code examples
javascriptvue.jstreeview

Update the inital array of a recursive treeview in VueJS


I was folling this tutorial for my own tree view with a recursive component in vuejs. So the input array looks like this:

let tree = {
  label: 'root',
  nodes: [
    {
      label: 'item1',
      nodes: [
        {
          label: 'item1.1'
        },
        {
          label: 'item1.2',
          nodes: [
            {
              label: 'item1.2.1'
            }
          ]
        }
      ]
    }, 
    {
      label: 'item2'  
    }
  ]
}
<template>
  <div>
    ...
    <tree-menu 
      v-for="node in nodes" 
      :nodes="node.nodes" 
      :label="node.label" />
    ...
  </div>
<template

<script>
  export default { 
    props: [ 'label', 'nodes' ],
    name: 'tree-menu'
  }
</script>

So basically a label and a subarray of nodes is passed to a child node. Now I want to update or delete a node (e.g. item1.1), but reflect this change in the outmost array (here tree), because I want to send this updated structure to the server. How can I achive this? If I change the label of a node, this will be rendered in the DOM, but the tree array is not updated.


Solution

  • Here's how you can use the .sync modifier to update recursively:

    Vue.config.devtools = false;
    Vue.config.productionTip = false;
    Vue.component('tree-node', {
      template: `
    <div style="margin-left: 5px;">
      <input :value="label"
             type="text"
             @input="$emit('update:label', $event.target.value)" />
      <tree-node v-for="(node, key) in nodes"
                 :key="key"
                 v-bind.sync="node" />
    </div>
    `,
      props: ['label', 'nodes']
    });
    
    let tree = {
          label: 'root',
          nodes: [{
              label: 'item 1',
              nodes: [
                { label: 'item 1.1' },
                { label: 'item 1.2', 
                  nodes: [
                    { label: 'item 1.2.1' }
                  ]
                }
              ]
            },
            { label: 'item 2' }
          ]
        };
    
    new Vue({
      el: '#app',
      data: () => ({
        tree
      })
    })
    #app {
      display: flex;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.js"></script>
    <div id="app">
      <div>
        <tree-node v-bind.sync="tree" />
      </div>
      <pre v-html="tree" />
    </div>

    v-bind.sync="node" is shorthand for :label.sync="node.label" :nodes.sync="node.nodes". v-bind unwraps all object members as attributes of the tag, resulting in props for the component.

    The other half of the solution is replacing v-model on the input with :value + an $emit('update:propName', $event.target.value) call on @input which updates the .sync-ed property in the parent. To conceptualize it, it's a DIY v-model exposed by Vue so it could be customized (you decide when to call the update and what to update with). You can replace the <input> with any other type of input, depending on what you're binding/modifying (checkboxes, textarea, select, or any fancier input wrapper your framework might feature). Depending on type of input you'll want to customize the listener: @change, @someCustomEvent, etc...

    .sync makes everything reactive at each individual level. Since everything is :key-ed, no re-rendering actually happens (Vue only re-renders DOM elements which actually changed). If that wasn't the case, the input would lose focus upon re-rendering.

    The update principle is: instead of making the change at child level you update the parent property which, through v-bind, sends it back to the child.

    It's the same exact principle used by Vuex. Rather than changing some local prop you call a store mutation which comes back through getters and modifies the local value but it happens for any component using that store data, not just for current one.