Unspecified checkbox selection with Vue.js

Recently delving into the world of Vue, I've been grappling with how to display a nested list visually.

In this list, each item should feature a triple-state checkbox system: When a child item is checked, the parent item's checkbox should become 'indeterminate'. If all child-checkboxes are checked, then the parent checkbox should also become checked. Additionally, if a parent item checkbox is checked, all child item checkboxes (even those nested deeper) should be selected as well.

Although I have a partially functional solution (have a look at this pen or the code snippet provided below), the checkbox logic still needs fine-tuning. In this demonstration, checked boxes are represented in green, indeterminate ones in orange, and unchecked ones in red.

I've hit a roadblock in resolving this issue. Can anyone offer some guidance on how to achieve this functionality 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"
          }]
        }]
      }]
    }
  }
});

Answer №1

Update: Just patched a bug pointed out by @PhillSlevin. For more information, refer to this pen here.

Have you had a chance to review this pen? Does it align with your desired outcome?
Consider utilizing either eventbus or vuex to tackle this issue if you view each 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>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Having an issue with retrieving value from a textfield in JavaScript

<input id="checkOldPassword" type="button" title="Check New Password" value="Check New Password" onclick="checkPassword()" /> <input id="newPassword" type="text" maxlength="8" min="8" /> <script language="javascript"> function checkPassw ...

How can I subtract a value from an array using node-js?

If we consider a simple scenario where an array totalSpots = [95] contains only one value, and a new booking is made, the goal is to automatically assign one parking spot to the user who booked it. This will involve reducing the value in the array by 1 or ...

How can you use ui.router to set up one controller as the child of another controller?

To create a nested controller in html, you can simply write the child controller inside the parent controller like this: <div ng-controller="parentCtrl"> <div ng-controller="childCtrl"> When using ui.router, you can specify one state as a c ...

Setting up a Bootstrap tokenfield for usage with a textarea

I was attempting to set up a tokenfield on a textarea with increased height, but it is showing up as a single-line textbox. How can I modify the tokenfield to function properly with a textarea? <textarea name="f1_email" placeholder="Enter Friends' ...

What is preventing me from installing socket.io?

I keep seeing an error in the console, what could be causing this? npm ERR! code 1 npm ERR! path E:\full-stack\proshop-2\socket\node_modules\utf-8-validate npm ERR! command failed npm ERR! command C:\WINDOWS\system32&bso ...

Understanding the concept of callbacks and scopes

While experimenting with the concept of callbacks, I encountered a scenario where I wanted to confirm that my understanding of the situation was correct. function greet(callback) { // 'greet' function utilizes a callback var greeting = "hi"; ...

Attempting to adjust the width of a text animation loaded with jQuery using Textillate, but encountering difficulties

I found a captivating animation on this website: http://codepen.io/jschr/pen/GaJCi Currently, I am integrating it into my project. #content { position: relative; margin-left: auto; margin-right: auto; width: 1000px; height: 700px; } ...

Using the && operator for conditional rendering in a React return statement

How should I format my return statement in a class component when using the && operator in the condition like this: if (x === null && y === null) If the condition is met, display this HTML: <div className="col-12"> <SomeComponent /> < ...

Can you tell me how to add a variable to an array of objects in JavaScript?

I am currently engaged in a small project aimed at: Reading data from a CSV file (such as employee names and shifts) Displaying this data on FullCalendar. How can I incorporate the CSV result into this line of code: { id: 'a', title: 'Audi ...

Retrieving an attribute through the act of clicking a button

How can I retrieve the rel attribute value when clicking on a button with the class selector? <button class="nameClass" rel="relName">Content</button> I am attempting to achieve this by: $(".nameClass").click(function(){ // Here is where ...

Utilizing Vue 3 props validation in conjunction with the power of Typescript

Looking to customize a Link component using Nuxt, Typescript, and the composition-api. The prop target can accept specific values as outlined below. I'm curious if using a custom validator function to check prop types at runtime adds value when compar ...

Oops! Could not compile due to a syntax error: Invalid assignment expression on the left-hand side

I am currently developing an application that requires me to retrieve data from the backend containing a userdetail object. In my code, I need to set a current accessToken for the userdetail object: useEffect(() => { if (session?.user && ...

How can I use vanilla JavaScript to retrieve all elements within the body tag while excluding a specific div and its descendants?

My goal is to identify all elements within the body tag, except for one specific element with a class of "hidden" and its children. Here is the variable that stores all elements in the body: allTagsInBody = document.body.getElementsByTagName('*&apos ...

Unnecessary Page Diversion

Within my index.php file, I have a download button with the id of "render". Using AJAX, I am sending a request to the server. The JavaScript code being utilized is as follows: $('#render').click(function(e){ $('html,body').animat ...

What is the best way to link function calls together dynamically using RXJS?

I am seeking a way to store the result of an initial request and then retrieve that stored value for subsequent requests. Currently, I am utilizing promises and chaining them to achieve this functionality. While my current solution works fine, I am interes ...

Setting a consistent theme or style for all HTML/React tags using a selector inside a specific component

Here's a simplified example of what I'm trying to do: I'm using Material UI Styles for styling my components. I want to style all the <Link> tags in my component. For instance: const useStyles = makeStyles(theme => ({ menuLink: ...

Customize the color of each individual column in DotNet.HighCharts by setting unique colors for

Can DotNet.HighCharts be used to create a chart where each column is a unique color? ...

What steps can be taken to fix the issue of an Undefined Index occurring while using ajax

Trying to set up a dependent combobox using Ajax, but encountering an error (Undefined index: faculty_id). No typos in the code, and the query works fine in SQLyog tests. Here's the Ajax code: $(document).ready(function(){ $('#faculty&apos ...

What is the solution to the error message "Cannot assign to read only property 'exports' of object '#<Object>' when attempting to add an additional function to module.exports?"

I've been struggling for the past 5 days with this issue. Despite numerous Google searches, I haven't found a solution that works for me. I have a file called utils.js which contains useful functions to assist me. However, when I include the func ...

How can I optimize the performance of JS-heavy pages on mobile devices?

As a website owner, I strive to optimize the performance of my site on mobile devices without the need for a separate "mobile-friendly" version or replacing large sections of code. With around 100K of JS code, including jQuery, I am determined to enhance b ...