vue.jsvuejs3vue-componentv-forvue-props

Changing Prop of a Single Component Among Components Iterated via V-For


I'm facing the below described challenge on my Vue project, and quite confused about what to do.

There is a one child component called button-base:

<template>
<button 
    :type="buttonType"
    :class="
        { 
            primary_button: buttonClass == 'primary', 
            secondary_button: buttonClass == 'secondary', 
            tertiary_button: buttonClass == 'tertiary', 
            large: large,
            medium: medium,
            small: small,
            xsmall: xsmall,
            iconName: iconName
        }"
    >
    <div class="text_wrapper">
        <span v-if="iconName" class="icon material-icons">{{ iconName }}</span> {{ buttonText }}
    </div>
</button>
</template>

<script>
export default {
    props: [
        "buttonText", 
        "buttonType", 
        "buttonClass", 
        "large", 
        "small",
        "xsmall",
        "iconName"
    ]
}
</script>

Which is used on the parent component (feed-card) like this:

<template>
    <div class="sub_options_left_side">
        <button-base 
            buttonType="button" 
            buttonClass="tertiary" 
            :iconName="idCopyIconName" 
            :buttonText="idCopyButtonText"
            xsmall="xsmall"
            class="feed_card_option_button"
            @click="copyId"
        ></button-base>
    </div>
</template>

<script>
export default {
props: [
    //some oter props..
    "idCopyIconName",
    "idCopyButtonText"
],
emits: ["copyId"],
data() {
    return {
        //other unrelated data info..
    }
},
methods: {
    copyId() {
        this.$emit("copyId");
    }
},
//other stuff..
</script>

Above component is also a child component under this final component (feed-page):

<template>
    <div class="feeds_wrapper_feed_container">
        <feed-card v-for="feed in feedList"
            :key="feed.id"
            
            <!-- other props -->
            
            :id-copy-icon-name="idCopyIconName"
            :id-copy-button-text="idCopyButtonText"
            @copy-id="copyId(feed.id)"
        ></feed-card>
    </div>
</template>

<script>
//imports..
export default {
//components..
data() {
    return {
        idCopyButtonText: "ID",
        idCopyIconName: "file_copy"
    }
},
methods: {
    copyId(docId) {
        //some other stuff copying parameter to clipboard..
        
        this.idCopyIconName = "check";
        this.idCopyButtonText = "";
    }
}
</script>

Here is the thing: When the user click that button which eventually triggers that copyId method, I want to change that button's image and remove its text to improve some UX but only that button's image and text.

When I do it like the way I show, all button-base icons and texts affected, including the ones which are not clicked, as they are iterated in the same v-for.

How should I re-structure my props and components to achieve what I'm looking for.

Thanks in advance.


Solution

  • You can structure your components however you like, but it's important to remember that Vue relies on data. And your data should always flow down the structure (from parent to child), and events can rise from child components to parent components to, for example, change data in the parent component and then distribute the updated data to the child components that need it.

    In the example below, the data drives the css classes that modify the buttons.

    <base-button :button-text="read ? 'readed' : 'read'" :read="read" @copy-id="$emit('copy-id')"></base-button>
    

    The change concerns only those data that relate to the button that we pressed at the moment. In the parent component, we find the entry that relates to this button and change its status to “read”.

    copyId(docId) {
          const indexItemClick = this.list.findIndex(x => x.id === docId);
    
          if (indexItemClick !== -1) {
            this.list[indexItemClick].read = !this.list[indexItemClick].read;
          }
        }
    

    After which the data is passed through props to the child component and it is rendered according to this.

    ...
    app.component('base-button', {
      props: ['type', 'buttonText', 'read'],
    ...
    

    Full working example.

    const app = Vue.createApp({
      data() {
        return {
          list: [{
              id: 1,
              text: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. ',
              read: false
            },
            {
              id: 2,
              text: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. ',
              read: false
            }
          ]
        }
      },
      methods: {
        copyId(docId) {
          const indexItemClick = this.list.findIndex(x => x.id === docId);
    
          if (indexItemClick !== -1) {
            this.list[indexItemClick].read = !this.list[indexItemClick].read;
          }
        }
      }
    })
    
    app.component('base-button', {
      props: ['type', 'buttonText', 'read'],
      emit: ['copy-id'],
      template: `
    <button 
        type="button"
        :class="[read ? 'primary_button' : 'secondary_button']"
         @click="$emit('copy-id')">
        <div class="text_wrapper">
            {{ buttonText }}
        </div>
    </button>
    `
    })
    
    app.component('feed-card', {
      props: ['text', 'read'],
      emit: ['copy-id'],
      template: `
    <div>
      <p>{{ text }}</p>
      <base-button :button-text="read ? 'readed' : 'read'" :read="read" @copy-id="$emit('copy-id')"></base-button>
     </div>
    `
    })
    
    app.mount('#app')
    .primary_button {
      background: red;
    }
    
    .secondary_button {
      background: green;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.4.8/vue.global.min.js"></script>
    <div id="app">
      <feed-card v-for="l in list" :key="l.id" :read="l.read" :text="l.text" @copy-id="copyId(l.id)"></feed-card>
    </div>

    Sorry for my bad English)