vue.jsvuejs3vue-component

How to dynamically apply a class to a child component?


I have a child component within a parent component. Whenever the child component is clicked, I need to apply a class called .selected to it.

Parent.vue

<template>
   <ol>
    <li v-for="(Contact, index) in Contacts">
      <Component-Contact :ref="(el) => { RefContacts[index] = el}" @click="onClick(RefContacts[index])" :Contact="Contact" @Selected="(Data) => onSelected(Data)"/>
     </li>
   </ol>

</template>
<script>
import ComponentContact from '../components/Contact.vue'
....
setup() {
const RefContacts = ref([]);

function onSelected(Contact){
  //Adds data received from child component to an array
}

onClick(el) {
 el.toggleClass('selected') // Returns error saying toggleClass is not a function!!
}

 const Contacts = ref();
 onMounted(() => {
    const Result = await axios.get(`/api/contacts`);
    Contacts.value = Result.data
}
 )

return {
  Contacts, // An array of contacts from the DB
  onSelected
}
}
</script>

Child component Contact.vue

<template>
 <div style="cursor: pointer" @click="onClick($el, Contact)">
  <p>{{Contact.Forename}}</p>
  <p>{{Contact.Surname}}</p>
 </div>
</template>
<script>
...
props: {
  Contact: {
    type: Object,
    required: true
   },
emits: 'Selected',
setup() {

  function onClick(el, Contact) {
   el.toggleClass('selected') // This works but I don't want to set it here in the child
   return context.emit('Selected', {
   FullName: Contact.forename + ' ' + Contact.surname
   }
 }

}
</script>

How can I get Vue to apply the class of .selected to the <Child-Component /> within the Parent.vue view? I want to set it in the Parent (or wherever else its being used) and then remove the class when the user finished doing what hes doing .I feel this is more flexible than setting it within the child component.


Solution

  • Refrain from DOM manipulation as much as possible.
    Instead of applying the class yourself, let Vue do it:

    <my-component :class="{ selected: /* condition here */ }" />
    

    See Class and Style Bindings.


    Basic example, where only one item can be selected (selecting another unselects the previous one):

    const { createApp, ref } = Vue
    
    const app = createApp({
      setup() {
        const selected = ref(null)
        return {
          selected,
          select: (index) =>
            (selected.value = selected.value === index ? null : index)
        }
      }
    }).mount('#app')
    li {
      cursor: pointer;
    }
    li:hover {
      background-color: #f5f5f5;
    }
    .selected {
      color: red;
    }
    #app {
      display: grid;
      grid-template-columns: repeat(2, 1fr);
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.5.4/vue.global.prod.min.js"></script>
    <div id="app">
      <ul>
        <li
          v-for="(_, index) in 6"
          @click="() => select(index)"
          :class="{ selected: index === selected }"
          v-text="'item'"
        />
      </ul>
      <pre v-text="JSON.stringify({ selected }, null, 2)" />
    </div>


    And here's one where you can select items individually:

    const { createApp, ref } = Vue
    
    const app = createApp({
      setup() {
        const items = ref(
          Array.from({ length: 7 }).map(() => ({ selected: false }))
        )
        return {
          items,
          toggle: (index) =>
            items.value.splice(index, 1, { selected: !items.value[index].selected })
        }
      }
    }).mount('#app')
    li {
      cursor: pointer;
    }
    li:hover {
      background-color: #f5f5f5;
    }
    .selected {
      color: red;
    }
    #app {
      display: grid;
      grid-template-columns: repeat(2, 1fr);
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.5.4/vue.global.prod.min.js"></script>
    <div id="app">
      <ul>
        <li
          v-for="({ selected }, index) in items"
          @click="() => toggle(index)"
          :class="{ selected }"
          v-text="'item'"
        />
      </ul>
      <pre v-text="JSON.stringify( { items }, null, 2)" />
    </div>


    In each case, I'm keeping a minimal local state (in parent). When the state is modified, Vue updates the template.