Reputation: 127
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
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