PhillSlevin
PhillSlevin

Reputation: 127

Indeterminate checkboxes with Vue.js

I just started out working with Vue and I'm trying to visualise a nested list.

The list-items should contain triple-state checkboxes: When a child item is checked, the parent item's checkbox should become 'indeterminate'. When all child-checkboxes are checked, the parent checkbox should also become checked. When a parent item checkbox is checked, all child item checkboxes (also the ones nested deeper) should be selected too.

I kind of have a working solution (check out this pen or the code below) but the checkbox-logic is still flawed. For this example, checked boxes are green, indeterminate ones are orange and unchecked ones are red.

I've run out of ideas how to fix it. Could someone shed some light on how to accomplish this in Vue?

'use strict';
Vue.component("book-chapter", Vue.extend({
  name: "book-chapter",
  props: ["data", "current-depth"],
  data: function() {
    return {
      checked: this.data.checked,
      indeterminate: this.data.indeterminate || false
    };
  },
  methods: {
    isChecked: function() {
      return this.checked && !this.indeterminate;
    },
    isIndeterminate: function(){
      return this.indeterminate;
    },
    toggleCheckbox: function(eventData) {
      if (this.currentDepth > 0){

        if (!this.data.children) {
          this.checked != this.children
        } else {
          this.indeterminate = !this.indeterminate;
        }
      }

      if (eventData) {
        // fired by nested chapter
        this.$emit('checked', eventData);

      } else {
        // fired by top level chapter
        this.checked = !this.checked;
        this.$emit('checked', {
          data: this.data
        });
      }
    },
    isRootObject: function() {
      return this.currentDepth === 0;
    },
    isChild: function() {
      return this.currentDepth === 2;
    },
    isGrandChild: function() {
      return this.currentDepth > 2;
    }
  },
  template: `
  <div class='book__chapters'>
   <div
      class='book__chapter'
      v-bind:class="{ 'book__chapter--sub': isChild(), 'book__chapter--subsub': isGrandChild() }"
      v-show='!isRootObject()'>
      <div class='book__chapter__color'></div>
      <div
         class='book__chapter__content'
         v-bind:class="{ 'book__chapter__content--sub': isChild(), 'book__chapter__content--subsub': isGrandChild() }">
         <div class='book__chapter__title'>
            <span class='book__chapter__title__text'>{{data.title}}</span>
         </div>
         <div class='book__chapter__checkbox triple-checkbox'>
            <div class='indeterminatecheckbox'>
               <div
                  class='icon'
                  @click.stop="toggleCheckbox()"
                  v-bind:class="{'icon--checkbox-checked': isChecked(), 'icon--checkbox-unchecked': !isChecked(), 'icon--checkbox-indeterminate': isIndeterminate()}">
               </div>
            </div>
         </div>
      </div>
   </div>
   <book-chapter
      ref='chapter'
      :current-depth='currentDepth + 1'
      v-for='child in data.children'
      key='child.id'
      @checked='toggleCheckbox(arguments[0])'
      :data='child'>
   </book-chapter>
</div>
`
}));

Vue.component("book", Vue.extend({
  name: "book",
  props: ["data"],
  template: `
    <div class='book'>
      <book-chapter 
        :data='this.data'
        :currentDepth='0'>
      </book-chapter>
    </div>
`
}));

var parent = new Vue({
  el: "#container",
  data: function() {
    return {
      book: {}
    };
  },
  mounted: function() {
    this.book = {
      "title": "Book",
      "children": [{
        "title": "1 First title",
        "children": [{
          "title": "1.1 Subtitle"
        }, {
          "title": "1.2 Subtitle"
        }]
      }, {
        "title": "2 Second title",
        "children": [{
          "title": "2.1 Subtitle",
          "children": [{
            "title": "2.1.1 Sub-Sub title"
          }, {
            "title": "2.1.2 Another sub-sub title"
          }]
        }]
      }]
    }
  }
});

Upvotes: 1

Views: 2869

Answers (1)

choasia
choasia

Reputation: 10852

Update: fixed a bug found by @PhillSlevin. See pen here

Check this pen, is it what you want to achieve?
I think you can use either eventbus or vuex to solve this problem,
if you treated every 's section as a component.

'use strict';

var bus = new Vue();

var book = {
  "title": "Book",
  "children": [{
    "title": "1 First title",
    "children": [{
      "title": "1.1 Subtitle"
    }, {
      "title": "1.2 Subtitle"
    }]
  }, {
    "title": "2 Second title",
    "children": [{
      "title": "2.1 Subtitle",
      "children": [{
        "title": "2.1.1 Sub-Sub title"
      }, {
        "title": "2.1.2 Another sub-sub title"
      }]
    }]
  }]
};

Vue.component('book', {
  template: `
<div class="book__chapter">
  <p :class="'book__title ' + status" @click="clickEvent">{{title}} {{parent}}</p>
  <book v-for="child in children" :key="child" :info="child"></book>
</div>
`,
  props: ['info'],
  data() {
    return {
      parent: this.info.parent,
      title: this.info.title,
      children: [],
      status: this.info.status,
    };
  },
  created() {
    const info = this.info;
    if(info.children) {
      info.children.forEach(child => {
        child.status = "unchecked";
        // use title as ID
        child.parent = info.title;
      });
      this.children = info.children;
    }
  },
  mounted() {
    const vm = this;
    bus.$on('upside', (payload) => {
      const targetArr = vm.children.filter((child) => child.title === payload.from);
      if (targetArr.length === 1) {
        const target = targetArr[0];
        target.status = payload.status;
        if (vm.children.every(ele => ele.status === 'checked')) {
          vm.status = 'checked';
        } else if (vm.children.every(ele => ele.status === 'unchecked')) {
          vm.status = 'unchecked';
        } else {
          vm.status = 'indeterminate';
        }
        bus.$emit('upside', {
          from: vm.title,
          status: vm.status,
        });
      }
    });
    
    bus.$on('downside', (payload) => {
      if (payload.from === this.parent) {
        if (payload.status === 'checked') {
          vm.status = 'checked';
          vm.children.forEach(child => child.status = 'checked');
        } else if (payload.status === 'unchecked') {
          vm.status = 'unchecked';
          vm.children.forEach(child => child.status = 'unchecked')
        }
        bus.$emit('downside', {
          from: vm.title,
          status: vm.status,
        })
      }
    });
  },
  methods: {
    clickEvent() {
      if (this.status === 'checked') {
        this.status = 'unchecked';
        this.children.forEach(child => child.status = 'unchecked');
      } else {
        this.status = 'checked';
        this.children.forEach(child => child.status = 'checked');
      }
      
      const vm = this;
      bus.$emit('upside', {
        from: vm.title,
        status: vm.status,
      });
      bus.$emit('downside', {
        from: vm.title,
        status: vm.status,
      });
    },
  }
});

var parent = new Vue({
  el: "#container",
  data: function() {
    return {
      book
    };
  },
});
.book__title.unchecked::after {
  content: '□';
}

.book__title.indeterminate::after {
  content: '△';
}

.book__title.checked::after {
  content: '■';
}

.book__chapter {
  display: block;
  position: reletive;
  margin-left: 40px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.6/vue.js"></script>
<div id="container">
  <book :info="book" :parent="'container'"></book>
</div>

Upvotes: 1

Related Questions