0

I am trying to access an element in Vue as I want to see when it's class changes (I'm using Bootstrap so I want to watch for when a class changes to shown). The problem is that as soon as I put a ref'd element into a v-for loop it always returns null. I have tried appending the ref's name with the index of the loop item, and then trying to log this to the console it just always returns 'null'.

As soon as I take the element out of the v-for loop it returns the element fine.

I can't seem to find a solution. Is this something that Vue just simply doesn't support? If not, is there a better way for me to watch the DOM to see if an element's class changes?

P.S. I'm using the composition API.

Template:

<template>
    <div v-for="room in rooms" :key="room.id" class="">
        <p class="test" :ref="'test'+room.id">Hello world</p>
    </div>
</template

Script:

setup(){
      const test1 = ref(null)

      onMounted(() => {
          console.log("test1: ", test1.value)
      })

      return { test1 }
}

2 Answers 2

1

This looks like a pretty common problem, so I went digging through the Vue source and found the following:

It's a limitation of the way Vue does updates and renders. If you have multiple s for the same template, then each of them has its own patch queue. Each patch queue gets processed on its own schedule, so it's possible for one to finish before another even starts. If that happens, then one of the queued updates won't happen. That would mean Vue wouldn't detect changes to your element and hence wouldn't do any re-rendering at all, which is why your element doesn't show up in your console output!

You can work around this by making sure all update queues finish before each other begins, which we can do by adding an update: { sync: true } config to your component in question. This will force Vue to process all of your update queues synchronously before processing any other changes in the application.

You could also add a sync: true config to your parent's component that wraps all of your other components and make sure they all sync.

<script>
  setup() {
    const test1 = ref(null)

    onMounted(() => {
      console.log("test1: ", test1.value)
    })

    return { test1 }
  }

  render() {
      return (
        <div>Hello World</div>
      )}

  </script>

6
  • Thank you for your reply. Where would I apply the update: {sync: true} snippet into my code exactly? Is this what you're doing with this render() function? I've never used the render function before.
    – mellows
    Commented Mar 9, 2022 at 17:51
  • You'd apply the {sync: true} config to the component where you want to ensure that updates are processed synchronously.
    – PJMan0300
    Commented Mar 9, 2022 at 17:58
  • Sorry I am new to Vue and still learning - what is the render() function you've added to my code doing? And so does the update: {sync: true} go in the template in the tag of the wrapper of my v-for list? If there is any documentation on this I can refer to please link me.
    – mellows
    Commented Mar 9, 2022 at 20:18
  • Sorry for the late reply! To answer your question: Yes, it does. The render() function is your component's render function. It's called when the data in your component changes and the component needs to re-render itself, which means it updates its patch queue.
    – PJMan0300
    Commented Mar 9, 2022 at 21:56
  • The patch queue that you've mentioned is a list of changes that will be applied to your element at some point in the future. This means that this queue can include virtual DOM elements that won't actually be added to the DOM until later, after all elements with lower priorities have been processed first. That's why it may happen for one of these queues to finish before another one starts - since the higher priority patches get applied first, and then lower priority ones are only processed when there are no higher priority patches pending anymore.
    – PJMan0300
    Commented Mar 9, 2022 at 21:56
1

Template refs in a v-for

There's a known bug (vuejs/core#5525), where array refs are not properly populated.

One workaround is to bind a function that pushes the element refs into the array:

                       👇
<p class="test" :ref="el => roomRefs.push(el)">Hello world</p>

demo 1

Watching class changes

To observe changes to the class attribute of the elements, use a MutationObserver.

mounted hook: For each roomRefs item:

  1. Create a MutationObserver with a callback that handles mutations (i.e., a change to the observed attribute).

  2. Use that MutationObserver instance to observe the class attribute on the roomRefs item.

  3. Collect each observer into an array, as we'll need to disconnect the observer upon unmounting.

unmounted hook:

  1. disconnect all observers to stop watching for changes. This cleanup step is necessary to avoid a memory leak.
export default {
  props: {
    rooms: {
      type: Array,
      required: true,
    },
  },
  setup() {
    const roomRefs = ref([])
    let observers = []

    onMounted(() => {
      1️⃣
      observers = roomRefs.value.map(roomEl => {
        const observer = new MutationObserver(mutations => {
          for (const m of mutations) {
            const newValue = m.target.getAttribute(m.attributeName)
            console.log('class changed', { roomEl, newValue, oldValue: m.oldValue })
          }
        })
        2️⃣
        observer.observe(roomEl, {
          attributes: true,
          attributeOldValue: true,
          attributeFilter: ['class'],
        })
        3️⃣
        return observer
      })
    })
    4️⃣
    onUnmounted(() => observers.forEach(obs => obs.disconnect()))
  }
}

demo 2

2
  • Fantastic thank you I have managed to implement this in my code. One thing - what is the purpose of the onUnmounted() function? I get an error in my code saying observer isn't defined until I remove the Unmounted function. Will this be problematic without it?
    – mellows
    Commented Mar 12, 2022 at 12:51
  • That was a typo in my code (fixed now). observer.forEach should've been observers.forEach (with an s). That is needed as cleanup to avoid a memory leak.
    – tony19
    Commented Mar 12, 2022 at 22:31

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.