Vue.js course Β· Module 5: Events & Forms

Forms - v-model for Mission Data

6 min read
In this lesson4

The crew registration panel has a name field and a paragraph that repeats the name. For this to work, two connections are needed: the variable has to reach the field, and whatever the operator types has to get back into the variable. Writing that by hand for each of ten fields means twenty bindings and twenty chances for a mistake. Vue closes both directions in a single directive: v-model, a two-way data binding between a form field and a variable.

What v-model does under the hood

v-model is a shorthand. On a text field it expands into two things you already know: the :value binding, which writes the variable into the field, and the @input listener, which after every change reads $event.target.value and saves the result into the variable.

1<template>
2  <input :value="crewName" @input="crewName = $event.target.value" />
3  <input v-model="crewName" />
4</template>
5
6<script setup>
7import { ref } from 'vue'
8
9const crewName = ref('')
10</script>

Both fields work identically and stay in sync with each other: type in one, and the text appears in the other, because both read and write the same ref. crewName remains an ordinary ref, so v-model needs no special state. Remember one rule: v-model ignores the value, checked and selected attributes written in the HTML. The source of truth is always the variable, which is why you set the initial value in JavaScript.

v-model Basics

The same directive works on every form field, but under the hood it picks the property and the event that fit the field type. Text fields and <textarea> use value and the input event, checkboxes and radio buttons use checked and change, and <select> uses value and change. Let us walk through the mission form in stages, starting with text.

1<template>
2  <div class="mission-form" style="background: #0a0e27; color: #00ff88;">
3    <!-- Text input -->
4    <input v-model="crewName" placeholder="Crew name" style="background: #1a1e37; color: #00ff88;" />
5    <p>Crew: {{ crewName }}</p>
6
7    <!-- Textarea -->
8    <textarea v-model="missionNotes" placeholder="Mission notes" style="background: #1a1e37; color: #00ff88;"></textarea>
9    <p>Notes: {{ missionNotes }}</p>

The field and the text area behave the same way: the paragraph below them changes with every letter. Do not put {{ missionNotes }} between the <textarea> tags, because interpolation does not work in that spot. Always attach v-model to a text area. Now the checkboxes.

1    <!-- Checkbox - boolean -->
2    <label style="color: #00ff88;">
3      <input type="checkbox" v-model="isAutomated" />
4      Automatic mode
5    </label>
6    <p>Automatic: {{ isAutomated }}</p>
7
8    <!-- Checkbox - array -->
9    <label style="color: #00ff88;">
10      <input type="checkbox" v-model="activeSystems" value="oxygen" />
11      Oxygen generator
12    </label>
13    <label style="color: #00ff88;">
14      <input type="checkbox" v-model="activeSystems" value="water" />
15      Water recycler
16    </label>
17    <p>Active systems: {{ activeSystems }}</p>

A single checkbox bound to isAutomated produces a boolean value: true when checked and false when unchecked. When several checkboxes share one array, each has its own value attribute, and activeSystems collects the values of the checked boxes, in the order they were checked. Time for the radio buttons and the drop-down list.

1    <!-- Radio -->
2    <label style="color: #00ff88;">
3      <input type="radio" v-model="powerMode" value="eco" />
4      Eco
5    </label>
6    <label style="color: #00ff88;">
7      <input type="radio" v-model="powerMode" value="performance" />
8      Performance
9    </label>
10    <p>Power mode: {{ powerMode }}</p>
11
12    <!-- Select -->
13    <select v-model="selectedModule" style="background: #1a1e37; color: #00ff88;">
14      <option disabled value="">Select module</option>
15      <option value="alpha">Oxygen Alpha</option>
16      <option value="beta">Plasma Beta</option>
17      <option value="gamma">Water Gamma</option>
18    </select>
19    <p>Module: {{ selectedModule }}</p>
20  </div>
21</template>

The radio group writes the value of the selected button into powerMode, so the variable always holds a single value. <select> does the same with the chosen option. The first option with disabled and an empty value is the recommended pattern: when the variable starts as an empty string, the list shows a prompt to choose, and on iOS it does not block choosing the first real option. The script is all that is left.

1<script setup>
2import { ref } from 'vue'
3
4const crewName = ref('')
5const missionNotes = ref('')
6const isAutomated = ref(false)
7const activeSystems = ref([])
8const powerMode = ref('')
9const selectedModule = ref('')
10</script>

Every field gets a starting value that matches its type: an empty string for the text fields, the radio group and the list, false for the single checkbox and an empty array for the checkbox group. A type mistake, such as ref('') for a checkbox group, is the most common cause of strange form behavior.

Select options from an array

The catalog of station modules keeps growing, so options are not typed by hand. You generate them with the v-for directive from an array of objects, and you provide the option value through the :value binding. As with any v-for list, every option also gets a unique :key.

