简体   繁体   中英

Adding 'input' used to edit array object key in selected elements - Vue.js

i need to add an input field used to edit the title in the currently selected element component (selection was done by clicking). The problem is that there should be one input and work for each selected element . I couldn't find a similar task and solving on the Internet. Maybe someone will tell you how to do it?

ItemsList.vue component:

<template>
    <input type="text" placeholder="Edit selected items"/>
    <div class="items-col">
        <ul class="items-list">
            <Item v-for="item in items" :key="item" :title="item.title"/>
        </ul>
    </div>
</template>

<script>
import Item from '@/components/Item.vue'
export default {
    data() {
        return {
            items: [
                { title: 'item 1' },
                { title: 'item 2' },
                { title: 'item 3' },
                { title: 'item 4' },
                { title: 'item 5' },
                { title: 'item 6' }
            ]
        }
    },
    components: {
        Item
    }
}
</script>

Item.vue component:

<template>
    <li class="item" @click="isActive = !isActive" :class="{ active: isActive }">{{ title }}</li>
</template>

<script>

export default {
    name: 'ItemsList',
    data() {
        return {
            isActive: false
        }
    },
    props: {
        title: String
    }
}
</script>

<style>
    .item.active {
        color: red;
    }
</style>

You might want to reconsider which component should be responsible of knowing which item is active at any point of time: hint: it should be the parent/consuming component. That is because you:

  1. Have only a single input field, which means only one item can be edited at any point of time
  2. You want to let the parent/consuming component to be the single source of truth of which item is actually active

Therefore, the first thing you should do is to ensure that isActive is a prop on the Item component, while the parent ItemList component keeps track of which is active at any point.

Then, it is simply a matter of:

  • Implementing a toggling logic for the isActive flag. The flag is updated when a native click event is fired from the Item component. For the toggling logic, we can simply toggle between a zero-based index of the click item, or -1 , which we used to indicate that nothing is active.
  • Using v-bind:value and a computed property to reflect the value of the currently active item. We can simply retrieve it using this.items[this.activeIndex] on the parent component
  • Listening to the onInput event and then updating the correct item

See proof-of-concept below:

 Vue.component('item-list', { template: '#item-list-template', data() { return { items: [{ title: 'item 1' }, { title: 'item 2' }, { title: 'item 3' }, { title: 'item 4' }, { title: 'item 5' }, { title: 'item 6' } ], activeIndex: -1, } }, methods: { onItemClick(index) { this.activeIndex = this.activeIndex === index ? -1 : index; }, setActiveItemValue(event) { const foundItem = this.items[this.activeIndex]; if (!foundItem) return; return this.items[this.activeIndex].title = event.currentTarget.value; } }, computed: { activeItemValue() { return this.items[this.activeIndex]?.title ?? ''; } } }); Vue.component('item', { template: '#item-template', props: { isActive: Boolean, title: String } }); new Vue({ el: '#app' });
 li.active { background-color: yellow; }
 <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script> <div id="app"> <item-list></item-list> </div> <script type="text/x-template" id="item-list-template"> <div> <input type="text" placeholder="Edit selected items" :value="activeItemValue" @input="setActiveItemValue" /> <div class="items-col"> <ul class="items-list"> <item v-for="(item, i) in items" :key="i" :title="item.title" :isActive="activeIndex === i" @click.native="onItemClick(i)" /> </ul> </div> </div> </script> <script type="text/x-template" id="item-template"> <li class="item" :class="{ active: isActive }">{{ title }}</li> </script>

If you want a solution with your current components (not the cleanest) , you can actually emit an event to the parent component when you activate an element that event should containe the index of the object in the items array

Then you can use the index to get and set the title variable , here is an example :

Item.vue

<template>
  <li class="item" @click="activateItem" :class="{ active: isActive }">{{ title }}</li>
</template>

<script>

export default {
  name: 'ItemsList',
  data() {
    return {
      isActive: false
    }
  },
  methods:{
    activateItem() {
      this.isActive = !this.isActive
      this.$emit('activatedItem', this.isActive ? this.index : null)
    }
  },
  props: {
    title: String,
    index: Number
  }
}
</script>

