Skip to main content

Vue.js Template Refs

Introduction

In Vue.js applications, you'll often need to interact directly with DOM elements or child component instances. While Vue's declarative rendering model reduces the need for direct DOM manipulation, there are legitimate cases where you need this level of access - like focusing an input, controlling a video player, initializing a third-party library, or measuring element dimensions.

Template refs provide a way to reference an element or a component instance in your Vue.js template, making it accessible in your JavaScript code. In the Composition API, template refs have a specific approach that differs from the Options API counterpart.

What are Template Refs?

Template refs are special attributes that allow you to obtain direct references to specific DOM elements or child component instances in your Vue.js application. They serve as a "bridge" between the declarative Vue template and imperative DOM operations.

Template Refs in Composition API

In the Composition API, template refs are created using the ref() function and then bound to elements using the ref attribute in your template.

Basic Syntax

Here's the basic pattern for using template refs in the Composition API:

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

// Declare a ref to hold the element reference
const inputElement = ref(null)

// Now you can access the element in lifecycle hooks or methods
onMounted(() => {
// Access the actual DOM element with .value
inputElement.value.focus()
})
</script>

<template>
<!-- Bind the ref to an element using the same name -->
<input ref="inputElement" />
</template>

When the component is mounted, Vue will automatically set the inputElement.value to the corresponding DOM element, allowing you to interact with it directly.

Common Use Cases for Template Refs

Let's explore some practical examples of how template refs can be used in Vue applications.

1. Focusing an Input Element

One of the most common use cases is automatically focusing an input when a component mounts.

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

const searchInput = ref(null)

onMounted(() => {
// Focus the input element when component mounts
searchInput.value.focus()
})
</script>

<template>
<div>
<label for="search">Search:</label>
<input
id="search"
ref="searchInput"
type="text"
placeholder="Search..."
/>
</div>
</template>

2. Accessing a Child Component's Methods or Properties

You can also use refs to access a child component's public properties and methods:

html
<script setup>
import { ref } from 'vue'
import VideoPlayer from './VideoPlayer.vue'

const player = ref(null)

const playVideo = () => {
// Access the child component's public method
player.value.play()
}

const pauseVideo = () => {
player.value.pause()
}
</script>

<template>
<div>
<VideoPlayer ref="player" src="/path/to/video.mp4" />
<div class="controls">
<button @click="playVideo">Play</button>
<button @click="pauseVideo">Pause</button>
</div>
</div>
</template>

3. Initializing a Third-Party Library

Template refs are useful when you need to initialize a third-party library that needs to operate on a DOM element:

html
<script setup>
import { ref, onMounted } from 'vue'
import * as Chart from 'chart.js'

const chartCanvas = ref(null)
let chartInstance = null

onMounted(() => {
// Initialize Chart.js with the canvas element
chartInstance = new Chart.Chart(chartCanvas.value.getContext('2d'), {
type: 'bar',
data: {
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
],
borderWidth: 1
}]
}
})
})
</script>

<template>
<canvas ref="chartCanvas" width="400" height="200"></canvas>
</template>

4. Measuring Element Dimensions

You can use template refs to get the dimensions of an element:

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

const boxElement = ref(null)
const dimensions = reactive({
width: 0,
height: 0
})

onMounted(() => {
// Get the element's dimensions
dimensions.width = boxElement.value.offsetWidth
dimensions.height = boxElement.value.offsetHeight
})
</script>

<template>
<div>
<div ref="boxElement" class="box">
This is a box element
</div>
<p>Box dimensions: {{ dimensions.width }}px × {{ dimensions.height }}px</p>
</div>
</template>

<style scoped>
.box {
width: 200px;
height: 100px;
background-color: #f0f0f0;
border: 1px solid #ccc;
padding: 10px;
}
</style>

Template Refs on v-for Elements

When using template refs with v-for, the ref will contain an array of elements instead of a single element:

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

const itemRefs = ref([])

onMounted(() => {
console.log(itemRefs.value) // An array of DOM elements

// You can iterate through the elements
itemRefs.value.forEach((item, index) => {
console.log(`Item ${index} height:`, item.offsetHeight)
})
})
</script>

<template>
<ul>
<li v-for="(item, index) in ['Apple', 'Banana', 'Cherry']" :key="index" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>

Template Refs with Function Values

You can also pass a function to the ref attribute for more control:

html
<script setup>
import { onMounted } from 'vue'

const itemRefs = {}

onMounted(() => {
console.log(itemRefs) // An object with keys corresponding to item ids
})

const setItemRef = (el, item) => {
if (el) {
// Store elements in an object using item.id as the key
itemRefs[item.id] = el
}
}
</script>

<template>
<ul>
<li
v-for="item in [
{ id: 'a', text: 'Apple' },
{ id: 'b', text: 'Banana' },
{ id: 'c', text: 'Cherry' }
]"
:key="item.id"
:ref="el => setItemRef(el, item)"
>
{{ item.text }}
</li>
</ul>
</template>

Best Practices and Considerations

