In the previous lesson you wired the mission panel to variables: the crew name, the notes, the system checkboxes, the module list. The form works, but the signal leaving it is raw. The Mission Control duty officer reports three faults. First, the telemetry log records a new value after every keystroke, so a single word leaves a dozen entries behind it. Second, the credits field hands over text instead of a number, so every addition glues digits together instead of summing them. Third, the operator pastes a module name from the clipboard together with a trailing space, and the lookup against the station catalog stops matching.
You do not have to repair any of this by hand, @name. Exactly as you appended
.prevent or .stop to events, you can append a modifier to v-model: a dot and a name right after the directive name. A modifier does not change what the field is bound to - it changes the way the value travels into the variable. Vue offers three of them: .lazy, .number and .trim. Each one repairs exactly one of the duty officer's faults.Let us start with the first fault, because it is about timing rather than content. A text field with
v-model listens for the browser event called input, which the browser fires after every single character. For a field with a live preview that is ideal behavior - the operator types, the panel shows the effect at once. For a telemetry log, or for an expensive trajectory recalculation, it is waste: we care about one finished value, not about a dozen intermediate states.The browser has a separate event for exactly this occasion:
change. On a text field it fires only once the altered value has been committed - that is, when you leave the field (the blur event) or press Enter. The .lazy modifier does one single thing: it switches the v-model listener from input over to change. Below are two identically built readout channels that differ by nothing except that modifier.1<template>
2 <label>Live (input)</label>
3 <input v-model="liveSignal" />
4 <p>Readout: {{ liveSignal }}</p>
5
6 <label>Lazy (change)</label>
7 <input v-model.lazy="committedSignal" />
8 <p>Readout: {{ committedSignal }}</p>
9</template>
10
11<script setup>
12import { ref } from 'vue'
13
14const liveSignal = ref('')
15const committedSignal = ref('')
16</script>Type something into the first field - the paragraph beneath it changes letter by letter. Type the same thing into the second one - the paragraph stays empty until you click away or press Enter. And now the most valuable observation: what did not change. The binding is still two-way,
committedSignal is still ordinary text, and the template differs by five characters. Only the moment of the update moved.Three misunderstandings are worth ruling out immediately, because
.lazy gets confused with things it is not. It is not a countdown - nothing happens every 500 milliseconds. A delay measured by a clock is called debounce, and in Vue you build it yourself, with a watcher and setTimeout, just as in the module on station monitoring systems. It is also not waiting for the form to be submitted - the value lands in the variable the moment you leave the field, long before anybody clicks a button. And finally: the update is still automatic, you never rewrite anything manually.The second fault is sneakier, because the panel looks correct right up until the first piece of arithmetic. A form field in the browser always hands back text - even when you type nothing but digits into it. So if you assign that value to a variable and add a number to it, JavaScript will not compute a sum, it will glue two pieces of text together. See it on the mission credits field, for now with no modifier at all.
1<template>
2 <label>Credits</label>
3 <input v-model="budget" />
4
5 <p>Type: {{ typeof budget }}</p>
6 <p>budget + 10 = {{ budget + 10 }}</p>
7</template>
8
9<script setup>
10import { ref } from 'vue'
11
12const budget = ref('')
13</script>Type 1500. The first paragraph shows
string, and the second one - instead of 1510 - a piece of text made of the value you typed with a ten stuck onto the end. This is not a Vue bug and not a browser bug, only the natural consequence of a form field storing characters. As long as the value is used purely for display, nobody notices. The trouble begins when you sum budgets, compare limits and sort modules by cost.The cure is the
.number modifier. Vue then runs the typed value through a numeric conversion - it behaves like parseFloat: a number is pulled out of the text, and if the conversion fails, the untouched original goes into the variable. This is a very important distinction, @name: .number converts, but it does not validate. It will not reject an entry, light a red lamp or lock the form - if the content cannot be turned into a number, it simply leaves the text exactly as it was. Checking correctness is separate work, and you will do it in the next lesson.1<template>
2 <label>Credits</label>
3 <input v-model.number="budget" type="number" />
4
5 <p>Type: {{ typeof budget }}</p>
6 <p>budget + 10 = {{ budget + 10 }}</p>
7</template>
8
9<script setup>
10import { ref } from 'vue'
11
12const budget = ref(0)
13</script>The same template, one modifier added - and the first paragraph now shows
number, while the second shows a genuine sum. Notice what .number does not do. It does not make sure that only digits can be entered - that is the job of the type="number" attribute, which belongs to the browser and decides what may be tapped into the field in the first place. Nor does it format the number with thousands separators; that reader-friendly notation is produced separately, at display time, with toLocaleString. Inside the field itself the value stays a raw number.Since the
type="number" attribute has already appeared, one honest caveat. Vue treats such a field as numeric by assumption and attempts the conversion even without the modifier, so in this particular example .number changes nothing. I still recommend writing it always and explicitly. First, the intent is then visible in the template and does not depend on which type somebody swaps in later. Second, the very same notation also works on a plain text field. The canonical form worth having in your fingers looks exactly like the block above: first <input, then v-model.number= with the variable name in quotes, and type="number" at the end.The third fault comes from a human reflex. The operator copies a module name out of a report and carries a trailing space along with it. Nothing alarming shows up on screen, but the variable now holds a character that ruins everything which compares text: a search through the station catalog fails to find the module, and a naive "is the field empty" check lets through an entry made of nothing but spaces. The
.trim modifier removes whitespace from the beginning and the end of the value before it reaches the variable.1<template>
2 <label>Raw</label>
3 <input v-model="rawName" />
4 <p>chars: {{ rawName.length }}</p>
5
6 <label>Trimmed</label>
7 <input v-model.trim="missionName" />
8 <p>chars: {{ missionName.length }}</p>
9</template>
10
11<script setup>
12import { ref } from 'vue'
13
14const rawName = ref('')
15const missionName = ref('')
16</script>Type a name with a space at the front and at the back into both fields. The character counter under the first field counts those spaces, the counter under the second one does not. Now three boundaries that are easy to forget.
.trim does not shorten text to a given length - there is no character limit whatsoever, only the edges get cut. Neither does it remove every space: the name "Oxygen Alpha" keeps the space in its middle, because the modifier never touches the inside of the string. And it does not change letter case - lowercasing is toLowerCase, an entirely different tool. Just as importantly, the modifier works on the value travelling toward the variable, not on what you physically tap into the field - while you type, the space is visible in the field, the variable simply never receives it.The mission registration panel needs all three repairs at once, and you do not have to choose just one. Modifiers are chained: you write the names one after another, each preceded by a dot, with no spaces and no commas. The form
v-model.lazy.trim="text" is perfectly valid - whereas a comma placed between the names would be read by Vue as part of one single unknown modifier name, and would simply do nothing.Vue does not impose an order, so
v-model.trim.lazy means precisely the same as v-model.lazy.trim. Since the choice belongs to you, I recommend keeping one steady convention: first the modifier describing what the value is (.number or .trim), then the one describing when it reaches the variable (.lazy). Hence v-model.number.lazy - it reads like a sentence: a number, committed once you leave the field.1<template>
2 <form @submit.prevent="registerMission">
3 <label>Mission name</label>
4 <input v-model.trim.lazy="missionName" />
5
6 <label>Credits</label>
7 <input v-model.number.lazy="budget" type="number" />
8
9 <button type="submit">Register</button>
10 </form>
11
12 <p>Panel: {{ missionName }} / {{ budget }}</p>
13</template>
14
15<script setup>
16import { ref } from 'vue'
17
18const missionName = ref('')
19const budget = ref(0)
20
21function registerMission() {
22 console.log('Mission registered', missionName.value, budget.value)
23}
24</script>Fill in both fields and move between them with the Tab key, without touching the button at all. The paragraph under the form already shows the trimmed name and the numeric budget - proof that
.lazy waits for you to leave the field, not for the form to be sent. Clicking the button takes focus away from the field first, so registerMission always receives current values. Apart from that there is nothing new in this block: @submit.prevent you know from the lesson on event modifiers, and ref together with v-model have been with you since the start of the module. All that has been added are the dots carrying modifiers.One warning at the end, because modifiers tend to be added out of reflex. Do not give
.lazy to a field that filters the module list live - the operator loses the preview of results while typing and will declare the panel broken. Be careful with .number on designations that merely look like numbers: a sector number written as 007 turns into a plain seven after conversion and stops matching the catalog. With .trim on names, addresses and identifiers there is practically no trouble at all, and you may safely apply it by default.Remember, @name: a
v-model modifier is the calibration dial beside the panel - the signal runs down the very same cable, but it reaches the mission computer already clean, in the right type and at the right moment.