Metal3d
Metal3d

Reputation: 2941

Remove element in data doesn't refresh keys in VueJS

I'm trying to make a list of items that provides an input and a color picker. It should always have an empty element at the end of the list.

Elements are populated with objects that are stored in data.

Everything is OK, I do a simple method that filters objects with empty value, then add a new one.

But, when the user set empty value on an element that is not the last one:

This is what I do on the parent Vue:

addEmptyElement() {
  // now find any empty values
  this.values = this.values.filter((v) => v.length);
  // and append an empty one
  this.values.push("");
},

And the values are use to bind Item component like this:

<Item
  v-for="(v, i) in values"
  :key="i"
  v-bind:text="v"
  @valueChanged="onItemChanged"
/>

The "valueChanged" event is emitted by Item and provides the position in the list and value that chaged.

I created a simpler example that you can check here:

https://codesandbox.io/s/vue-component-order-problem-rc4o1?file=/src/App.vue

To give some precision:

Maybe I did it wrong, with a bad implementation, so please, tell me what I badly do :)

Thanks !

Upvotes: 1

Views: 1525

Answers (1)

Michal Lev&#253;
Michal Lev&#253;

Reputation: 37763

I think what you see has more to do with this paragraph in documentation

When Vue is updating a list of elements rendered with v-for, by default it uses an "in-place patch" strategy. If the order of the data items has changed, instead of moving the DOM elements to match the order of the items, Vue will patch each element in-place and make sure it reflects what should be rendered at that particular index.

This default mode is efficient, but only suitable when your list render output does not rely on child component state or temporary DOM state (e.g. form input values).

This why using simple index in v-for as a key is tricky. It's much better to not rely on index and maintain your own unique id even it's a bit more work...

Also the usage of this.$.vnode.key smells...it is not a documented API so you should definitely avoid it and just pass an id by a prop explicitly

working exaple

...other thought

Just after I posted my answer I realized one more thing ...so just to be complete:

This can easily work with v-for index as a key. The reason why it didn't worked for you is in your <Item> component. You pass a text prop to your Item component. You use the value of the prop for one time initialization of the value in the data and use the value as v-model for the input.

What happens when the list is re-rendered ? Vue updates the text prop for each item in the list, but that update has no effect on the internal component's value used as v-model (so it has no effect on the value displayed in the <input> element)!!

Solution is to rewrite Item component:

<template>
  <div>
    <input type="text" v-model="model" />
  </div>
</template>
<script>
export default {
  props: ["modelValue"],
  computed: {
    model: {
      get() {
        return this.modelValue;
      },
      set(evt) {
        this.$emit("update:modelValue", evt);
      },
    },
  },
};
</script>

...and change the App.vue:

<Item
    v-for="(v, i) in values"
    :key="i"
    :modelValue="v"
    @update:modelValue="onItemChanged(i, $event)"
  />

And it just works with the rest of the code unchanged - 2nd example

Upvotes: 2

Related Questions