Wat is een template ref?

Normaal beschrijf je in Vue wat de UI moet doen, en Vue regelt het zelf. Maar soms heb je directe toegang tot een DOM-element nodig:

  • Focus zetten op een input bij het laden
  • Scrollen naar een specifiek element
  • Een video starten of pauzeren
  • Een canvas tekenen
  • Een externe library initialiseren (Chart.js, Leaflet, etc.)

Daarvoor gebruik je een template ref: een ref die naar een DOM-element verwijst in plaats van een waarde.

Vue vs React: dit is Vue's tegenhanger van useRef() in React. Zelfde idee, andere syntax. Het verwarrende: in Vue heet het ook gewoon ref() — dezelfde functie die je voor state gebruikt.

De drie stappen

Een template ref opzetten doe je in drie stappen:

<script setup>
import { ref, onMounted } from 'vue'

// 1. Maak een ref met dezelfde naam als het attribute
const inputEl = ref(null)

// 2. Gebruik hem in onMounted
onMounted(() => {
  console.log(inputEl.value)   // → het <input> DOM-element
  inputEl.value.focus()
})
</script>

<template>
  <!-- 3. Koppel via het ref attribute -->
  <input ref="inputEl" />
</template>

Belangrijk:

  • De ref begint op null — tijdens setup bestaat het DOM-element nog niet
  • De ref="inputEl" in de template moet matchen met de naam van je ref-variabele
  • Pas vanaf onMounted heeft inputEl.value het echte element

Waarom moet het in onMounted?

Een component levensloop:

  1. <script setup> draait — refs worden aangemaakt
  2. De template wordt naar HTML omgezet
  3. HTML wordt in de pagina gezet → nu pas bestaan de DOM-elementen
  4. onMounted draait

Tussen stap 1 en 3 is je ref null — er is nog geen DOM-element om naar te verwijzen. Daarom werkt dit niet:

<script setup>
import { ref } from 'vue'

const inputEl = ref(null)
inputEl.value.focus()    // ❌ inputEl.value is null
</script>

En dit wel:

<script setup>
import { ref, onMounted } from 'vue'

const inputEl = ref(null)
onMounted(() => {
  inputEl.value.focus()  // ✅ DOM bestaat nu
})
</script>

Vergeet deze regel niet: alle code die de echte DOM aanraakt moet in onMounted (of een event handler die later draait).

Voorbeeld 1 — Focus op input bij laden

De klassieke use case. Een zoekbalk of login-form: zodra de pagina laadt, kan de gebruiker meteen typen.

<script setup>
import { ref, onMounted } from 'vue'

const searchInput = ref(null)

onMounted(() => {
  searchInput.value.focus()
})
</script>

<template>
  <input
    ref="searchInput"
    type="search"
    placeholder="Zoek..."
  />
</template>

Focus na een actie

Na het toevoegen van een todo: focus terug op het input:

<script setup>
import { ref } from 'vue'

const newTodo = ref('')
const todos = ref([])
const inputEl = ref(null)

const addTodo = () => {
  if (!newTodo.value) return

  todos.value.push(newTodo.value)
  newTodo.value = ''

  // Focus terug op input voor de volgende todo
  inputEl.value.focus()
}
</script>

<template>
  <form @submit.prevent="addTodo">
    <input ref="inputEl" v-model="newTodo" />
    <button type="submit">Voeg toe</button>
  </form>
  <ul>
    <li v-for="t in todos">{{ t }}</li>
  </ul>
</template>

Wat heb je nu? De gebruiker kan tien todos achter elkaar toevoegen zonder elke keer op het input te klikken. Kleine details als deze maken een app meteen prettiger.

Voorbeeld 2 — Scrollen naar een element

Een chat-app die naar het laatste bericht scrollt, of een knop "Naar boven":

<script setup>
import { ref, onMounted } from 'vue'

const chatBottom = ref(null)

const scrollToBottom = () => {
  chatBottom.value.scrollIntoView({ behavior: 'smooth' })
}

onMounted(() => {
  scrollToBottom()
})
</script>

<template>
  <div class="chat">
    <div v-for="msg in messages" :key="msg.id">{{ msg.text }}</div>

    <!-- Onzichtbare ankertag onderaan -->
    <div ref="chatBottom"></div>
  </div>

  <button @click="scrollToBottom">Naar onder ↓</button>
</template>

Voorbeeld 3 — Video controleren

Soms moet je een media-element programmatic besturen — bijv. play/pause via een eigen UI:

<script setup>
import { ref } from 'vue'

const videoEl = ref(null)
const isPlaying = ref(false)

const togglePlay = () => {
  if (isPlaying.value) {
    videoEl.value.pause()
  } else {
    videoEl.value.play()
  }
  isPlaying.value = !isPlaying.value
}
</script>

<template>
  <video ref="videoEl" src="/movie.mp4"></video>

  <button @click="togglePlay">
    {{ isPlaying ? '⏸ Pauze' : '▶ Play' }}
  </button>
</template>

Refs in een v-for — een array van elementen

Voor een lijst kun je een ref op elk item zetten. Vue verzamelt ze in een array:

<script setup>
import { ref, onMounted } from 'vue'

const itemRefs = ref([])
const items = ['Anna', 'Bram', 'Chloé']

onMounted(() => {
  console.log(itemRefs.value)        // → array van 3 <li> elementen
  itemRefs.value[0].focus()          // eerste item
})
</script>

<template>
  <ul>
    <li
      v-for="item in items"
      :key="item"
      ref="itemRefs"
    >
      {{ item }}
    </li>
  </ul>
</template>

Let op: de volgorde van elementen in de array komt overeen met de volgorde in v-for, maar is niet gegarandeerd stabiel bij wijzigingen. Voor de meeste cases is dat geen probleem.

Belangrijke Regels

  • Maak een ref met ref(null) bovenaan je script
  • Koppel via ref="naam" in de template — moet matchen met de ref-variabele
  • Gebruik .value in JS, net als bij gewone refs
  • Code die de DOM aanraakt moet in onMounted
  • In een v-for krijg je een array van elementen

Veelgemaakte Fouten

Fout — gebruik in <script setup> op top-level:

<script setup>
const inputEl = ref(null)
inputEl.value.focus()    // ❌ inputEl.value is null tijdens setup
</script>

Goed — wacht op onMounted:

<script setup>
const inputEl = ref(null)
onMounted(() => inputEl.value.focus())    // ✅
</script>

Fout — naam mismatch:

<script setup>
const inputEl = ref(null)
</script>

<template>
  <input ref="input" />    <!-- ❌ "input" ≠ "inputEl" -->
</template>

Goed — exact dezelfde naam:

<input ref="inputEl" />   <!-- ✅ -->

Fout — : (v-bind) voor het ref-attribute:

<input :ref="inputEl" />
<!-- ❌ verwacht een dynamische waarde, niet een naam -->

Goed — gewoon attribute zonder ::

<input ref="inputEl" />
<!-- ✅ "inputEl" als string, Vue koppelt aan de variabele -->