0

My vue component like this :

Vue.component('list-category', {
  template: "#lc",
  props: ['data', 'category', 'search'],
  data() {
    return {
      open: false,
      categoryId: this.category
    }
  },
  mounted() {
    let isDataOpen = (d) => d.id === this.categoryId || d.children && d.children.some(isDataOpen);
  	this.open = isDataOpen(this.data);
  },
  computed: {
    icon() {
      return {
        'fa-plus': !this.open,
        'fa-minus': this.open,
      }
    },
    isFolder() {
      return this.data.children && this.data.children.length
    },
    isShow() {
      return this.open ? 'show' : 'hide'
    }
  },
  methods: {
    toggle() {
      this.open = !this.open
    },
    filterByCategory(id) {
      this.categoryId = id
    }
  }
})

new Vue({
  el: '#app',
  data() {
    return {
      categories: [{
          id: 1,
          name: 'England',
          children: [{
              id: 3,
              name: 'Chelsea',
              children: [{
                  id: 7,
                  name: 'Hazard'
                },
                {
                  id: 8,
                  name: 'Morata'
                }
              ]
            },
            {
              id: 4,
              name: 'Manchester United',
              children: [{
                  id: 9,
                  name: 'Pogba'
                },
                {
                  id: 10,
                  name: 'Lukaku'
                }
              ]
            }
          ]
        },
        {
          id: 2,
          name: 'Spain',
          children: [{
              id: 5,
              name: 'Real Madrid',
              children: [{
                  id: 11,
                  name: 'Ronaldo'
                },
                {
                  id: 12,
                  name: 'Bale'
                }
              ]
            },
            {
              id: 6,
              name: 'Barcelona',
              children: [{
                  id: 13,
                  name: 'Messi'
                },
                {
                  id: 14,
                  name: 'Suarez'
                }
              ]
            },
          ]
        }
      ],
      category: 7
    }
  }
})
.active {
  background: yellow;
}

.pd-search-filter > .panel-body ul.filter-category {
  padding-left: 0;
  list-style: none;
  margin: 0 -15px 0;
}

