1👍
So the core problem is that each component instance has its own property for holding the currentItem
. This isn’t shared, so the value for currentItem
in one tree has no impact on the value for the other trees.
To get the behaviour you want there needs to be some form of shared state. There are several ways this can be solved, including:
- Shared global state, e.g. via the Vuex store. This is a common approach for sharing data between distantly related components, especially in cases where there are intermediary components who don’t care about that state. That isn’t relevant here as there aren’t any components between the tree nodes.
- Passing a reactive object down through the tree nodes as a prop with the current selection held in a property of that object. Changes to that object will automatically propagate throughout the tree as all nodes are referencing the same object. The use of a wrapper object makes this appear somewhat clumsy. It is also regarded as a violation of one-way data flow as mutations can occur within any node that impact all the other nodes.
- Passing the current selection down via a prop and propagating changes up via events. There are a lot of little pieces involved so it ends up looking a bit messy but it does stick to ‘the rules’ for one-way data flow.
There are other viable options, such as delegated DOM events, but for the example below I’ve gone with the third approach described above: props down, events up.
Vue.component('tree', {
template: '#item-template',
props: {
item: Object,
currentItem: Number
},
data: function () {
return {
isOpen: false
}
},
computed: {
isFolder: function () {
return this.item.children && this.item.children.length
},
isActiveItem: function () {
return this.currentItem === this.item.uuid;
}
},
methods: {
toggle: function () {
if (this.isFolder) {
this.isOpen = !this.isOpen
}
},
selectGroup: function () {
this.select(this.item.uuid);
},
select: function (uuid) {
this.$emit('select', uuid);
}
}
});
new Vue({
el: '#groups',
data: {
currentItem: null,
treeData: [
{
"uuid": 1,
"name": "Group1",
"children": [
{
"uuid": 2,
"name": "Group2"
},
{
"uuid": 3,
"name": "Group3"
}
]
},
{
"uuid": 4,
"name": "Group4",
"children": [
{
"uuid": 5,
"name": "Group5",
"children": [
{
"uuid": 7,
"name": "Group7"
}
]
}
]
},
{
"uuid": 6,
"name": "Group6"
}
]
},
methods: {
onSelect: function (uuid) {
if (this.currentItem === uuid) {
this.currentItem = null;
} else {
this.currentItem = uuid;
}
}
}
});
.bold {
font-weight: bold;
}
.active {
background: #000;
color: #fff;
}
<script src="https://unpkg.com/vue@2.6.10/dist/vue.js"></script>
<script type="text/x-template" id="item-template">
<li class="list-group-item group-item">
<div
:class="{bold: isFolder, active : isActiveItem}"
@click="selectGroup"
>
{{ item.name }}
<span v-if="isFolder" @click="toggle">[{{ isOpen ? '-' : '+' }}]</span>
</div>
<ul v-show="isOpen" v-if="isFolder" class="list-group">
<tree
class="item"
v-for="(child, index) in item.children"
:current-item="currentItem"
:key="index"
:item="child"
@select="select"
></tree>
</ul>
</li>
</script>
<div id="groups">
<ul id="demo" class="list-group">
<tree
v-for="(item, index) in treeData"
class="item"
:current-item="currentItem"
:item="item"
:key="index"
@select="onSelect"
></tree>
</ul>
</div>
The root component has gained a currentItem
property that is passed down the tree via currentItem
props. The individual nodes no longer have local data to hold the currentItem
, they just use the prop instead. The prop is defined as a Number
to match the example data. I’ve tried to retain the original behaviour of only storing the uuid
as the currentItem
, though there’s no reason why the whole object couldn’t be stored.
When a tree node is clicked it will emit a select
event with the appropriate uuid
. This will only go as far as the parent component, so the events need to be manually propagated up all the way to the top so that the currentItem
data property can be updated. This new value will then cascade down through the props.
I’ve also added a bit of code to allow a node to be deselected by clicking on it. That can easily be omitted if you would rather retain the selection.