<style>
.item.active {
  color: red;
}
</style>

ItemList.vue

<template>
  <div>
    <input type="text" placeholder="Edit selected items" @input="inputChange" :value="inputValue"/>
    <div class="items-col">
      <ul class="items-list">
        <Item v-for="(item, index) in items" :key="index" :title="item.title" :index="index" @activatedItem="itemSelected"/>
      </ul>
    </div>
  </div>
</template>

<script>
import Item from '@/components/Item.vue'
export default {
  data() {
    return {
      items: [
        { title: 'item 1' },
        { title: 'item 2' },
        { title: 'item 3' },
        { title: 'item 4' },
        { title: 'item 5' },
        { title: 'item 6' }
      ],
      selectedIndex: null,
      inputValue: ''
    }
  },
  methods:{
    itemSelected(index){
        this.selectedIndex = index;
        if(this.selectedIndex != null) {
          this.inputValue = this.items[this.selectedIndex].title;
        }
    },
    inputChange(event){
      this.inputValue = event.target.value;
      if(this.selectedIndex != null){
        this.items[this.selectedIndex].title = this.inputValue
      }
    }
  },
  components: {
    Item
  }
}
</script>

You should also be aware that with the component Item you have given you can select more than one item !

For your second question you should just change the index to match the one from the array , for instance if rowIndex starts with 1 :

onItemClick(index, rowIndex) {
    let correctIndex = (index + (this.itemsPerRow * (rowIndex - 1));
    this.activeIndex = this.activeIndex === correctIndex ? -1 : correctIndex);
},

Thank's guys, it's working fine.

Another question for this task:

How to make it work for the condition that the elements are in a row of 3 items?

For example i need to get this final effect:


<ul class="items-list">
    //my Item.vue component
    <li class="item" :class="{ active: isActive }">item 1</li>
    //my Item.vue component
    <li class="item" :class="{ active: isActive }">item 2</li>
    //my Item.vue component
    <li class="item" :class="{ active: isActive }">item 3</li>
</ul>
<ul class="items-list">
    //my Item.vue component
    <li class="item" :class="{ active: isActive }">item 4</li>
    //my Item.vue component
    <li class="item" :class="{ active: isActive }">item 5</li>
    //my Item.vue component
    <li class="item" :class="{ active: isActive }">item 6</li>
</ul>

I wrote a method that separates items by 3 in a row and after your solution, I have the following:

ItemsList.vue component:

<template>
    <div class="items-col">
        <input type="text" placeholder="Edit selected items" :value="activeItemValue" @input="setActiveItemValue" />
        <ul class="items-list" v-for="index in rowCount" :key="index">
            <Item v-for="(item, i) in itemCountInRow(index)" :key="i" :title="item.title"  :isActive="activeIndex === i" @click="onItemClick(i)"/>
        </ul>
    </div>
</template>

<script>

import Item from '@/components/Item.vue'

export default {
    name: 'ItemsList',
    data() {
        return {
            items: [
                { title: 'item 1' },
                { title: 'item 2' },
                { title: 'item 3' },
                { title: 'item 4' },
                { title: 'item 5' },
                { title: 'item 6' },
            ],
            activeIndex: -1,
            itemsPerRow: 3
        }
    },
    components: {
        Item
    },
    computed: {
        rowCount(){
            return Math.ceil(this.items.length / this.itemsPerRow);
        },
        activeItemValue() {
            return this.items[this.activeIndex]?.title ?? '';
        }
    },
    methods: {
        onItemClick(index) {
            this.activeIndex = this.activeIndex === index ? -1 : index;
        },
        setActiveItemValue(event) {
            const foundItem = this.items[this.activeIndex];
            if (!foundItem) return;
            
            return this.items[this.activeIndex].title = event.currentTarget.value;
        },
        itemCountInRow(index){
            return this.items.slice((index - 1) * this.itemsPerRow, index * this.itemsPerRow)
        }
    }
}
</script>

Now when i click on the item your method only works for the first list.

Is it possible to make it work together with the division of 3 items per row?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM