In the previous lesson you took a
.vue module apart into three sections and saw how template, logic and styling all fit inside one file. The life-support panel you built there had a serious flaw, though: it was dead. It showed a single reading and nothing more - no button, no reaction, no change over time.In NOVA LAB Mission Control a panel like that is fit for a door sign at best. The operator on duty has to be able to press a button, move the mission to the next phase, watch the distance to Mars shrink, and in that same second catch the moment when a green label turns into a yellow alert. Today, @name, you are going to build your first panel that can do all of that - the MARS ONE mission status panel.
Along the way you will meet four tools, and they are the real subject of this lesson: the
ref() function, interpolation with {{ }}, event handling through @click, and dynamic classes with :class. You will meet each of them first as a gap that hurts, and only then as the fix.Let us start with the simplest possible version of the panel - the kind you could write in plain HTML, with no framework at all. The mission name and the number of days left to target are typed straight into the markup.
1<template>
2 <h1>MARS ONE - First Colony</h1>
3 <p>Days to target: 180</p>
4</template>This code is correct and it will display without a complaint. The trouble starts the next morning: telemetry reports 179 days, and the only way to update the panel is to open the file, fix the number by hand and upload the system again. The value is glued to the markup. Vue has nothing to do here, because it has no idea that the number 180 means anything more than a piece of text.
For anything to be able to change on its own, the value has to move out of the template into the
<script setup> section first, and be given a keeper that notices every change made to it.Vue gives you the
ref function for exactly that. You bring it in with an import statement from the vue package and call it, passing the starting value. In return you get a container - a single box with your value sitting inside it, and Vue watching that box. The name comes from the word reference: this is not the number itself, it is a pointer to the place where that number lives.That one property - being watched - is the whole difference. When you swap the contents of the container, Vue knows about it, finds every place in the template that uses it, and refreshes exactly those. Not the whole page, not the whole panel, just those fragments. That is what we call reactivity, and it is the heart of Vue.
1<script setup>
2import { ref } from 'vue'
3
4const missionName = ref('MARS ONE - First Colony')
5const daysRemaining = ref(180)
6</script>The values are identical to the ones a moment ago - that has not changed. What changed is where they live, and the fact that Vue now knows about them. Look closely at the shape of such a line, because you will write it hundreds of times: first the word
const, then the variable name, then the = sign, and at the very end the ref() call with the starting value in the parentheses. Four parts, always in that order - const counter = ref(0) looks exactly the same.Using
const for a value that is meant to change looks like a mistake, but it is not one. const locks the container itself against being swapped, and that is a good thing - if you pointed that name at something completely different, Vue would lose its connection to the template. The contents of the container may be changed as often as you like, and in a moment you will be doing exactly that.One housekeeping note right away, because
ref() gets confused with entirely different tools. This function serves one purpose only: creating reactive data. It has nothing to do with navigating between station screens - that is the job of Vue Router from the ecosystem table you saw in the first lesson. It does not style components - the appearance of a module is handled by the <style scoped> section from the previous lesson. And it is not for importing files either - files and packages are brought in with an import statement, exactly the way you have just brought in ref itself from the vue package.The data sits in the script now, but the template still cannot see it. The bridge between one section and the other is interpolation: the double curly braces, known in the Vue world as mustaches. You put a variable name inside them, Vue drops the current value in at that spot - and makes sure to drop it in again after every change.
1<template>
2 <h1>{{ missionName }}</h1>
3 <p>Days to target: {{ daysRemaining }}</p>
4</template>The same mission name, the same number on screen - except that Vue is in charge of them now. Notice what you did not have to change: the
<h1> and <p> tags stayed plain HTML, the words "Days to target" are still plain text, and the mustaches took exactly the spot where the hand-typed number used to sit. Vue does not take HTML away from you, it merely adds places into which it can drop a value.Inside the mustaches you may put more than a variable name - any JavaScript expression that returns something: arithmetic, a method call, a condition. What will not fit there are control statements such as
if, or loops - this is not a place for logic, only for its result.Remember one limitation right away, because we will come back to it when we style the panel: mustaches work in the text content of an element, not in the value of an attribute. Writing
class="{{ statusClass }}" substitutes nothing, because attributes are reached in Vue by a completely different road.If
ref() returns a container rather than the value itself, how do you get inside? Through the .value property. And here comes the one Vue rule that beginners forget most often.In the
<script setup> section you work on the container, so you always write .value. In the template Vue unwraps the container for you, so you never write .value. That is why {{ daysRemaining }} was enough in the block above - {{ daysRemaining.value }} would be a mistake in a template.1<script setup>
2import { ref } from 'vue'
3
4const daysRemaining = ref(180)
5
6// the whole container, not the number
7console.log(daysRemaining)
8
9// the value from inside the container
10console.log(daysRemaining.value)
11
12// swapping the value inside the container
13daysRemaining.value = 179
14</script>The first
console.log prints the wrapping object; its exact shape depends on the Vue version, so do not get attached to what you see on the console - what matters is that it is not a number. The second one prints 180, the contents of the container. The last line is the interesting one: it swaps the value inside the container, not the container itself. The daysRemaining variable still points at the same object, the link to the template holds unchanged, and yet the number on screen will update by itself.The panel shows data already, but the operator has nothing to click. You need a button that runs your code when it is pressed - the browser calls such a press a
click event.Vue hooks into events with the
v-on directive. You write v-on:click, and after the equals sign you give the name of the function Vue should call. This is so common that it got a shorthand in the shape of an at sign: @click means exactly the same as v-on:click. At NOVA LAB we use the short form only, and that is the one I recommend - it is shorter, and in a dense template you can see at a glance where something happens.You give the function name without parentheses. Writing
@click="advanceDay" means: here is a function, call it when someone clicks. The function itself is ordinary JavaScript, written in <script setup> next to your variables, and inside it the rule from the previous section applies, so you read and write through .value. We will also use the built-in Math.max function, which returns the largest of the values it is given - thanks to it the day counter stops at zero instead of dropping below it.1<template>
2 <p>Days to target: {{ daysRemaining }}</p>
3 <button @click="advanceDay">Next mission day</button>
4</template>
5
6<script setup>
7import { ref } from 'vue'
8
9const daysRemaining = ref(180)
10
11function advanceDay() {
12 daysRemaining.value = Math.max(0, daysRemaining.value - 1)
13}
14</script>Click the button and the counter drops by one, and it keeps dropping until it reaches zero, where it stops. The most important thing here, though, is what this code does not contain. There is not a single line that looks a paragraph up in the document and swaps the text inside it. There is no panel refresh. The
advanceDay function deals with data and nothing else; redrawing the screen is the job of Vue, which noticed the change in the container by itself. The template did not change either - {{ daysRemaining }} looks exactly the way it did when the number stood still.There is one road to a button in Vue, and it is worth knowing why the others are dead ends.
The plain HTML attribute
onclick="advanceDay()" looks familiar and it is technically valid HTML - except that the browser handles it, not Vue. The browser will look for an advanceDay function among the global functions, while your function lives inside the component and does not exist outside it at all. The result is that the click does nothing and an error about an unknown name lands in the console.The form
v-event:click="advanceDay" sounds sensible, but there simply is no v-event directive in Vue. The list of directives is closed, and events are handled in it by v-on alone.The form
bind:click="advanceDay" confuses two different tools. Vue does have v-bind, with a colon for a shorthand, but it binds attributes and properties, never events. Remember that split, because it comes back in every template: the at sign is for events, the colon is for attributes.Not every click deserves a function of its own. When the whole reaction fits into a single operation you can write it straight into the template, because
@click accepts not only a function name but also a JavaScript expression. The same principle as in the mustaches applies inside it, so .value disappears: you write the variable name as if it were a plain number.See it on the station's second panel - the power meter, where two buttons move the level by five points and a third one restores the starting state.
1<template>
2 <p>Energy: {{ energyLevel }}%</p>
3
4 <button @click="energyLevel -= 5">- 5</button>
5 <button @click="energyLevel += 5">+ 5</button>
6 <button @click="resetEnergy">Reset</button>
7</template>
8
9<script setup>
10import { ref } from 'vue'
11
12const energyLevel = ref(50)
13
14function resetEnergy() {
15 energyLevel.value = 50
16}
17</script>All three buttons work on the same
energyLevel container and all three refresh the same paragraph - that does not change with the notation. The only difference is where the instruction lives: in the template for the first two buttons, in a function for the third.When should you reach for which form? My recommendation, @name, is simple: an expression in the template only when it is one short operation you can read at a glance. Anything with a condition, two steps, or a name worth remembering goes into a function that you hook up to
@click by name. A template is there to show what happens, not to hide mission logic between the tags.One tool from today's four is left. The panel reports the station status as a label, but in Mission Control it is the color that counts: green means calm, yellow means check. A plain
class="ok" attribute is hard-coded in exactly the same way the number 180 was, and it is just as unable to change.For attributes Vue has the
v-bind directive, and its shorthand is a colon placed before the attribute name. Writing :class tells Vue: what stands on the right is not text, it is a JavaScript expression - evaluate it and put the result in as the class of the element. The simplest version is the name of a variable that holds the class name.1<template>
2 <span :class="statusClass">{{ statusLabel }}</span>
3</template>
4
5<script setup>
6import { ref } from 'vue'
7
8const statusClass = ref('ok')
9const statusLabel = ref('ALL OK')
10</script>This works, but it has a weak spot: two containers describe one state and nothing keeps them in agreement. Change the class somewhere and forget the label, and the panel will show a green alert. It is better to keep one truth - a plain boolean - and derive both the class and the label from it. The conditional operator is all you need: the JavaScript form of condition, question mark, value when true, colon, value when false.
1<template>
2 <span :class="systemsOk ? 'ok' : 'alert'">
3 {{ systemsOk ? 'ALL OK' : 'ALERT' }}
4 </span>
5</template>
6
7<script setup>
8import { ref } from 'vue'
9
10const systemsOk = ref(true)
11</script>One container, two places that use it, and a set that is always consistent. Notice that the same conditional expression sits once after a colon in an attribute and once inside mustaches - it is the same JavaScript syntax used in two different places in the template. Nothing changed in the
<span> tag itself either: it is still an ordinary element, except that its class is now decided by the state of the mission.What does
:class not do? It does not create styles. It supplies a class name and nothing else; the appearance is still described by ordinary CSS - here in a <style scoped> section, exactly like the one from the previous lesson.1<style scoped>
2.ok { color: #00ff88; }
3.alert { color: #ffd60a; }
4</style>Two rules, no magic - and that is the whole secret of dynamic classes. The directive switches the name, CSS turns the name into a color. When
systemsOk takes the value false, Vue removes the ok class from the tag, puts alert on, and the browser repaints the label yellow. The rules in the stylesheet stay untouched.One preview to close with:
:class also accepts an object form, for example :class="{ alert: !systemsOk }", which adds a class only when the condition is true. We will come back to it with the directives, once the panel has more than two states.Three mix-ups come back regularly at this point.
Writing
class="{{ statusClass }}" is an attempt to use mustaches in an attribute. Interpolation handles the text content of an element only, so Vue will not evaluate the expression here and will not substitute the value of the variable. The ok class never reaches the element and the color never appears.Writing
css-class="statusClass" exists neither in HTML nor in Vue. The browser treats it as an unknown attribute and the element ends up with no class at all.Writing
style:class="statusClass" mixes two things up. Vue does have :style, but it sets styles written directly on the element, not class names - and there is no such thing as style:class. A class always goes through :class.You have all four tools now, so it is time to assemble the mission status panel from them - the one that really hangs in Mission Control. It shows the mission name, the current phase, the distance to Mars, the days to target and the station status, and the button moves the mission to the next phase.
One new thing joins in, but from outside the Vue world: a plain JavaScript array with the phase names, plus a container holding the index of the current phase. Notice that the
phases array is not wrapped in ref - its contents never change, so there is nothing to watch. Reactivity has a cost, and in space you do not carry cargo you do not need: put containers only around values that really change and are meant to refresh the screen. The remainder operator % makes the counter return to the beginning after the last phase - that is a convenience for the demo, so you can keep clicking, not a description of a real mission.1<template>
2 <div class="mission-status">
3 <header>
4 <h1>{{ missionName }}</h1>
5 <span class="phase">{{ currentPhase }}</span>
6 </header>
7
8 <div class="metrics">
9 <div class="metric">
10 <label>Distance to Mars</label>
11 <span>{{ distanceToMars }} M km</span>
12 </div>
13 <div class="metric">
14 <label>Time to target</label>
15 <span>{{ daysRemaining }} days</span>
16 </div>
17 <div class="metric">
18 <label>Station status</label>
19 <span :class="systemsOk ? 'ok' : 'alert'">
20 {{ systemsOk ? 'ALL OK' : 'ALERT' }}
21 </span>
22 </div>
23 </div>
24
25 <button @click="nextPhase">Next mission phase</button>
26 </div>
27</template>
28
29<script setup>
30import { ref } from 'vue'
31
32const phases = ['Preparation', 'Launch', 'Flight', 'Mars Orbit', 'Nova Base']
33
34const missionName = ref('MARS ONE - First Colony')
35const currentPhase = ref(phases[0])
36const phaseIndex = ref(0)
37const distanceToMars = ref(225)
38const daysRemaining = ref(180)
39const systemsOk = ref(true)
40
41function nextPhase() {
42 phaseIndex.value = (phaseIndex.value + 1) % phases.length
43 currentPhase.value = phases[phaseIndex.value]
44
45 distanceToMars.value = Math.max(0, distanceToMars.value - 45)
46 daysRemaining.value = Math.max(0, daysRemaining.value - 36)
47}
48</script>Click the button a few times: the phase label moves through the stages, the distance and the day counter go down, and both of them stop at zero. All of that without a single line of code that touches the document. Look at the template once more - apart from the mustaches, one colon and one at sign, it is ordinary HTML, the same you have been writing since the first world. That has not changed and will not change: Vue does not replace HTML, it wires it up to data.
The number of places in which you describe the state of the mission has not changed either. There is exactly one: the
<script setup> section. The template only shows that state, the function only modifies it, and Vue makes sure the screen always agrees with the data. This division of roles will stay with you through the whole NOVA LAB world - in the next lesson you will see how modules like this one are wired into a single mission application.Remember this, @name:
ref() creates the container, the mustaches display it, the at sign reacts to the astronaut, and the colon paints the panel according to the state - and that is the full toolkit of your first working system.