Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v-for, refs and access to refs.<array of components> by array index after Array.unshift #4952

Closed
alekzonder opened this issue Feb 17, 2017 · 28 comments

Comments

@alekzonder
Copy link

alekzonder commented Feb 17, 2017

here example: https://jsfiddle.net/alekzonder/1zcLkuwq/
vue.js 2.1.10

steps in example:

  1. press "unshift new" button
  2. then press "set bold" with 0 index in input

problem: item with index 1 is bold after Array.unshift in data

if remove :key=item.id in template with v-for, all works right

bug or feature?

@Kingwl
Copy link
Member

Kingwl commented Feb 17, 2017

https://jsfiddle.net/z11fe07p/773/
a simple version
looks like a bug but i have no idea to fix that
the problem is https://github.com/vuejs/vue/blob/dev/src/core/vdom/modules/ref.js#L21
only push the new ref to refs

@alekzonder
Copy link
Author

@Kingwl thanks for simple version!

i can iterate $refs.items and search component by data, but not sure this right way

@yyx990803
Copy link
Member

Note that v-for refs do not guarantee the same order as your source Array.

In the next release you can determine how to register refs yourself by passing a function to :ref.

@ixcat
Copy link

ixcat commented May 23, 2017

Sorry to respond to a closed issue -

Is the list behavior documented anywhere?

I did not see in for example:

https://vuejs.org/v2/guide/list.html#key
https://vuejs.org/v2/guide/migration.html#v-for-Argument-Order-for-Objects-changed

Seems to snag others, occasionally, as seen here and in:
https://forum-archive.vuejs.org/topic/5190/this-refs-ref_name-on-dynamic-component-is-array-instead-of-vuecomponent/3

seems a quick note might be beneficial to save headaches..

thanks for a great framework either way :)

@feliperaul
Copy link

"Note that v-for refs do not guarantee the same order as your source Array"

Well this is a huge bummer.

@Fmajor
Copy link

Fmajor commented Sep 21, 2017

In the document https://vuejs.org/v2/guide/components.html#Child-Component-Refs, it says

$refs are only populated after the component has been rendered, and it is not reactive.

But i really have the requirement for a dynamic ref in v-for

for the example @Kingwl https://jsfiddle.net/z11fe07p/773/, i found a solution https://jsfiddle.net/Fmajor/cuay7v1j/2/

just manually clear the old ref array to null, and it works
==========after other tries============
i found that vue only push new added vue components in the ref array in the v-for loop, so in my solution, the its array will only have one item, problem not solved.
we need to wait the new ":ref" feature from @yyx990803

@feliperaul
Copy link

For the problem of $refs not being reactive, I'm using this.$nextTick(function () { }) and it works, I get an updated list of $refs.

But for the problem of v-for $refs returning an array that does NOT match the order of your source data, I had to code a hack using custom uids, and resetting those uids (0 indexed) just before trying to access $refs, and then filtering that array by that uid property.

@adamwathan
Copy link

Anyone come up with a solution they think is clean for this yet? It’s currently really hard to answer questions like “what is the bounding box of the element where item x in this array is being rendered?” since the refs array order is not synchronized with the data source order.

Best I’ve come up with is assigning a different unique ref name for each item in the list like :ref=“‘option:’ + item.id” but that’s really gross, especially considering I have to access that ref as element 0 of an array with only one item in it now since the ref is on a v-for.

Other solution I had was adding a :data-key=“...” attribute so I can find the ref I want by traversing all of them and looking for a match but man does it ever feel anti-Vue to be storing data in the DOM like that.

@ghost
Copy link

ghost commented May 9, 2018

@yyx990803
Is there any update on this?

@trollfred

This comment has been minimized.

@HaykoKoryun
Copy link

An approach we just took is to give a _dirty property to each element in the array and bind that as the key à la: :key="item._dirty". The _dirty property is a pseudo random string e.g. `${Date.now()}:${Math.random()}`and it gets regenerated for each element when the $refs array order needs to be synchronised with the underlying data array order.

