Vue.js course Β· Module 2: Template Syntax
Template Refs
In this lesson5
Sometimes you need direct access to a DOM element - like an engineer who needs to check a physical sensor.
Templates describe how the panel should look, and Vue takes care of the page elements itself. Some tasks, however, cannot be written declaratively: placing the cursor in the command field right after the panel opens, measuring a container, or handing a <canvas> element to an external charting library. Then you need a handle to the real element - and that is what template refs give you. In the second part of the lesson you will also meet a few special directives for unusual jobs.
Basic Template Refs
The special ref attribute marks an element in the template. In the script you declare a ref with the same name and the value null, and once the component is mounted, Vue puts the element into it. The moment of mounting is caught by the onMounted function - a lifecycle hook that Vue calls when the component is already on the page; we will cover the lifecycle in module 6.
1<template>
2 <div class="input-panel" style="background: #0a0e27; padding: 1rem;">
3 <input ref="inputRef" type="text" placeholder="Module code"
4 style="background: #0a0e27; border: 1px solid #00b4d8; color: #00ff88; padding: 0.5rem;">
5 <button @click="focusInput" class="btn-focus">Set focus</button>
6 </div>
7</template>
8
9<script setup>
10import { ref, onMounted } from 'vue'
11
12// Name must match the ref attribute in template
13const inputRef = ref(null)
14
15function focusInput() {
16 inputRef.value?.focus()
17}
18
19onMounted(() => {
20 // Element is available after mounting
21 inputRef.value?.focus()
22})
23</script>The order is always the same: declare ref(null), put the ref="inputRef" attribute on the element, mount, and only then use it. Before mounting, and also after the element is removed by v-if, the ref holds null, which is why ?. protects you from an error. It is the same ref() function you use for data - here the container simply holds an element instead of a number.
Since Vue 3.5 the recommended way is the useTemplateRef helper. Its argument must match the value of the ref attribute, and the variable can have any name:
1<script setup>
2import { useTemplateRef, onMounted } from 'vue'
3
4const commandInput = useTemplateRef('command-input')
5
6onMounted(() => {
7 commandInput.value?.focus()
8})
9</script>
10
11<template>
12 <input ref="command-input" placeholder="Module code">
13</template>It works identically, and in TypeScript the element type is inferred automatically. The older form still works, and you will meet it in existing code and in the exercises, but in new projects I recommend useTemplateRef.
Refs in v-for
When ref sits on an element with v-for, after mounting you get an array of elements rather than a single element:
1<template>
2 <div class="modules-list">
3 <ul style="list-style: none; background: #0a0e27;">
4 <li v-for="module in modules" :key="module.id" ref="itemRefs"
5 style="color: #00b4d8; padding: 0.5rem; border-left: 2px solid #00ff88;">
6 {{ module.name }}
7 </li>
8 </ul>
9 </div>
10</template>
11
12<script setup>
13import { ref, onMounted } from 'vue'
14
15const modules = ref([
16 { id: 1, name: 'Oxygen Generator' },
17 { id: 2, name: 'Water Recycler' }
18])
19
20const itemRefs = ref([])
21
22onMounted(() => {
23 console.log(itemRefs.value) // Array of LI elements
24})
25</script>The console shows two li elements. The docs warn, however, that the order of this array does not have to match the order of the data, so do not assume that the element at index 0 is the first module.
Function Refs
Instead of a name you can pass a function by using the dynamic :ref binding. Vue calls it with the element on every component update, and with null when the element is unmounted:
1<template>
2 <input :ref="setInputRef" type="text"
3 style="background: #0a0e27; border: 1px solid #00ff88; color: #00b4d8;">
4</template>
5
6<script setup>
7import { ref } from 'vue'
8
9const dynamicRef = ref(null)
10
11function setInputRef(el) {
12 dynamicRef.value = el
13 console.log('Element set:', el)
14}
15</script>You decide where to store the element, for example in a map keyed by an identifier. Just remember the null case.
Refs on Components
A ref attribute on a component gives you access to its instance. SensorPanel.vue is a separate component file, imported like any other module - we will deal with components in module 6. The parent calls the child's method after mounting:
1<template>
2 <SensorPanel ref="sensorRef" />
3</template>
4
5<script setup>
6import { ref, onMounted } from 'vue'
7import SensorPanel from './SensorPanel.vue'
8
9const sensorRef = ref(null)
10
11onMounted(() => {
12 // Access component methods and properties
13 sensorRef.value?.refreshData()
14})
15</script>Components using <script setup> are private by default: the parent sees nothing until the child exposes it with the defineExpose macro, which does not need to be imported:
1<!-- SensorPanel.vue - must expose methods -->
2<script setup>
3import { ref } from 'vue'
4
5const sensorData = ref({})
6
7function refreshData() {
8 console.log('Refreshing sensor data!')
9}
10
11// defineExpose - make available to the parent
12defineExpose({
13 refreshData,
14 sensorData
15})
16</script>defineExpose has to be called before the first await. This kind of connection couples components tightly, so use it sparingly - props and events are usually the better choice.
Special Directives
A few built-in directives solve rarer tasks, but they are worth knowing. v-text sets the element's textContent, so it replaces the element's entire content, while mustaches replace only their own fragment:
1<template>
2 <p v-text="statusMessage"></p>
3 <p>Status: {{ statusMessage }}</p>
4</template>Both paragraphs show the message, but only the second one keeps the Status label. v-once renders an element once and then treats it as static. v-memo (Vue 3.2+) memoizes a fragment of the template and updates it only when one of the values in its array changes:
1<template>
2 <p v-once>Mission code: {{ missionCode }}</p>
3
4 <div v-for="module in modules" :key="module.id" v-memo="[module.id === selectedId]">
5 {{ module.name }} - selected: {{ module.id === selectedId }}
6 </div>
7</template>A later change of missionCode will not refresh the paragraph. After selectedId changes, only the two modules whose flag changed are re-rendered. v-memo="[]" works like v-once, and badly chosen dependencies leave stale data on screen, which is why it is a tool for very long lists. The last two directives concern compilation:
1<template>
2 <code v-pre>{{ this will not be compiled }}</code>
3</template>
4
5<style>
6[v-cloak] {
7 display: none;
8}
9</style>v-pre skips compilation of the element and its children, so you see the mustaches literally. v-cloak is needed only without a build step, when Vue compiles a template written directly in HTML: together with this CSS rule it hides the raw {{ }} until the app is mounted. In a Vite project you do not need it.
Treat template refs as a last resort - first check whether the template is enough. In the module project you will combine all these tools into one hologram panel.
Remember: a template ref is a service key to a single device - you reach for it only after mounting, and only when the control panel is not enough.
Code for this lesson: App.vue
1<script setup>
2import { ref, onMounted } from 'vue'
3
4// Template refs
5const inputRef = ref(null)
6const containerRef = ref(null)
7const canvasRef = ref(null)
8
9// Data
10const focusCount = ref(0)
11const containerInfo = ref({})
12const canvasDrawn = ref(false)
13
14// Reactive data for v-once demonstration
15const staticData = ref({
16 launchDate: '2087-03-15',
17 missionCode: 'NOVA-MARS-001',
18 initialCrew: 6
19})
20
21const dynamicCounter = ref(0)
22
23// Functions
24function focusInput() {
25 if (inputRef.value) {
26 inputRef.value.focus()
27 focusCount.value++
28 }
29}
30
31function getContainerDimensions() {
32 if (containerRef.value) {
33 const rect = containerRef.value.getBoundingClientRect()
34 containerInfo.value = {
35 width: Math.round(rect.width),
36 height: Math.round(rect.height),
37 top: Math.round(rect.top),
38 left: Math.round(rect.left)
39 }
40 }
41}
42
43function drawOnCanvas() {
44 if (canvasRef.value) {
45 const ctx = canvasRef.value.getContext('2d')
46 const canvas = canvasRef.value
47
48 // Clear canvas
49 ctx.fillStyle = '#0a0e27'
50 ctx.fillRect(0, 0, canvas.width, canvas.height)
51
52 // Draw Mars
53 ctx.beginPath()
54 ctx.arc(200, 150, 80, 0, Math.PI * 2)
55 ctx.fillStyle = '#ff6600'
56 ctx.fill()
57
58 // Draw craters
59 ctx.beginPath()
60 ctx.arc(180, 130, 20, 0, Math.PI * 2)
61 ctx.fillStyle = '#cc5500'
62 ctx.fill()
63
64 ctx.beginPath()
65 ctx.arc(220, 160, 15, 0, Math.PI * 2)
66 ctx.fill()
67
68 // Draw NOVA LAB base
69 ctx.fillStyle = '#00ff88'
70 ctx.fillRect(150, 220, 100, 40)
71
72 // Draw dome
73 ctx.beginPath()
74 ctx.arc(200, 220, 50, Math.PI, 0)
75 ctx.strokeStyle = '#00b4d8'
76 ctx.lineWidth = 3
77 ctx.stroke()
78
79 // Add text
80 ctx.fillStyle = '#00ff88'
81 ctx.font = 'bold 16px Courier'
82 ctx.fillText('NOVA LAB', 160, 245)
83
84 canvasDrawn.value = true
85 }
86}
87
88function incrementCounter() {
89 dynamicCounter.value++
90}
91
92onMounted(() => {
93 getContainerDimensions()
94
95 // Auto-focus on mount
96 setTimeout(() => {
97 focusInput()
98 }, 500)
99})
100</script>
101
102<template>
103 <div class="advanced-lab">
104 <h1>Advanced Directives - NOVA LAB</h1>
105
106 <!-- Template Refs Section -->
107 <section class="lab-section">
108 <h2>Template Refs - Accessing DOM Elements</h2>
109
110 <!-- Input ref example -->
111 <div class="demo-card">
112 <h3>ref on an input - Programmatic focus</h3>
113 <div class="ref-demo">
114 <input
115 ref="inputRef"
116 type="text"
117 placeholder="This input can be focused programmatically..."
118 class="input-field"
119 >
120 <button @click="focusInput" class="btn btn-primary">
121 Focus Input
122 </button>
123 </div>
124 <p class="info-text">Number of focus calls: {{ focusCount }}</p>
125 <code class="code-hint">ref="inputRef" β inputRef.value.focus()</code>
126 </div>
127
128 <!-- Container ref example -->
129 <div class="demo-card">
130 <h3>ref on container - Reading dimensions</h3>
131 <div ref="containerRef" class="ref-container">
132 <p>This container has ref="containerRef"</p>
133 <p>Click button to read its dimensions</p>
134 </div>
135 <button @click="getContainerDimensions" class="btn btn-success">
136 Get Dimensions
137 </button>
138 <div v-if="containerInfo.width" class="dimensions-info">
139 <p>Width: {{ containerInfo.width }}px</p>
140 <p>Height: {{ containerInfo.height }}px</p>
141 <p>Position: ({{ containerInfo.left }}px, {{ containerInfo.top }}px)</p>
142 </div>
143 </div>
144
145 <!-- Canvas ref example -->
146 <div class="demo-card">
147 <h3>ref on a canvas - Drawing</h3>
148 <canvas
149 ref="canvasRef"
150 width="400"
151 height="300"
152 class="canvas-field"
153 ></canvas>
154 <button @click="drawOnCanvas" class="btn btn-warning">
155 {{ canvasDrawn ? 'Redraw' : 'Draw' }} Mars Base
156 </button>
157 <code class="code-hint">ref="canvasRef" β canvasRef.value.getContext('2d')</code>
158 </div>
159 </section>
160
161 <!-- v-once Section -->
162 <section class="lab-section">
163 <h2>v-once - One-time rendering</h2>
164
165 <div class="demo-card">
166 <h3>Static data (v-once)</h3>
167 <div v-once class="static-content">
168 <p><strong>Launch Date:</strong> {{ staticData.launchDate }}</p>
169 <p><strong>Mission Code:</strong> {{ staticData.missionCode }}</p>
170 <p><strong>Initial Crew:</strong> {{ staticData.initialCrew }}</p>
171 <p class="info-text">This data is rendered only once and will not update</p>
172 </div>
173 </div>
174
175 <div class="demo-card">
176 <h3>Comparison: v-once vs normal binding</h3>
177 <div class="comparison-grid">
178 <div class="comparison-item">
179 <h4>With v-once (does not update)</h4>
180 <div v-once class="counter-display static">
181 {{ dynamicCounter }}
182 </div>
183 </div>
184 <div class="comparison-item">
185 <h4>Without v-once (updates)</h4>
186 <div class="counter-display dynamic">
187 {{ dynamicCounter }}
188 </div>
189 </div>
190 </div>
191 <button @click="incrementCounter" class="btn btn-primary">
192 Increment Counter ({{ dynamicCounter }})
193 </button>
194 <p class="info-text">
195 Left counter stays at the initial value, right one updates
196 </p>
197 </div>
198 </section>
199
200 <!-- v-cloak Section -->
201 <section class="lab-section">
202 <h2>v-cloak - Hiding unrendered templates</h2>
203
204 <div class="demo-card">
205 <h3>v-cloak - Prevents showing <code v-pre>{{ }}</code></h3>
206 <div v-cloak class="cloak-demo">
207 <p>This section uses v-cloak</p>
208 <p>With slow loading, the user won't see raw mustaches <code v-pre>{{ }}</code></p>
209 <p>Data: {{ staticData.missionCode }}</p>
210 </div>
211 <code class="code-hint">
212 v-cloak + CSS: [v-cloak] { display: none; }
213 </code>
214 </div>
215 </section>
216
217 <!-- Info Box -->
218 <div class="info-box">
219 <h3>Advanced Vue directives</h3>
220 <div class="info-grid">
221 <div>
222 <h4>Template Refs:</h4>
223 <ul>
224 <li><strong>ref="name"</strong> - Creates a reference to an element</li>
225 <li><strong>const name = ref(null)</strong> - Declaration in setup</li>
226 <li><strong>name.value</strong> - Access to DOM element</li>
227 <li>Available after onMounted()</li>
228 </ul>
229 </div>
230 <div>
231 <h4>v-once:</h4>
232 <ul>
233 <li>Renders the element/component only once</li>
234 <li>Skips all subsequent updates</li>
235 <li>Optimization for static content</li>
236 <li>Useful for large data that does not change</li>
237 </ul>
238 </div>
239 <div>
240 <h4>v-cloak:</h4>
241 <ul>
242 <li>Hides unrendered templates</li>
243 <li>Requires CSS: [v-cloak] { display: none; }</li>
244 <li>Prevents "flashing" <code v-pre>{{ }}</code></li>
245 <li>Removed automatically after compilation</li>
246 </ul>
247 </div>
248 <div>
249 <h4>Uses of ref:</h4>
250 <ul>
251 <li>Programmatic focus on an input</li>
252 <li>Integration with DOM libraries</li>
253 <li>Canvas and SVG manipulation</li>
254 <li>Scrolling to an element</li>
255 <li>Reading element dimensions</li>
256 </ul>
257 </div>
258 </div>
259 </div>
260 </div>
261</template>
262
263<style scoped>
264/* v-cloak CSS */
265[v-cloak] {
266 display: none;
267}
268
269.advanced-lab {
270 background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
271 min-height: 100vh;
272 padding: 2rem;
273 color: #ffffff;
274 font-family: 'Courier New', monospace;
275}
276
277h1 {
278 text-align: center;
279 color: #00ff88;
280 text-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
281 margin-bottom: 3rem;
282}
283
284h2 {
285 color: #00b4d8;
286 margin-bottom: 1.5rem;
287 border-bottom: 2px solid #00b4d8;
288 padding-bottom: 0.5rem;
289}
290
291h3 {
292 color: #00ff88;
293 font-size: 1.1rem;
294 margin-bottom: 1rem;
295}
296
297h4 {
298 color: #00b4d8;
299 font-size: 1rem;
300 margin-bottom: 0.75rem;
301}
302
303.lab-section {
304 background: rgba(0, 255, 136, 0.05);
305 border: 2px solid #00ff88;
306 border-radius: 10px;
307 padding: 2rem;
308 margin-bottom: 2rem;
309}
310
311.demo-card {
312 background: rgba(0, 180, 216, 0.1);
313 border: 2px solid #00b4d8;
314 border-radius: 8px;
315 padding: 1.5rem;
316 margin-bottom: 1.5rem;
317}
318
319.ref-demo {
320 display: flex;
321 gap: 1rem;
322 margin-bottom: 1rem;
323 flex-wrap: wrap;
324}
325
326.input-field {
327 background: rgba(0, 0, 0, 0.3);
328 border: 2px solid #00b4d8;
329 border-radius: 5px;
330 padding: 0.75rem;
331 color: #ffffff;
332 font-family: 'Courier New', monospace;
333 font-size: 1rem;
334 flex: 1;
335 min-width: 250px;
336}
337
338.input-field:focus {
339 outline: none;
340 border-color: #00ff88;
341 box-shadow: 0 0 15px rgba(0, 255, 136, 0.5);
342 animation: focusPulse 0.5s ease;
343}
344
345@keyframes focusPulse {
346 0%, 100% { transform: scale(1); }
347 50% { transform: scale(1.02); }
348}
349
350.ref-container {
351 background: rgba(0, 255, 136, 0.1);
352 border: 2px dashed #00ff88;
353 border-radius: 8px;
354 padding: 2rem;
355 text-align: center;
356 margin: 1rem 0;
357}
358
359.dimensions-info {
360 background: rgba(0, 0, 0, 0.3);
361 border-left: 4px solid #00ff88;
362 border-radius: 5px;
363 padding: 1rem;
364 margin-top: 1rem;
365}
366
367.dimensions-info p {
368 margin: 0.5rem 0;
369 color: #00ff88;
370}
371
372.canvas-field {
373 display: block;
374 width: 100%;
375 max-width: 400px;
376 background: #0a0e27;
377 border: 2px solid #00b4d8;
378 border-radius: 8px;
379 margin: 1rem 0;
380}
381
382.static-content {
383 background: rgba(0, 255, 136, 0.1);
384 border: 2px solid #00ff88;
385 border-radius: 8px;
386 padding: 1.5rem;
387}
388
389.static-content p {
390 margin: 0.75rem 0;
391 color: #ffffff;
392}
393
394.static-content strong {
395 color: #00b4d8;
396}
397
398.comparison-grid {
399 display: grid;
400 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
401 gap: 1.5rem;
402 margin: 1.5rem 0;
403}
404
405.comparison-item {
406 background: rgba(0, 0, 0, 0.3);
407 border-radius: 8px;
408 padding: 1.5rem;
409 text-align: center;
410}
411
412.counter-display {
413 font-size: 3rem;
414 font-weight: bold;
415 padding: 2rem;
416 border-radius: 8px;
417 margin: 1rem 0;
418}
419
420.counter-display.static {
421 background: rgba(255, 180, 0, 0.2);
422 border: 2px solid #ffb400;
423 color: #ffb400;
424}
425
426.counter-display.dynamic {
427 background: rgba(0, 255, 136, 0.2);
428 border: 2px solid #00ff88;
429 color: #00ff88;
430 animation: pulse 2s infinite;
431}
432
433.cloak-demo {
434 background: rgba(0, 180, 216, 0.1);
435 border: 2px solid #00b4d8;
436 border-radius: 8px;
437 padding: 1.5rem;
438}
439
440.btn {
441 padding: 0.75rem 1.5rem;
442 border: none;
443 border-radius: 5px;
444 font-size: 1rem;
445 font-weight: bold;
446 cursor: pointer;
447 transition: all 0.3s ease;
448 font-family: 'Courier New', monospace;
449 margin: 0.5rem;
450}
451
452.btn-primary {
453 background: #00ff88;
454 color: #0a0e27;
455}
456
457.btn-success {
458 background: #00b4d8;
459 color: #ffffff;
460}
461
462.btn-warning {
463 background: #ffb400;
464 color: #0a0e27;
465}
466
467.btn:hover {
468 transform: scale(1.05);
469 box-shadow: 0 5px 15px rgba(0, 255, 136, 0.4);
470}
471
472.code-hint {
473 display: block;
474 background: rgba(0, 0, 0, 0.5);
475 border-left: 4px solid #00ff88;
476 padding: 0.75rem;
477 border-radius: 5px;
478 color: #00ff88;
479 font-family: 'Courier New', monospace;
480 font-size: 0.9rem;
481 margin-top: 1rem;
482}
483
484.info-text {
485 color: #00b4d8;
486 font-size: 0.9rem;
487 margin-top: 1rem;
488}
489
490.info-box {
491 background: rgba(0, 255, 136, 0.05);
492 border: 2px solid #00ff88;
493 border-radius: 10px;
494 padding: 2rem;
495}
496
497.info-box h3 {
498 margin-top: 0;
499 margin-bottom: 1.5rem;
500}
501
502.info-grid {
503 display: grid;
504 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
505 gap: 2rem;
506}
507
508.info-box ul {
509 list-style: none;
510 padding: 0;
511}
512
513.info-box li {
514 padding: 0.5rem 0;
515 color: #ffffff;
516 border-left: 3px solid #00b4d8;
517 padding-left: 1rem;
518 margin: 0.5rem 0;
519}
520
521.info-box strong {
522 color: #00ff88;
523}
524
525@keyframes pulse {
526 0%, 100% { opacity: 1; }
527 50% { opacity: 0.7; }
528}
529</style>Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Which Vue mechanism gives direct access to a DOM element from the component's template?
2. What is the difference between {{ }} and v-text?
3. What does v-pre do?
4. What does the v-once directive do in Vue.js?
5. missionCode changes from 'A-1' to 'B-2'. What does <p v-once>Code: {{ missionCode }}</p> show then?
6. Why would you use v-cloak?
7. Why should you be careful when using v-html?
8. Which Vue directive is used for performance optimization through memoization?
9. Which key modifier in Vue.js is used to detect the Escape key?
10. What should you do when a template becomes too complex?
11. What does v-memo do in Vue 3?
12. Which binding calls onSelf only when the element itself is clicked, not its child?
13. Which practice is recommended in Vue templates?
Hands-on tasks in the game
- Code editor
Use a template ref to automatically focus an input after the component mounts.
- Vertical ordering
Order the steps of using a template ref:
- Vertical ordering
Arrange the template ref code pieces in the order they take effect:
- Click in order
Arrange the template ref code with auto-focus:
- Vertical ordering
Arrange the style binding methods from simple to advanced:
- Vertical ordering
Order the steps of v-if in Vue:
- Horizontal ordering
Arrange the correct v-for syntax with index access: