Search code examples
vue.jsrecursionvuejs2vue-componentemit

Vuejs - v-bind.sync on resursive components (hierarchical list)


I have a hierarchical list component where child items have checkboxes. Checkbox actions(check/uncheck) must keep the parent component in sync with the checkbox's changed state. I cannot figure out how to achieve this using v-bind.sync recursively. My code is as below:

Menu.vue

This component holds the hierarchical list. (Only relevant code included)

  1. HierarchicalCheckboxList is the component that displays the hierarchical list
  2. Property 'value' holds the check/uncheck value (true/false)
  3. Property 'children' contains the child list items

How do I define the .sync attribute on HierarchicalCheckboxList and with what parameter?

<template>
<div>
    <HierarchicalCheckboxList
      v-for="link in links"
      @checked="primaryCheckChanged"
      :key="link.id"
      v-bind="link">
    </HierarchicalCheckboxList>
</div>
</template>
<script>
    import HierarchicalCheckboxList from 'components/HierarchicalCheckboxList'

    data () {
       return {
          links: [{
             id: 1,
             title: 'Home',
             caption: 'Feeds, Dashboard & more',
             icon: 'account_box',
             level: 0,
             children: [{
               id: 2,
               title: 'Feeds',
               icon: 'feeds',value: true,
               level: 1,
               children: [{
                  id: '3',
                  title: 'Dashboard',
                  icon: 'settings',
                  value: true,
                  level: 1
               }]
             }]
          }]
       }
    },
    methods: {
      primaryCheckChanged (d) {
        // A child's checked state is propogated till here
        console.log(d)
      }
    }
</script>

HierarchicalCheckboxList.vue

This component calls itself recursively:

<template>
  <div>
    <div v-if="children != undefined && children.length == 0">
      <!--/admin/user/user-->
      <q-item clickable v-ripple :inset-level="level" :to="goto">
        <q-item-section>
          {{title}}
        </q-item-section>
      </q-item>
    </div>
    <div v-else>
      <div v-if="children != undefined && children.length > 0">
        <!-- {{children}} -->
        <q-expansion-item
            expand-separator
            :icon="icon"
            :label="title"
            :caption="caption"
            :header-inset-level="level"
            default-closed>
            <template v-slot:header>
              <q-item-section>
                {{ title }}
              </q-item-section>
              <q-item-section side>
                <div class="row items-center">
                  <q-btn icon="add" dense flat color="secondary"></q-btn>
                </div>
              </q-item-section>
            </template>
          <HierarchicalCheckboxList
            v-for="child in children"
            :key="child.id"
            @checked="primaryCheckChanged"
            v-bind="child">
          </HierarchicalCheckboxList>
        </q-expansion-item>
      </div>
      <!-- to="/admin/user/user" -->
      <div v-else>
        <q-item clickable v-ripple :inset-level="level">
          <q-item-section>
            <q-checkbox :label="title" v-model="selection" />
          </q-item-section>
        </q-item>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'HierarchicalCheckboxList',
  props: {
    id: { type: String, required: true },
    title: { type: String, required: false },
    caption: { type: String, default: '' },
    icon: { type: String, default: '' },
    value: { type: Boolean, default: false },
    level: { type: Number, default: 0 },
    children: { type: Array }
  },
  data () {
    return {
      localValue: this.$props.value
    }
  },
  computed: {
    selection: {
      get: function () {
        return this.localValue
      },
      set: function (newvalue) {
        this.localValue = newvalue
        this.$emit('checked', this.localValue) 
        // or this.$emit('checked', {id: this.$props.id, value: this.localValue })
      }
    }
  },
  methods: {
    primaryCheckChanged (d) {
      this.$emit('checked', d)
    }
  }
}
</script>

What works so far

As a work-around I am able to get the checkbox state emitted with $emit('checked'), which I use to send it to the next process. But the parent's state is not updated until I refresh it back from the database.

How do I update the parent component's state using v-bind.sync recursively?

Appreciate any help!!

UI

enter image description here


Solution

  • Figured out how to do it after I broke the code down from the whole 2000 line code to a separate 'trial-n-error' code of 20 lines and then things became simple and clear.

    Menu.vue

    A few changes in the parent component in the HierarchicalCheckboxList declaration: Note the sync property

    <HierarchicalCheckboxList
       v-for="child in children"
       :key="child.id"
       :u.sync="link.value"
       v-bind="child">
    </HierarchicalCheckboxList>
    

    HierarchicalCheckboxList.vue

    Change the same line of code in the child component (as its recursive)

       <HierarchicalCheckboxList
         v-for="child in children"
         :key="child.id"
         :u.sync="child.value"
         v-bind="child">
       </HierarchicalCheckboxList>
    

    And in the computed set property, emit as below:

       this.$emit('update:u', this.localValue)
    

    That's it - parent n children components now stay in snyc.