@zijjj
Copy link

zijjj commented Jul 18, 2018

@yyx990803
如果更新后的refs数组与源数据数组的顺序不一致,那么这个refs数组还有什么意义呢?
数组元素我们只能通过索引来查找,但是这时候顺序又不一致,那这不算是一个BUG吗?

@karljakober
Copy link

I needed to find a dom resource of the first element in a v-for using a dynamic component.
I solved this using lodash to find where :key was equal to 0, but I'm concerned about if the array gets huge.

let index = _.findIndex(this.$refs.dataArray, function(ele) { return ele.$vnode.key == 0; });
this.$refs.dataArray[index].$el.offsetHeight;

@fnlctrl
Copy link
Member

fnlctrl commented Jul 24, 2018

@zijjj @karljakober
The current behavior is to ensure better performance, otherwise we get the overhead of ensuring $ref array index every time render functions are called.

The correct approach to get the correct index is just as @adamawang proposed:
Bind index to dom as :data-index="index" and then access it via parseInt(el.dataset.index) when using $refs

@Anima-t3d
Copy link

https://vuejs.org/v2/guide/components-edge-cases.html#Accessing-Child-Component-Instances-amp-Child-Elements

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

I think this text is misleading as clearly it doesn't "mirror" the sequence.
Should be:

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 take note that v-for refs do not guarantee the same order as your source Array.

It took me a few hours to discover that the order in $refs was not guaranteed.

For being complete these are the resources I went through to find this issue:

  1. refs[key].push(ref)
    (discovered that the implementation is just pushing)
  2. https://github.com/vuejs/vue/blob/52719ccab8fccffbdf497b96d3731dc86f04c1ce/test/unit/features/ref.spec.js (discovered that the spec only tests sequential DOM/data)
  3. Found this thread when looking to report this issue
  4. https://vuejs.org/v2/api/#ref (checked and doesn't mention this behaviour)
  5. https://vuejs.org/v2/guide/components-edge-cases.html#Accessing-Child-Component-Instances-amp-Child-Elements (checked here and also doesn't mention anything, it even insinuates the opposite)

@thedamon
Copy link

thedamon commented Jan 29, 2019

Trying to scroll to a newly added element similar to @adamwathan ..
would it be more performant to just grab the dom element based on a known data-attribute and scroll to that, or to create a ref and access the first element of its array to scroll to..?

both look fairly bad, but i think the ref approach maybe looks slightly less bad if we look past the [0], assuming if i understand correctly that it is an array bc Vue automatically places any ref inside an array if it's on or inside a v-for?

The other option would be setting the same ref to get a multi-item array and filtering for the one I want...

What would be the recommended approach here?

@cihad
Copy link

cihad commented Jun 12, 2019

I think the best way is that to write vue instance the object of iterating:

https://jsfiddle.net/cihad/2a9wyvbh/

@dexcell

This comment has been minimized.

@mernst-github
Copy link

mernst-github commented Mar 26, 2020

I'm late to this party but wouldn't the obvious fix be for vue to publish for-refs as an object keyed with the item :key instead of an array? vue already uses :key to identify the items, why shouldn't we?

FWIW, changing

