moses toh
moses toh

Reputation: 13172

How can I add class active if the element click on the vue component?

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?

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

Upvotes: 0

Views: 3466

Answers (2)

acdcjunior
acdcjunior

Reputation: 135762

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>

Upvotes: 1

Omar Tanti
Omar Tanti

Reputation: 1448

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>

Upvotes: 0

Related Questions