When working with Vue we sometimes need access to DOM elements or child components to manipulate them manually instead of relying on data binding. Luckily, we can use Template Refs to do just that.
Some use cases for template refs would be to:
- Automatically focus an input when the page loads.
- Manipulate the value of the input programmatically.
- Mount a JS library that requires a DOM element, like ChartJS.
In this tutorial, let’s explore the simple use case of auto-focusing an input. Afterward, we will develop unit tests for it and tackle the edge cases that arise from using refs using Vitest.
Accessing DOM elements
First, create a Search.vue
component:
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const model = defineModel<string>()
</script>
<template>
<div>
<input
v-model="model"
/>
</div>
</template>
The component has a single element, an input, and a model that will expose the search query to the parent.
Next, add the template ref:
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const input = ref<HTMLInputElement | null>(null)
const model = defineModel<string>()
</script>
<template>
<div>
<input
ref="input"
v-model="model"
/>
</div>
</template>
Finally, to focus on the input, access the refs value and call the focus
method:
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const input = ref<HTMLInputElement | null>(null)
const model = defineModel<string>()
onMounted(() => {
input.value?.focus()
})
</script>
<template>
<div>
<input
ref="input"
v-model="model"
/>
</div>
</template>
Very simple, right? Remember that the input ref will only be defined once the component is mounted.
Testing Template Refs
Now that we have a working example, let’s explore testing it. We will test that when the component is mounted, it automatically focuses on the input.
Finding out if the input ref is focused or not can be a bit tricky. A prerequisite is to have an environment specified. This can be jsdom
or happy-dom
.
Adjust the vite config to use either:
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
},
})
This way, vue test utils can mount the component onto a fake document that we can reference in our tests. You will see how this is useful in the next bit.
Next, write the unit test:
import { describe, expect, test } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import Input from './Input.vue'
describe('Search', () => {
describe('when mounted', () => {
test('focuses on the input', () => {
const wrapper = shallowMount(Input, {
attachTo: document.body,
})
const input = wrapper.find<HTMLInputElement>({
ref: 'input',
})
expect(document.activeElement).toBe(input.element)
})
})
})
And when running the test, it should work:
There are a few things happening here. First, we use attachTo
so that the component is rendered and added to our fake document (remember why we added jsdom
earlier?)
Second, we use document.activeElement
which holds a reference to the focused element and checks if it’s equal to the input element we have in our component. that way we can assert if focusing
is working as expected.
As a side note, using .toBe
to compare HTML elements is safe, but you can also do:
expect(document.activeElement?.isSameNode(input.element)).toBe(true)