1<template>
2  <select v-model="selectedModuleId">
3    <option disabled value="">Select a module</option>
4    <option v-for="mod in modules" :key="mod.id" :value="mod.id">
5      {{ mod.name }}
6    </option>
7  </select>
8  <p>Selected id: {{ selectedModuleId }}</p>
9</template>
10
11<script setup>
12import { ref } from 'vue'
13
14const modules = ref([
15  { id: 1, name: 'Oxygen Alpha' },
16  { id: 2, name: 'Plasma Beta' }
17])
18const selectedModuleId = ref('')
19</script>

After you pick Plasma Beta, the paragraph shows 2. Pay attention to the type: thanks to :value, the variable receives the number 2, exactly the one from the array, while a plain value="2" attribute would always produce text. The :value binding accepts any value, even a whole object.

From data to submission

Every form is built in the same order: first the state in ref or reactive, then binding the fields with v-model, next @submit.prevent on the form, and finally validation before sending. You already know the first three steps, and we will add validation in the lesson on advanced forms. I recommend giving variables names that say what they store rather than what kind of field they belong to: activeSystems survives replacing the checkboxes with a multi-select list, checkboxValues does not. In the next lesson you will tune the signal from the fields with the .lazy, .number and .trim modifiers.

Remember: v-model is a two-way communication channel - the field on the panel and the variable in the mission computer always show the same value, no matter which side changed it.