.pd-search-filter > .panel-body ul.filter-category > li a {
  display: block;
  padding: 10px 15px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pd-search-filter > .panel-body ul.filter-category > li a:last-child {
  padding-left: 45px;
}

.pd-search-filter > .panel-body ul.filter-category > li a:focus, .pd-search-filter > .panel-body ul.filter-category > li a:hover {
  background-color: #eeeeee;
  text-decoration: none;
}

.pd-search-filter > .panel-body ul.filter-category > li a + ul {
  padding-left: 0;
  list-style: none;
}

.pd-search-filter > .panel-body ul.filter-category > li a + ul > li > a {
  padding-left: 30px;
}

.show {
  display: block !important;
}

.hide {
  display: none !important;
}
<script src="https://unpkg.com/vue"></script>
<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">

<div id="app">
  <div class="panel panel-default pd-search-filter">
    <div class="panel-heading">
      <h3 class="panel-title"><i class="fa fa-circle-o"></i> By Category</h3>
    </div>
    <div class="panel-body">
      <ul class="filter-category" v-for="list in categories">
        <list-category :data="list" :category="category"></list-category>
      </ul>
    </div>
  </div>
</div>

<template id="lc">
    <li>
        <!--parent-->
        <a v-if="isFolder" href="javascript:" @click="toggle">
            <span class="fa fa-fw" :class="icon"></span> {{data.name}}
        </a>
        <!--if not folding, we do not need an binding event-->
        <a v-else href="javascript:" :title="data.name" :class="{active: data.id === categoryId}" @click="filterByCategory(data.id)"><span class="fa fa-fw fa-circle-o"></span> {{data.name}}</a>
        <!--children-->
        <ul v-if="isFolder" :class="isShow">
            <list-category v-for="(data, index) in data.children" :key="index" :data="data" :search="search" :category="categoryId"></list-category>
        </ul>
    </li>
</template>

Seems you need to see demo and full code

It's like this : http://jsfiddle.net/vxLhbo5m/861/

From demo seen category hazard active. If I click on morata category, it is not active. Whereas I have made the code

How can I solve this problem?

===========================================================================

7
  • 2
    it seems like it's working for me, what is your issue? Commented Mar 25, 2018 at 6:47
  • @samayo For example : If you click morata category, Where the morata category active? Commented Mar 25, 2018 at 6:59
  • @samayo If the category active, the color change to yellow color Commented Mar 25, 2018 at 7:00
  • see these two code samples, with the three numbers: vuejs.org/v2/guide/components.html#data-Must-Be-a-Function your issue is that you are using data(){} which creates a copy of your data and gives it to each component, so any click event is meaningless because that active link is not saved in the same place for all ... if it makes sense Commented Mar 25, 2018 at 7:22
  • You need to use the this.$emit('update-active-category', id); in filterByCategory method since you are trying to update data from a parent component. Then use @update-active-category when using list-category component in order to update the category in parent app Commented Mar 25, 2018 at 7:30

2 Answers 2

1

You would have to move the category calculator to a watcher (instead of mount()) and emit/listen to some events from child to parent to update the category and collapse the non-selected sub-tree.

Updated JSFiddle here.

Changes:

  • Template:

    • Parent:

      • From:

        <div id="app">
            ...
                <list-category :data="list" :category="category"></list-category>
        
      • Adding listening to the category event and updating the category property at parent:

        <div id="app">
            ...
                <list-category :data="list" :category="category" @category="category = $event"></list-category>
        
    • Child:

      • From:

        <template id="lc">
            ...
                <list-category v-for="(data, index) in data.children" :key="index" :data="data" :search="search" :category="categoryId"></list-category>
        
      • Listen to the category event and emit it up to the parent:

        <template id="lc">
            ...
                <list-category v-for="(data, index) in data.children" :key="index" :data="data" :search="search" :category="categoryId" @category="$emit('category', $event)"></list-category>
        
  • JavaScript (all in child):

    • Change filterByCategory to emit event instead of mutating property:

      • From:

        filterByCategory(id) {
          this.categoryId = id
        }
        
      • To:

        filterByCategory(id) {
          this.$emit('category', id);
        }
        
    • Remove mounted hook and add watcher:

      • Remove mounted:

        mounted() {
          let isDataOpen = (d) => d.id === this.categoryId || d.children && d.children.some(isDataOpen);
          this.open = isDataOpen(this.data);
        },
        
      • Add watcher to pick up when category changes in the parent:

        watch: {
          category: {
            handler() {
              this.categoryId = this.category
              let isDataOpen = (d) => d.id === this.categoryId || d.children && d.children.some(isDataOpen);
              this.open = isDataOpen(this.data);
            },
            immediate: true
          }
        }
        

Demo:

Vue.component('list-category', {
  template: "#lc",
  props: ['data', 'category', 'search'],
  data() {
    return {
      open: false,
      categoryId: this.category
    }
  },
  computed: {
    icon() {
      return {
        'fa-plus': !this.open,
        'fa-minus': this.open,
      }
    },
    isFolder() {
      return this.data.children && this.data.children.length
    },
    isShow() {
      return this.open ? 'show' : 'hide'
    }
  },
  methods: {
    toggle() {
      this.open = !this.open
    },
    filterByCategory(id) {
      this.$emit('category', id);
    }
  },
  watch: {
    category: {
    	handler() {
        this.categoryId = this.category
	      let isDataOpen = (d) => d.id === this.categoryId || d.children && d.children.some(isDataOpen);
	      this.open = isDataOpen(this.data);
      },
      immediate: true
    }
  }
})

new Vue({
  el: '#app',
  data() {
    return {
      categories: [{
          id: 1,
          name: 'England',
          children: [{
              id: 3,
              name: 'Chelsea',
              children: [{
                  id: 7,
                  name: 'Hazard'
                },
                {
                  id: 8,
                  name: 'Morata'
                }
              ]
            },
            {
              id: 4,
              name: 'Manchester United',
              children: [{
                  id: 9,
                  name: 'Pogba'
                },
                {
                  id: 10,
                  name: 'Lukaku'
                }
              ]
            }
          ]
        },
        {
          id: 2,
          name: 'Spain',
          children: [{
              id: 5,
              name: 'Real Madrid',
              children: [{
                  id: 11,
                  name: 'Ronaldo'
                },
                {
                  id: 12,
                  name: 'Bale'
                }
              ]
            },
            {
              id: 6,
              name: 'Barcelona',
              children: [{
                  id: 13,
                  name: 'Messi'
                },
                {
                  id: 14,
                  name: 'Suarez'
                }
              ]
            },
          ]
        }
      ],
      category: 7
    }
  }
})
.active {
  background: yellow;
}

.pd-search-filter > .panel-body ul.filter-category {
  padding-left: 0;
  list-style: none;
  margin: 0 -15px 0;
}

.pd-search-filter > .panel-body ul.filter-category > li a {
  display: block;
  padding: 10px 15px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pd-search-filter > .panel-body ul.filter-category > li a:last-child {
  padding-left: 45px;
}

.pd-search-filter > .panel-body ul.filter-category > li a:focus, .pd-search-filter > .panel-body ul.filter-category > li a:hover {
  background-color: #eeeeee;
  text-decoration: none;
}

.pd-search-filter > .panel-body ul.filter-category > li a + ul {
  padding-left: 0;
  list-style: none;
}

.pd-search-filter > .panel-body ul.filter-category > li a + ul > li > a {
  padding-left: 30px;
}

.show {
  display: block !important;
}

.hide {
  display: none !important;
}
<script src="https://unpkg.com/vue"></script>
<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">

<div id="app">
  <div class="panel panel-default pd-search-filter">
    <div class="panel-heading">
      <h3 class="panel-title"><i class="fa fa-circle-o"></i> By Category</h3>
    </div>
    <div class="panel-body">
      <ul class="filter-category" v-for="list in categories">
        <list-category :data="list" :category="category" @category="category = $event"></list-category>
      </ul>
    </div>
  </div>
</div>

<template id="lc">
    <li>
        <!--parent-->
        <a v-if="isFolder" href="javascript:" @click="toggle">
            <span class="fa fa-fw" :class="icon"></span> {{data.name}}
        </a>
        <!--if not folding, we do not need an binding event-->
        <a v-else href="javascript:" :title="data.name" :class="{active: data.id === categoryId}" @click="filterByCategory(data.id)"><span class="fa fa-fw fa-circle-o"></span> {{data.name}}</a>
        <!--children-->
        <ul v-if="isFolder" :class="isShow">
            <list-category v-for="(data, index) in data.children" :key="index" :data="data" :search="search" :category="categoryId" @category="$emit('category', $event)"></list-category>
        </ul>
    </li>
</template>

Sign up to request clarification or add additional context in comments.

12 Comments

Thanks. It seems there is still a problem If I run your script above, the hazard category active (yellow color). If I click morata category, it had add class active in the element. But the morata category has not changed to yellow. If I click to another element, the morata category looks change to yellow
The morata does is yellow. Try in the demo above. Click morata then click somewhere else outside the demo iframe.
Yes, it works. But why the element does not immediately turn yellow when diclick?
Because when you click it, you focus it. And the focus style .pd-search-filter > .panel-body ul.filter-category > li a:focus turns it gray. To remove it, just remove the such style: jsfiddle.net/acdcjunior/vxLhbo5m/917 (just removed the style with :focus at line 23 of CSS)
In your jsfiddle is no error. But if I implement in my case, there still error. The error like this : [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "category". Whereas I had follow your code
|
0

You cannot control the data of a parent element from the child component. In order to change the parent's data yyou would need to emit the change to the parent and than change the data from the parent.

Please find the below to have an idea how to use the this.$emit. I know I had to change the json data to avoid recursive calls to the same template, but now you have an idea on how to change the parent data element.

Vue.component('list-category', {
  template: "#lc",
  props: ['data', 'category', 'search'],
  data() {
    return {
      open: false,
      categoryId: this.category
    }
  },
  mounted() {
    let isDataOpen = (d) => d.id === this.categoryId || d.children && d.children.some(isDataOpen);
  	this.open = isDataOpen(this.data);
  },
  computed: {
    icon() {
      return {
        'fa-plus': !this.open,
        'fa-minus': this.open,
      }
    },
    isFolder() {
      return this.data.children && this.data.children.length
    },
    isShow() {
      return this.open ? 'show' : 'hide'
    }
  },
  methods: {
    toggle() {
      this.open = !this.open
    },
    filterByCategory: function(id){
      this.$emit('update-active-category', id);
      console.log('Emitting: ' + id);
    }
  }
})

new Vue({
  el: '#app',
  data() {
return {
  categories: [{
      id: 1,
      name: 'England',
      children: [{
          id: 3,
          name: 'Chelsea',
          children: [{
              id: 7,
              name: 'Hazard'
            },
            {
              id: 8,
              name: 'Morata'
            }
          ]
        },
        {
          id: 4,
          name: 'Manchester United',
          children: [{
              id: 9,
              name: 'Pogba'
            },
            {
              id: 10,
              name: 'Lukaku'
            }
          ]
        }
      ]
    },
    {
      id: 2,
      name: 'Spain',
      children: [{
          id: 5,
          name: 'Real Madrid',
          children: [{
              id: 11,
              name: 'Ronaldo'
            },
            {
              id: 12,
              name: 'Bale'
            }
          ]
        },
        {
          id: 6,
          name: 'Barcelona',
          children: [{
              id: 13,
              name: 'Messi'
            },
            {
              id: 14,
              name: 'Suarez'
            }
          ]
        },
      ]
    }
  ],
    category: 7
    }
  },
  methods: {
    updateActiveCategory: function(id) {
    this.category = id;
  }
}
})
.active {
  background: yellow !important;
}

.pd-search-filter > .panel-body ul.filter-category {
  padding-left: 0;
  list-style: none;
  margin: 0 -15px 0;
}

.pd-search-filter > .panel-body ul.filter-category > li a {
  display: block;
  padding: 10px 15px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pd-search-filter > .panel-body ul.filter-category > li a:last-child {
  padding-left: 45px;
}

.pd-search-filter > .panel-body ul.filter-category > li a:focus, .pd-search-filter > .panel-body ul.filter-category > li a:hover {
  background-color: #eeeeee;
  text-decoration: none;
}

.pd-search-filter > .panel-body ul.filter-category > li a + ul {
  padding-left: 0;
  list-style: none;
}

.pd-search-filter > .panel-body ul.filter-category > li a + ul > li > a {
  padding-left: 30px;
}

.show {
  display: block !important;
}

.hide {
  display: none !important;
}
<script src="https://unpkg.com/vue"></script>
<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">

<div id="app">
  <div class="panel panel-default pd-search-filter">
    <div class="panel-heading">
      <h3 class="panel-title"><i class="fa fa-circle-o"></i> By Category</h3>
    </div>
    <div class="panel-body">
      <ul class="filter-category" v-for="list in categories">
        <list-category :data="list" :category="category" @update-active-category="updateActiveCategory">
        </list-category>
      </ul>
    </div>
  </div>
</div>

<template id="lc">
    <li>
        <!--parent-->
        <a v-if="isFolder" href="javascript:" @click="toggle">
            <span class="fa fa-fw" :class="icon"></span> {{data.name}}
        </a>
        <!--if not folding, we do not need an binding event-->
        <a v-else href="javascript:" :title="data.name" :class="{active: data.id === category}" @click="filterByCategory(data.id)" @update-active-category="filterByCategory"><span class="fa fa-fw fa-circle-o"></span> {{data.name}}</a>
        <!--children-->
        <ul v-if="isFolder" :class="isShow">
            <list-category v-for="(data, index) in data.children" :key="index" :data="data" :search="search" :category="category" @update-active-category="filterByCategory"></list-category>
        </ul>
    </li>
</template>

4 Comments

Your answer is not like my case. I did not get an answer from your answer
You cannot accomplish what you want without the emit . I just explained how the emit works, you need to change the structure of you components and use the emit function. I will not restructure the code for you, but I think you have to separate the folder component and list-category-item separetly. Your question was to to add an active class and I just did.
I have updated my answer to show you that my answer could be edited and solve your issue.
Okay. Thanks. I will check it

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.