Vue.js ref inside the v-for loop

Javascriptvue.jsVuejs2Vue Component

Javascript Problem Overview


I tried to use components inside v-for loop and init the ref to future access some methods of these from parent. Here a simplified code of my case:

<template>
    <div class="hello">
        {{ msg }}
        <ul>
            <list-item 
                v-for="item in items" 
                :key="item.id" 
                :value="item.text" 
                :ref="`item${item.id}`"
            />
        </ul>
    </div>
</template>

<script>
    import ListItem from "./ListItem";
    export default {
        name: "HelloWorld",
        components: {
            ListItem
        },
        data() {
            return {
                msg: "Welcome to Your Vue.js App",
                items: [
                    { id: 1, text: "foo" },
                    { id: 2, text: "bar" },
                    { id: 3, text: "baz" },
                    { id: 4, text: "foobar" }
                ]
            };
        },
        mounted() {
            setTimeout(() => this.$refs.item2.highlight(), 1500);
        }
    };
</script>

And ListItem component:

<template>
    <li v-bind:class="{ highlight: isHighlighted }">
        {{value}}
    </li>
</template>

<script>
    export default {
        name: "list-item",
        props: ["value"],
        data() {
            return {
                isHighlighted: false
            };
        },
        methods: {
            highlight() {
                this.isHighlighted = !this.isHighlighted;
            }
        }
    };
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
    .highlight {
        color: red;
    }
</style>

It's just renders a few list items and highlights one of them after one and half second. But I got an error: Uncaught TypeError: _this.$refs.item2.highlight is not a function
After debug session I've found an interesting fact: refs defined inside v-for loop are not a components but the arrays with one component.
What is the logic, what is the f wrapper? Does anyone meet this case? Can somebody give the explanation of this behaviour?
Code presented above works fine with setTimeout(() => this.$refs.item2[0].highlight(), 1500);
Must I always pass [0]? Is there exist a better way? Help, please.

Javascript Solutions


Solution 1 - Javascript

When using refs with v-for, the component / DOM nodes are stored as an array directly to the variable name so you don't need to use index number in the ref name. So you can do this:

<list-item
  v-for="item in items" 
  :key="item.id" 
  :value="item.text" 
  ref="items"
/>

And use the refs in your component like this:

this.$refs.items[index]

Also note that the refs may not be in order and would need to be handled in a different way which is a completely different issue. You can follow that here: https://github.com/vuejs/vue/issues/4952

Solution 2 - Javascript

For Vue 3 users:

In Vue 3, such usage will no longer automatically create an array in $refs. To retrieve multiple refs from a single binding, bind ref to a function which provides more flexibility (this is a new feature):

HTML

<div v-for="item in list" :ref="setItemRef"></div>

With Options API:

export default {
  data() {
    return {
      itemRefs: []
    }
  },
  methods: {
    setItemRef(el) {
      if (el) {
        this.itemRefs.push(el)
      }
    }
  },
  beforeUpdate() {
    this.itemRefs = []
  },
  updated() {
    console.log(this.itemRefs)
  }
}

With Composition API:

import { onBeforeUpdate, onUpdated } from 'vue'

export default {
  setup() {
    let itemRefs = []
    const setItemRef = el => {
      if (el) {
        itemRefs.push(el)
      }
    }
    onBeforeUpdate(() => {
      itemRefs = []
    })
    onUpdated(() => {
      console.log(itemRefs)
    })
    return {
      setItemRef
    }
  }
}

Here is the doc link: https://v3.vuejs.org/guide/migration/array-refs.html

Solution 3 - Javascript

I tried to handle refs inside v-for by passing index from method:

<div v-for="(item, index) in items" @click="toggle(index)">
  <p ref="someRef"></p>
</div>

toggle(index) {
  this.refs['someRef'][index].toggle();
}

But in reality it was toggling the wrong elements as indexes of refs are not ordered.

So what I did is added data attribute to ref elements:

<div v-for="(item, index) in items" @click="toggle(index)">
  <p ref="someRef" :data-key="index"></p>
</div>

Now each ref has its specific data-key. And can be toggled like this:

toggle(index) {
  const dropdown = this.$refs['someRef'].find(
		el => el.$attrs['data-key'] === index
	);
  dropdown.toggle();
}

Solution 4 - Javascript

I had faced the same issue.