export function registerRef (vnode: VNodeWithData, isRemoval: ?boolean) {
to this makes for & ref immediately useful:

export function registerRef (vnode: VNodeWithData, isRemoval: ?boolean) {
    let key = vnode.data.ref;
    if (!isDef(key)) return;

    let refs = vnode.context.$refs;
    if (vnode.data.refInFor) {
        // In a for loop, $refs[key] becomes a nested object keyed by node key.
        let hash = refs[key];
        if (!hash) {
            refs[key] = hash = {};
        }
        refs = hash;
        key = vnode.data.key;
    }

    if (isRemoval) {
        delete refs[key];
    } else {
        refs[key] = vnode.componentInstance || vnode.elm;
    }
}

This way, with a v-for...ref="item" :key="item.key" you can refer to vm.$refs.item[item.key()] without searching through an array or a node query.

Yes I understand this is not backward compatible, it's just for the sake of the argument.

@rightaway
Copy link

Is this code not guaranteed to work as expected (comes from https://forum.vuejs.org/t/better-method-of-using-refs-inside-a-v-for-based-on-object/21352/2)?

<list v-for="(item, key, index) in objectOfItems" :key="key">
  <q-popover ref="editor">
    <editor :item="item" @cancel="$refs.editor[index].close()">

Or is this code fine and this github issue is actually referring to a different problem?

@colefranz
Copy link

To anyone coming to this late, another potential workaround is to reorder your ref onto a parent and use it's children instead.

<div ref="listParent">
    <div v-for="..." :key="key">
        <item />
    </div>
</div>
...
this.$refs.listParent.children[index];

But I'm not a huge fan of this loose coupling to the markup as adding another element to the listParent could result in unexpected defects to another developer coming into the component.

@quacainia
Copy link

It's not amazing, but if it works for anyone else who runs into this problem, I just made a method that does a lookup based on parameters:

<template>
  <div v-for="item in items" :key="item.key">
    <myitem :item="item" ref="items"></myitem>
  </div>
</template>

<script>
export default {
  methods: {
    refItem(item) {
      return this.$refs.items.find((refItem) => {
        return refItem.item.key === item.key;
      })
    },
    otherMethod(item) {
      this.refItem(item).doThing();
    }
  }
}
</script>

For me using it is relatively low incidence so it's not too expensive overall. I don't think this would be great if it was for every render.

@Drumstix42
Copy link

I went with this implementation for when there's a unique identifier for the iterated array/object. Does this make sense?

                <item-card
                    v-for="item in items"
                    v-bind:ref="`item_${load.id}`"
                    v-bind:key="item.id"
                    v-bind:item="item">
                </item-card>

@infinnie
Copy link

It took me a few hours to discover that the order in $refs was not guaranteed.

So true.

@fuweichin
Copy link

fuweichin commented Aug 21, 2023

It took me 40 minutes to discover and solve the order problem.

I solve it by using a helper function syncArrayOrder, which is defined and demonstrated below:

<template>
<ul>
  <li v-for="item of itemList" :key="item.id" :data-id="item.id"><canvas ref="canvas"></canvas></li>
</ul>
</template>

<script>
export default {
  data(){
    return {
      itemList: []
    }
  },
  mounted(){
    this.itemList = [{id:'1'}, {id:'2'}];
    this.$nextTick(()=>{
      // let canvasArray = this.$refs.canvas;
      let canvasArray = syncArrayOrder(this.$refs.canvas || [], (elem)=>elem.parentNode.dataset.id, this.itemList, (item) => item.id);
    })
  }
};

function syncArrayOrder(elemArray, idFunc, dataArray, keyFunc) {
  let map = new Map();
  for (let i = 0; i < elemArray.length; i++) {
    let elem = elemArray[i];
    map.set(idFunc(elem), elem);
  }
  return dataArray.map((item) => {
    let key = keyFunc(item);
    return map.get(key);
  });
}
</script>

@daniilgri
Copy link

Note that v-for refs do not guarantee the same order as your source Array.

In the next release you can determine how to register refs yourself by passing a function to :ref.

Could someone take a look at this? I think it might be worth adding some notes on why it does not guarantee the right order
vuejs/docs#2598

@DazzRune
Copy link

It took me a few hours to discover that the order in $refs was not guaranteed.
This is not a record... So now it's 2024

@Drumstix42
Copy link

Drumstix42 commented Sep 26, 2024

See template Function Refs now in Vue 3: https://vuejs.org/guide/essentials/template-refs.html#function-refs
Also, note the useTemplateRef as of Vue 3.5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests