Reputation: 1785
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.
Upvotes: 101
Views: 103277
Reputation: 215
If you've added vueuse
to your project (which I recommend), there is a perfect composable function to handle this job but only works in vue3.
<script setup>
import { watchEffect } from 'vue'
import { useTmplateRefsList } from '@vueuse/core'
const refs = useTemplateRefsList()
watchEffect(() => {
if(refs.value.length > 0) {
console.log(refs.value)
}
})
</script
<tempalte>
<ul>
<list-item
v-for="item in items"
:key="item.id"
:value="item.text"
:ref="refs.set"
/>
</ul>
</template>
More info: vueuse
Upvotes: 0
Reputation: 16513
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-migration.vuejs.org/breaking-changes/array-refs.html
Upvotes: 59
Reputation: 777
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:
Update: vuejs/core#5525 seems to be fixed so a different solution may be better.
<div
v-for="item in items"
:ref="addRef"
...
</div>
...
function addRef(el: unknown) {
if (el instanceof Element) {
participantRefs.value.push(el);
}
}
Upvotes: 6
Reputation: 126
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
Upvotes: 8
Reputation: 185
<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>
Upvotes: 1
Reputation: 1306
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.)
Upvotes: 4
Reputation: 2256
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();
}
Upvotes: 18
Reputation: 1366
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
Upvotes: 123
Reputation: 81
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.
Upvotes: 8