Code for this lesson: App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4const astronautName = ref('')
5const missionCode = ref('')
6const missionDescription = ref('')
7const statusReport = ref('')
8
9const characterCount = computed(() => missionDescription.value.length)
10const wordCount = computed(() => {
11  return missionDescription.value.trim().split(/\s+/).filter(word => word.length > 0).length
12})
13
14const isFormValid = computed(() => {
15  return astronautName.value.length >= 3 &&
16         missionCode.value.length >= 4 &&
17         missionDescription.value.length >= 10
18})
19</script>
20
21<template>
22  <div class="data-transmission">
23    <div class="transmission-header">
24      <h2>Data Transmission System</h2>
25      <p class="subtitle">v-model - Two-way Data Binding</p>
26    </div>
27
28    <div class="form-container">
29      <div class="form-group">
30        <label for="astronaut">Astronaut Name:</label>
31        <input
32          id="astronaut"
33          v-model="astronautName"
34          type="text"
35          placeholder="Enter name..."
36          class="data-input"
37        />
38        <div class="input-feedback">
39          Entered: <strong>{{ astronautName || '(empty)' }}</strong>
40        </div>
41      </div>
42
43      <div class="form-group">
44        <label for="mission-code">Mission Code:</label>
45        <input
46          id="mission-code"
47          v-model="missionCode"
48          type="text"
49          placeholder="e.g. MARS-2087-A1"
50          class="data-input"
51        />
52        <div class="input-feedback">
53          Code: <strong>{{ missionCode || '(empty)' }}</strong>
54        </div>
55      </div>
56
57      <div class="form-group">
58        <label for="description">Mission Description:</label>
59        <textarea
60          id="description"
61          v-model="missionDescription"
62          placeholder="Describe mission details..."
63          class="data-textarea"
64          rows="5"
65        ></textarea>
66        <div class="textarea-stats">
67          <span>Characters: {{ characterCount }}</span>
68          <span>Words: {{ wordCount }}</span>
69        </div>
70      </div>
71
72      <div class="form-group">
73        <label for="status">Status Report (optional):</label>
74        <textarea
75          id="status"
76          v-model="statusReport"
77          placeholder="Additional information..."
78          class="data-textarea"
79          rows="3"
80        ></textarea>
81      </div>
82
83      <div class="validation-status" :class="{ valid: isFormValid }">
84        <span v-if="isFormValid">Form ready for transmission</span>
85        <span v-else>Fill in all required fields</span>
86      </div>
87    </div>
88
89    <div class="data-preview">
90      <h3>Transmitted Data Preview</h3>
91      <div class="preview-box">
92        <pre>{{ {
93  astronaut: astronautName || null,
94  missionCode: missionCode || null,
95  description: missionDescription || null,
96  status: statusReport || null,
97  valid: isFormValid
98} }}</pre>
99      </div>
100    </div>
101
102    <div class="vmodel-info">
103      <h3>How v-model works:</h3>
104      <div class="info-content">
105        <p><code>v-model</code> is shorthand for:</p>
106        <code class="code-example">:value="variable" @input="variable = $event.target.value"</code>
107        <p>Automatically synchronizes data between input and variable!</p>
108      </div>
109    </div>
110  </div>
111</template>
112
113<style scoped>
114.data-transmission {
115  background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
116  color: #00ff88;
117  padding: 30px;
118  border-radius: 15px;
119  border: 2px solid #00b4d8;
120  font-family: 'Courier New', monospace;
121  max-width: 900px;
122  margin: 0 auto;
123}
124
125.transmission-header {
126  text-align: center;
127  margin-bottom: 30px;
128  padding-bottom: 20px;
129  border-bottom: 2px solid #00b4d8;
130}
131
132.transmission-header h2 {
133  color: #00ff88;
134  margin: 0 0 10px 0;
135  font-size: 28px;
136  text-shadow: 0 0 10px #00ff88;
137}
138
139.subtitle {
140  color: #00b4d8;
141  margin: 0;
142  font-size: 14px;
143}
144
145.form-container {
146  margin-bottom: 30px;
147}
148
149.form-group {
150  margin-bottom: 25px;
151}
152
153.form-group label {
154  display: block;
155  color: #00b4d8;
156  margin-bottom: 10px;
157  font-size: 14px;
158  font-weight: bold;
159}
160
161.data-input,
162.data-textarea {
163  width: 100%;
164  background: rgba(0, 180, 216, 0.1);
165  border: 2px solid #00b4d8;
166  border-radius: 8px;
167  padding: 12px 15px;
168  color: #00ff88;
169  font-size: 16px;
170  font-family: 'Courier New', monospace;
171  box-sizing: border-box;
172  transition: all 0.3s ease;
173}
174
175.data-input:focus,
176.data-textarea:focus {
177  outline: none;
178  border-color: #00ff88;
179  box-shadow: 0 0 15px rgba(0, 255, 136, 0.3);
180  background: rgba(0, 180, 216, 0.15);
181}
182
183.data-textarea {
184  resize: vertical;
185}
186
187.input-feedback {
188  margin-top: 8px;
189  color: #00b4d8;
190  font-size: 12px;
191  padding: 8px 12px;
192  background: rgba(0, 180, 216, 0.05);
193  border-radius: 4px;
194}
195
196.input-feedback strong {
197  color: #00ff88;
198}
199
200.textarea-stats {
201  margin-top: 8px;
202  display: flex;
203  gap: 20px;
204  color: #00b4d8;
205  font-size: 12px;
206}
207
208.validation-status {
209  padding: 15px;
210  background: rgba(255, 107, 53, 0.1);
211  border: 2px solid #ff6b35;
212  border-radius: 8px;
213  text-align: center;
214  font-weight: bold;
215  color: #ff6b35;
216  transition: all 0.3s ease;
217}
218
219.validation-status.valid {
220  background: rgba(0, 255, 136, 0.1);
221  border-color: #00ff88;
222  color: #00ff88;
223}
224
225.data-preview {
226  margin-bottom: 30px;
227}
228
229.data-preview h3 {
230  color: #00ff88;
231  margin-bottom: 15px;
232}
233
234.preview-box {
235  background: rgba(0, 0, 0, 0.4);
236  border: 1px solid #00b4d8;
237  border-radius: 8px;
238  padding: 20px;
239  overflow-x: auto;
240}
241
242.preview-box pre {
243  margin: 0;
244  color: #00ff88;
245  font-size: 14px;
246  line-height: 1.6;
247}
248
249.vmodel-info {
250  background: rgba(0, 255, 136, 0.05);
251  padding: 20px;
252  border-radius: 8px;
253  border: 1px solid #00ff88;
254}
255
256.vmodel-info h3 {
257  color: #00ff88;
258  margin-top: 0;
259}
260
261.info-content {
262  color: #00b4d8;
263}
264
265.info-content p {
266  margin: 10px 0;
267}
268
269.info-content code {
270  background: rgba(0, 255, 136, 0.2);
271  padding: 2px 6px;
272  border-radius: 4px;
273  color: #00ff88;
274  font-size: 13px;
275}
276
277.code-example {
278  display: block;
279  background: rgba(0, 180, 216, 0.2);
280  padding: 10px 15px;
281  border-radius: 6px;
282  margin: 10px 0;
283  font-size: 12px;
284}
285</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. 1. What is v-model in Vue.js?

  2. 2. v-model is a shorthand for which combination?

These are 2 of 7 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Use v-model with a text input and display the entered value in a paragraph.

  • Horizontal ordering

    Arrange the correct syntax for an input with v-model:

  • Click in order

    Arrange the syntax for a checkbox with v-model:

  • Code editor

    Use checkbox (array) to select equipment and radio to choose a mission type.

  • Vertical ordering

    Arrange the steps of creating a reactive Vue form from data to validation:

  • Horizontal ordering

    Arrange the correct syntax for a select option with v-for:

  • Code editor

    Build a select with v-for and :value for a list of objects.

Useful articles