Vue.js ref inside the v-for loop
Javascriptvue.jsVuejs2Vue ComponentJavascript 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.