As sobolevon mentioned, the returning value of $refs.{ref name} is an array in v-for refs, so my solution is to consider $refs.{ref name} is an array with one item only by default, and write $refs.{ref name}[0].methodToCall().

And it works for my case.

Solution 5 - Javascript

I solved the ordering issue by using a dynamic ref: :ref="'myRef' + index".

If you do this, Vue creates a new array for each item in the v-for, the only element of which will always be the ref you want. You can then access it with this.$refs['myRef' + index][0].

(This won't work in Vue 3.)

Solution 6 - Javascript

For anyone using Vue 3 with Typescript and this (vuejs/core#5525) issue still being open. Based on the other answers, you can do something like this:

<div
   v-for="item in items"
   :ref="addRef"
   ...
</div>

...

function addRef(el: unknown) {
  if (el instanceof Element) {
    participantRefs.value.push(el);
  }
}

Solution 7 - Javascript

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.21/vue.js"></script>
<div 
   v-for="(item,index) in items"
   :key="index">
<q-popup-proxy
   ref="qDateProxy">
  <q-date
     :data-key="index"
     v-model="item.date"
     @input="CalendarHide(index)"
     mask="MM/DD/YYYY"
    range>
  </q-date>
</q-popup-proxy>
</div>

<script>
CalendarHide (Val) {
      this.$refs ['qDateProxy'] [val].hide()
}
</script>

Solution 8 - Javascript

Building on @Syed answer with Vue 3 you have the issue stated here https://vuejs.org:

> It should be noted that the ref array does not guarantee the same > order as the source array.

I ran into the issue that I needed the rendered list to have parity with the refs list. This is what I do to solve this issue:

<script setup>
  import { ref } from 'vue'
  import Comp from './Comp.vue'

  const list = ref([
    {
      name: 'Stripe',
      ref: null,
    },
    {
      name: 'Default',
      ref: null,
    }
  ]);

  function setItemRef(el, idx) {
    if (el) {
      list.value[idx].ref = el;
    }
  }
</script>

<template>
  <ul>
    <li v-for="(item, idx) in list">
      <Comp :ref="(el) => setItemRef(el, idx)"/>
      {{item}}
    </li>
  </ul>
</template>

Here is this example running in SFC: https://sfc.vuejs.org

Solution 9 - Javascript

Considering your primary question: https://v2.vuejs.org/v2/api/#ref

The documentation says:

> When ref is used together with v-for, the ref you get will be an array containing the child components mirroring the data source.

But, I would say you are doing it wrong, because using refs are not a good way to go. We have very useful alternatives in vue-land. For example, one can use a prop.

That's how a rewritten version of your code would look like:

<template>
    <div class="hello">
        {{ msg }}
        <ul>
            <list-item 
                v-for="item in items" 
                :key="item.id" 
                :value="item.text" 
                :isHighlighed="item.isHighlighed"
            />
        </ul>
    </div>
</template>

<script>
    import ListItem from "./ListItem";
    export default {
        name: "HelloWorld",
        components: {
            ListItem
        },
        data() {
            return {
                msg: "Welcome to Your Vue.js App",
                items: [
                    // We have moved `isHighlighed` falg into the data array:
                    { id: 1, text: "foo", isHighlighed: false },
                    { id: 2, text: "bar", isHighlighed: true },
                    { id: 3, text: "baz", isHighlighed: false },
                    { id: 4, text: "foobar", isHighlighed: false }
                ]
            };
        };
    };
</script>

And then change your component definition to receive a new prop:

<script>
    export default {
        name: "list-item",
        props: ["value", "isHighlighted"]
    };
</script>

This will solve your issue.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionLev KhruschevView Question on Stackoverflow
Solution 1 - JavascriptSagar ChakravarthyView Answer on Stackoverflow
Solution 2 - JavascriptSyedView Answer on Stackoverflow
Solution 3 - JavascriptAlonadView Answer on Stackoverflow
Solution 4 - Javascript杨成林View Answer on Stackoverflow
Solution 5 - JavascriptYitzView Answer on Stackoverflow
Solution 6 - JavascriptVissieView Answer on Stackoverflow
Solution 7 - JavascriptPrajeesh K PView Answer on Stackoverflow
Solution 8 - JavascripttimView Answer on Stackoverflow
Solution 9 - JavascriptsobolevnView Answer on Stackoverflow