When working with template refs, keep these guidelines in mind:

  1. Timing matters: Refs are assigned after the DOM is updated, so always access them in onMounted or onUpdated lifecycle hooks, or in methods that are called after mounting.

  2. Use with discretion: While refs provide direct access to DOM elements, Vue's reactivity system often eliminates the need for direct DOM manipulation. Only use refs when necessary.

  3. Not reactive by default: Changes to the DOM elements themselves won't trigger reactivity. If you need to react to element changes, you'll need to use other techniques like MutationObserver or combine with reactive state.

  4. TypeScript support: When using TypeScript, you can define the type of your refs for better type checking:

html
<script setup lang="ts">
import { ref, onMounted } from 'vue'

const inputElement = ref<HTMLInputElement | null>(null)

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

Real-world Example: Custom Modal Component

Here's a more complete example of a custom modal component that uses template refs to manage focus and keyboard navigation:

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

const props = defineProps({
isOpen: Boolean,
title: String
})

const emit = defineEmits(['close'])

const modalRef = ref(null)
const closeButtonRef = ref(null)

// Close modal when Escape key is pressed
const handleKeyDown = (event) => {
if (event.key === 'Escape' && props.isOpen) {
emit('close')
}
}

// Focus trap within the modal
const createFocusTrap = () => {
const modal = modalRef.value
if (!modal) return

// Get all focusable elements in the modal
const focusables = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)

if (focusables.length === 0) return

const firstElement = focusables[0]
const lastElement = focusables[focusables.length - 1]

// Focus the first element when modal opens
firstElement.focus()

modal.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return

// Shift+Tab on the first element should loop to the last element
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault()
lastElement.focus()
}
// Tab on last element should loop to the first element
else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault()
firstElement.focus()
}
})
}

watch(() => props.isOpen, (isOpen) => {
if (isOpen) {
// Add event listener when modal opens
document.addEventListener('keydown', handleKeyDown)

// Setup focus trap in the next tick when DOM is updated
setTimeout(createFocusTrap, 0)
} else {
// Remove event listener when modal closes
document.removeEventListener('keydown', handleKeyDown)
}
})

onBeforeUnmount(() => {
// Clean up event listeners when component is destroyed
document.removeEventListener('keydown', handleKeyDown)
})
</script>

<template>
<div v-if="isOpen" class="modal-backdrop" @click="emit('close')">
<div
ref="modalRef"
class="modal"
role="dialog"
aria-modal="true"
:aria-labelledby="`modal-title-${title}`"
@click.stop
>
<div class="modal-header">
<h2 :id="`modal-title-${title}`">{{ title }}</h2>
<button
ref="closeButtonRef"
type="button"
class="close-button"
@click="emit('close')"
aria-label="Close"
>
×
</button>
</div>
<div class="modal-content">
<slot></slot>
</div>
<div class="modal-footer">
<slot name="footer">
<button @click="emit('close')">Close</button>
</slot>
</div>
</div>
</div>
</template>

<style scoped>
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}

.modal {
background: white;
border-radius: 8px;
width: 500px;
max-width: 90%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}

.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #eee;
}

.modal-content {
padding: 16px;
}

.modal-footer {
padding: 16px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
}

.close-button {
background: transparent;
border: none;
font-size: 24px;
cursor: pointer;
}
</style>

Usage of this modal component:

html
<script setup>
import { ref } from 'vue'
import Modal from './Modal.vue'

const isModalOpen = ref(false)

const openModal = () => {
isModalOpen.value = true
}

const closeModal = () => {
isModalOpen.value = false
}
</script>

<template>
<div>
<button @click="openModal">Open Modal</button>

<Modal :isOpen="isModalOpen" title="User Settings" @close="closeModal">
<p>This is the modal content!</p>
<input type="text" placeholder="Enter some text..." />
<select>
<option>Option 1</option>
<option>Option 2</option>
</select>

<template #footer>
<button @click="closeModal">Cancel</button>
<button @click="closeModal">Save</button>
</template>
</Modal>
</div>
</template>

Summary

Template refs in Vue.js Composition API provide a powerful way to directly access DOM elements and component instances when needed. They are particularly useful for specific scenarios such as focusing elements, integrating with third-party libraries, or measuring element dimensions.

Remember these key points about template refs:

  1. Create a ref with ref(null) in your script section
  2. Bind it to elements/components with the ref attribute in your template
  3. Access the actual DOM element or component instance via .value
  4. Access refs after mounting to ensure they're populated
  5. Use them sparingly, only when direct element access is necessary

By following the examples and best practices outlined above, you'll be able to effectively use template refs in your Vue.js applications while maintaining the benefits of Vue's declarative programming model.

Exercises

To solidify your understanding of template refs, try these exercises:

  1. Create a component with multiple form inputs and a button that, when clicked, focuses the first empty input field.

  2. Build a carousel component that uses template refs to access and control the slide elements.

  3. Implement a text editor that uses a third-party library like CodeMirror or TinyMCE, initializing it on a textarea element using template refs.

  4. Create a component that measures and displays the dimensions of an uploaded image using template refs.

  5. Build a form validation component that highlights invalid fields and uses template refs to scroll to and focus the first invalid input.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)