Reputation: 173
I have tree component, like default Vue tree view example. I want to toggle active class on click on tree item. But when I clicked, every element has active class :( I don't understand what I did wrong. In not recursive component works fine. Please, any help.
it's my code:
new Vue({
el: '#groups',
data: {
hasLoaded: true,
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"
}
]
}
});
My item template:
<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"
:key="index"
:item="child"
></tree>
</ul>
</li>
</script>
<div id="groups">
<ul id="demo" class="list-group">
<tree
v-for="(item, index) in treeData"
class="item"
:item="item"
:key="index"
></tree>
</ul>
</div>
and component
Vue.component('tree', {
template: '#item-template',
props: {
item: Object
},
data: function () {
return {
isOpen: false,
isItemActive: false,
currentItem: null
}
},
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.currentItem = this.item.uuid;
}
}
});
Upvotes: 0
Views: 1524
Reputation: 29092
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:
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/[email protected]/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.
Upvotes: 1