Course outline

Vue / Basic: reactive components and input contracts / Lesson 6

Forms, validation, and stable list identity

Model editable input and repeated controls so values, errors, and local state remain attached to the intended record.

About 4 min readContent reviewed
On this page

Lesson goals

  • Explain native v-model and validation boundaries.
  • Use stable keys and accessible repeated controls.

Treat form input as a draft

v-model coordinates a native control’s value and relevant event, with behavior depending on control type. A text input, checkbox, radio, and select do not all produce the same data shape. Keep draft state separate from a confirmed server record when users can cancel or validation can fail. Choose when errors appear and how a user corrects them. A client check improves feedback but does not replace server-side validation and authorization. Explicit labels, clear instructions, and keyboard submission are part of the form’s behavior rather than optional decoration added after the data flow works.

Validate the proposed value before emitting it

The form owns a text draft and emits a normalized title only when it is nonempty after trimming. The error is initially absent and becomes visible after an invalid submission. Its label is structurally associated with the input by nesting, avoiding an arbitrary shared id in this one-field example. The submit modifier prevents native navigation while the handler decides the result. A successful emit is still a proposal to the parent, not proof that a remote save succeeded. Keep pending, success, and server error states explicit when adding persistence.

vue A draft with explicit submit validation
<script setup>
import { ref } from "vue"
const draft = ref("")
const error = ref("")
const emit = defineEmits(["save-title"])
function submit() {
  const title = draft.value.trim()
  error.value = title ? "" : "Enter a title"
  if (title) emit("save-title", title)
}
</script>

<template>
  <form @submit.prevent="submit">
    <label>Note title <input v-model="draft"></label>
    <p v-if="error" role="alert">{{ error }}</p>
    <button type="submit">Save title</button>
  </form>
</template>

In a configured SFC app, submitting spaces displays Enter a title and emits no save event. Submitting a nonempty title emits its trimmed value. Browser focus, error announcement, and event behavior require separate actual UI testing.

Keep conversion and business validity separate

Modifiers such as trim, number, and lazy change the binding contract and should be chosen deliberately. number is not a promise that every state is a valid business number; empty input and unparsable values need explicit handling according to the documented behavior. Use bound values for nonstring select or radio choices when that is the intended model. A custom component can expose v-model through modelValue and update:modelValue, or defineModel in supported versions; defineModel is available in this Vue 3.5 baseline. Avoid an uncoordinated child default that diverges from an undefined parent value.

Bind repeated state to stable ids

A list key tells Vue which rendered item represents which logical record during insertion, filtering, and reordering. Use a stable unique primitive id rather than an array index when item state matters. A key is not an HTML id and does not automatically label a control. The example nests each checkbox inside its label and uses the record id as its key. Reordering keeps the intended record identity. Derive filtered lists before rendering instead of putting v-if and v-for on the same element, whose precedence can make scope confusing in Vue 3.

vue Stable records with labeled checkbox state
<script setup>
import { ref } from "vue"
const notes = ref([
  { id: "n1", title: "Reactivity", selected: false },
  { id: "n2", title: "Components", selected: true }
])
</script>

<template>
  <ul>
    <li v-for="note in notes" :key="note.id">
      <label><input type="checkbox" v-model="note.selected"> {{ note.title }}</label>
    </li>
  </ul>
</template>

The initial Components checkbox is selected and Reactivity is not. The component owns this local array; a prop-backed collection would need the parent-update contract instead of hidden nested mutation. No browser test is claimed.

Test editing, reordering, and error recovery

Verify that input survives the intended lifecycle, errors are associated with the relevant control, and a user can correct and resubmit using keyboard or assistive technology. Test insertion, deletion, and sorting while controls contain local state. Duplicate keys are a contract defect even when the first screenshot looks correct. For larger forms, separate reusable validation rules from presentation and retain server errors without confusing them with client checks. Avoid persisting every keystroke as if it were a confirmed record unless the product explicitly defines that behavior. A form is complete when the user task and data contract agree.

Common errors and fixes

Input state moves to another row after sorting

Why it happens
The list uses position as identity.
How to fix it
Use stable unique record keys.
Prevent it next time
Test stateful rows during reordering.

An empty numeric input is treated as a valid count

Why it happens
Binding conversion is confused with business validation.
How to fix it
Validate emptiness, type, range, and meaning explicitly.
Prevent it next time
Include boundary cases in the form contract.

A child model starts differently from the parent

Why it happens
A child default is uncoordinated with an undefined parent value.
How to fix it
Initialize ownership deliberately or require the model value.
Prevent it next time
Test initial and externally changed values.

Check your understanding

1. Why is an array index risky as a key for editable rows?

Show worked solution

The same position can represent another record after insertion or sorting, allowing local control state to follow the position instead of the record. A stable id preserves the intended logical identity; labels and validation still require their own contracts.

2. A title passes client validation and a server save fails. What state should remain?

Show worked solution

Retain the user’s draft and expose the server failure separately with a deliberate retry path. Do not erase the text or announce persistence merely because a local event was emitted. Verify retry does not create duplicate confirmed operations.

Key takeaways

  • A draft and a confirmed record have different lifetimes.
  • Binding conversion does not replace validation.
  • Stable keys protect logical identity during list changes.

Official references