# Events and Forms

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: User Interaction
Source: https://www.devtutorials.ai/learn/vue/user-interaction/events-and-forms

> v-on and @, method and inline handlers, $event, key aliases, system keys, .exact and event modifiers, then v-model on every control, value bindings, .lazy, .number and .trim, submit and reset, validation rules, error messages, accessible errors with useId() and validation libraries.

## Events

- An **event** = something that happens on the page: a click, a key press, a mouse move.
- `v-on` listens for an event and runs your code when it happens.
- Its short form is `@`. `v-on:click` and `@click` are the same.
- Your code usually changes state. Vue then updates the screen.

**Simple lesson:** `@event="code"` runs code when the event happens.

### A Real-World Example

- Meera's counter has buttons for each dish.
- Each button adds a different number of plates.
- One method handles all buttons. Each button passes its own numbers.

const plates = ref<number>(0);
const bill = ref<number>(0);

function add(count: number, price: number): void {
    plates.value += count;
    bill.value += count * price;
}
</script>

<template>
    <button @click="add(1, 90)">1 Dosa</button>
    <button @click="add(2, 50)">2 Idli</button>
    <button @click="add(3, 40)">3 Vada</button>
    <p>Plates: {{ plates }}, Bill: ₹{{ bill }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

### What You Will Learn

1. How to write inline handlers and method handlers, with arguments and `$event`.
2. How to listen to mouse and keyboard events, key aliases and system keys.
3. How event modifiers like `.stop`, `.prevent` and `.once` work.
4. How `v-model` links a form control and a ref both ways.
5. How to use `v-model` on every kind of control, with value bindings.
6. The `.lazy`, `.number` and `.trim` modifiers.
7. How to submit and reset a form without a page reload.
8. How to validate input and turn off the submit button.
9. How to make errors clear for screen readers.

## Inline and Method Handlers

- **Inline handler** = a short piece of code: `@click="count++"`.
- **Method handler** = the name of a function: `@click="addOrder"`.
- A method handler gets the **event object** as its first argument.
- **Event object** = details about the event, such as which element was clicked.

const count = ref<number>(0);

function showEvent(event: MouseEvent): void {
    const button = event.target as HTMLButtonElement;
    console.log("You clicked:", button.textContent, "at x =", event.clientX);
}
</script>

<template>
    <button @click="count++">Inline: {{ count }}</button>
    <button @click="showEvent">Method handler</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

### Arguments and $event

- Call the method with arguments: `@click="add(2)"`.
- Then the event object is not passed by itself.
- Need both? Pass `$event` as an argument: `@click="add(2, $event)"`.
- You can also run two things: `@click="one(); two()"`.

const log = ref<string[]>([]);

function order(dish: string, event: MouseEvent): void {
    log.value.push(\`\${dish} (shift held: \${event.shiftKey})\`);
}

function ring(): void {
    console.log("Ding! Kitchen bell");
}
</script>

<template>
    <button @click="order('Dosa', $event); ring()">Order Dosa</button>
    <ul>
      <li v-for="(line, i) in log" :key="i">{{ line }}</li>
    </ul>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**Simple lesson:** pass arguments in brackets. Add `$event` when you also need the event.

Name handlers after what they do, such as `addOrder` or `openMenu`, not after the event, such as `onClick`. The template then reads like a sentence.

## Mouse and Keyboard Events

- Common mouse events: `click`, `dblclick`, `mouseenter`, `mouseleave`, `contextmenu`.
- Common keyboard events: `keydown` and `keyup`.
- **Key alias** = a modifier that picks one key: `@keyup.enter`.
- Vue has aliases for `.enter`, `.tab`, `.delete`, `.esc`, `.space`, `.up`, `.down`, `.left` and `.right`.
- You can also use any key name in kebab-case, such as `.page-down`.

const hovering = ref<boolean>(false);
const notes = ref<string[]>([]);

function addNote(event: KeyboardEvent): void {
    const box = event.target as HTMLInputElement;
    if (box.value.trim()) notes.value.push(box.value.trim());
    box.value = "";
}

function clearBox(event: KeyboardEvent): void {
    (event.target as HTMLInputElement).value = "";
}
</script>

<template>
    <p @mouseenter="hovering = true" @mouseleave="hovering = false">
      {{ hovering ? "Today's special: Pongal!" : "Hover here" }}
    </p>
    <input placeholder="Type a note, press Enter (Esc clears)" @keyup.enter="addNote" @keyup.esc="clearBox" />
    <ul>
      <li v-for="(n, i) in notes" :key="i">{{ n }}</li>
    </ul>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

### System Keys and .exact

- **System keys** = `.ctrl`, `.alt`, `.shift` and `.meta` (⌘ on Mac, ⊞ on Windows).
- `@click.ctrl` runs only if Ctrl is held during the click.
- `@click.ctrl` still runs if Shift is held too.
- `.exact` means "these keys and no others". `@click.exact` = no system key at all.

    <button @click.exact="console.log('Plain click: open the dish')"
            @click.ctrl="console.log('Ctrl + click: add to favourites')"
            @click.shift.exact="console.log('Only Shift + click: compare')">
      Click, Ctrl + click or Shift + click
    </button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

`console` is one of the safe globals a template may use, so the handlers above can log straight from the template. In bigger components, call a script function instead. It is easier to read and test.

**Simple lesson:** add `.enter`, `.esc` and friends for keys, and `.ctrl` or `.exact` for system keys.

## Event Modifiers

- **Event modifier** = a dot word that changes how the event behaves.
- `.stop` = stop the event from reaching parent elements. Same as `event.stopPropagation()`.
- `.prevent` = stop the browser's default action, like reloading on a form submit.
- `.self` = run only if this exact element was clicked, not a child.
- `.capture` = run on the way **down**, before the children.
- `.once` = run one time only.
- `.passive` = tell the browser you will not call `preventDefault`. It makes scrolling smoother.

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 220" role="img" aria-labelledby="fig-vue-bubble" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-bubble">Animation: a click on a button inside a card inside the page. The event travels down in the capture phase, reaches the button, then bubbles up to the card and the page. With .stop on the button, it stops at the button</title>
  <rect x="40" y="20" width="560" height="180" rx="14" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} />
  <text x="60" y="44" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700 }}>page (div)</text>
  <rect x="120" y="60" width="400" height="120" rx="12" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }} />
  <text x="140" y="84" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700 }}>card (div)</text>
  <rect x="250" y="110" width="140" height="44" rx="10" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }} />
  <text x="320" y="137" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--mono)", fontSize: 13, fontWeight: 700 }}>button</text>
  <circle r="8" cx="320" style={{ fill: "var(--ngc-green)" }}>
    <animate attributeName="cy" values="10;10;72;132;132;72;30;30" keyTimes="0;0.05;0.2;0.35;0.45;0.6;0.75;1" dur="7s" repeatCount="indefinite" />
    <animate attributeName="opacity" values="1;1;1;1;1;1;1;0" keyTimes="0;0.05;0.2;0.35;0.45;0.6;0.75;1" dur="7s" repeatCount="indefinite" />
  </circle>
  <g style={{ fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>
    <text x="480" y="216" textAnchor="middle" opacity="0" style={{ fill: "var(--ngc-slate)" }}><animate attributeName="opacity" values="0;1;1;0;0" keyTimes="0;0.05;0.33;0.35;1" dur="7s" repeatCount="indefinite" />1. capture: down to the target</text>
    <text x="480" y="216" textAnchor="middle" opacity="0" style={{ fill: "var(--ngc-orange)" }}><animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.35;0.36;0.44;0.45;1" dur="7s" repeatCount="indefinite" />2. target: the button's handler</text>
    <text x="480" y="216" textAnchor="middle" opacity="0" style={{ fill: "var(--ngc-purple)" }}><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.45;0.46;0.95;1" dur="7s" repeatCount="indefinite" />3. bubble: up to card, then page</text>
  </g>
</svg>
<figcaption>Events go down (capture), hit the target, then bubble up. .stop ends the trip.</figcaption>
</figure>

function log(text: string): void {
    console.log(text);
}
</script>

<template>
    <div class="card" @click="log('card clicked')">
      <button @click="log('like clicked')">Like (bubbles)</button>
      <button @click.stop="log('save clicked')">Save (.stop)</button>
    </div>

    <form @submit.prevent="log('form sent, no page reload')">
      <button>Send (.prevent)</button>
    </form>

    <div class="card" @click.self="log('only the card itself')">
      Click the grey area, then the button <button>Inner</button>
    </div>

    <button @click.once="log('free dessert claimed')">Free dessert (.once)</button>

    <div @click.capture="log('capture: outer first')">
      <button @click="log('then the button')">Capture order</button>
    </div>
</template>

<style>
.card { background: #eee; padding: 8px; margin: 6px 0; border-radius: 8px; }
</style>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** "Like" logged twice, because the click bubbled to the card.
- "Save" logged once, because `.stop` stopped it.
- "Send" did not reload the page, because of `.prevent`.

Without `.prevent`, a form submit reloads the page and you lose all your state. Always write `@submit.prevent` on Vue forms.

### Chaining Modifiers

- You can chain modifiers: `@click.stop.prevent`.
- Order matters. `@click.prevent.self` prevents every click. `@click.self.prevent` prevents only clicks on the element itself.
- Do not use `.passive` and `.prevent` together. The browser ignores `.prevent` and warns.

## Mouse Button Modifiers

- `.left`, `.right` and `.middle` pick a mouse button.
- `@click.right` does not exist as a normal click. Use `@contextmenu` or `@mouseup.right`.
- Add `.prevent` to stop the browser's own right-click menu.

const message = ref<string>("Right-click the dish");
</script>

<template>
    <p class="dish" @contextmenu.prevent="message = 'Menu: Edit | Remove | Mark sold out'">Masala Dosa</p>
    <p>{{ message }}</p>
    <p @mouseup.middle="message = 'Middle button: open in new tab'">Middle-click me</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**Simple lesson:** modifiers change how an event behaves. `.prevent` and `.stop` are the ones you will use most.

## Forms in Vue

- Forms collect data: names, phone numbers, choices.
- Without Vue, you read each box by hand when the form is sent.
- `v-model` links a form control and a ref **both ways**.
- **Two-way binding** = typing changes the ref, and changing the ref changes the box.
- `v-model` ignores `value`, `checked` and `selected` written in the HTML. The **ref** is the only source of the start value.
- For languages typed with an input method (IME), the ref updates when a word is finished, not on every key.

**Simple lesson:** `v-model` keeps a form control and your state the same, both ways.

### In Practice: a Booking Form

- Meera takes table bookings online.
- She needs a name, the number of guests and a time.
- The summary below the form must update while people type.

const name = ref<string>("");
const guests = ref<number>(2);
const time = ref<string>("19:00");
</script>

<template>
    <form @submit.prevent>
      <label>Name <input v-model="name" /></label>
      <label>Guests <input v-model.number="guests" type="number" min="1" /></label>
      <label>Time
        <select v-model="time">
          <option>19:00</option>
          <option>20:00</option>
          <option>21:00</option>
        </select>
      </label>
    </form>
    <p>Table for {{ guests }} at {{ time }}, name: {{ name || "(none yet)" }}</p>
</template>

<style>
label { display: block; margin: 4px 0; }
</style>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

## How v-model Works

- `v-model="name"` on a text box does two jobs:
  - it sets the box's `value` from `name`
  - it updates `name` on every `input` event
- It is short for `:value="name" @input="name = $event.target.value"`.

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 170" role="img" aria-labelledby="fig-vue-vmodel" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-vmodel">Animation: typing in the input sends the text to the ref through the input event, and changing the ref sends the value back to the input through the value binding</title>
  <defs><marker id="vm-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10 z" style={{ fill: "var(--ngc-slate)" }} /></marker></defs>
  <rect x="30" y="50" width="200" height="60" rx="10" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-purple)", strokeWidth: 2 }} />
  <text x="130" y="40" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>input box</text>
  <text x="50" y="87" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--mono)", fontSize: 16 }}>Asha<animate attributeName="opacity" values="1;1;0;0;1" keyTimes="0;0.5;0.51;0.97;1" dur="7s" repeatCount="indefinite" /></text>
  <text x="50" y="87" opacity="0" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--mono)", fontSize: 16 }}>Ravi<animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.85;0.86;0.97;1" dur="7s" repeatCount="indefinite" /></text>
  <rect x="410" y="50" width="200" height="60" rx="10" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 2 }} />
  <text x="510" y="40" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>const name = ref("")</text>
  <text x="510" y="87" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--mono)", fontSize: 16 }}>"Asha"<animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.3;0.31;0.6;0.61;1" dur="7s" repeatCount="indefinite" /></text>
  <text x="510" y="87" textAnchor="middle" opacity="0" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--mono)", fontSize: 16 }}>"Ravi"<animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.61;0.62;1" dur="7s" repeatCount="indefinite" /></text>
  <path d="M232 70 H406" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} markerEnd="url(#vm-arrow)" />
  <path d="M408 92 H234" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} markerEnd="url(#vm-arrow)" />
  <text x="320" y="62" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--mono)", fontSize: 12 }}>@input</text>
  <text x="320" y="110" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--mono)", fontSize: 12 }}>:value</text>
  <circle r="7" cy="70" style={{ fill: "var(--ngc-orange)" }}><animate attributeName="cx" values="232;232;406;406" keyTimes="0;0.1;0.3;1" dur="7s" repeatCount="indefinite" /><animate attributeName="opacity" values="0;1;1;0;0" keyTimes="0;0.1;0.3;0.31;1" dur="7s" repeatCount="indefinite" /></circle>
  <circle r="7" cy="92" opacity="0" style={{ fill: "var(--ngc-green)" }}><animate attributeName="cx" values="408;408;234;234" keyTimes="0;0.62;0.84;1" dur="7s" repeatCount="indefinite" /><animate attributeName="opacity" values="0;0;1;1;0;0" keyTimes="0;0.61;0.62;0.84;0.85;1" dur="7s" repeatCount="indefinite" /></circle>
  <text x="320" y="155" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 13 }}>You type "Asha": the ref changes. The code sets "Ravi": the box changes.</text>
</svg>
<figcaption>v-model is a value binding plus an input listener.</figcaption>
</figure>

**Simple lesson:** `v-model` = `:value` + `@input`, written once.

## v-model on Every Control

- **Text box and text area:** binds the text.
- **One checkbox:** binds `true` or `false`.
- **Many checkboxes with one array:** the array holds the `value` of each ticked box.
- **Radio buttons:** binds the `value` of the chosen one.
- **Select:** binds the chosen option. Add `multiple` to bind an array.

const note = ref<string>("");
const takeaway = ref<boolean>(false);
const extras = ref<string[]>([]);
const spice = ref<string>("medium");
const drink = ref<string>("Lassi");
const sides = ref<string[]>([]);
</script>

<template>
    <textarea v-model="note" placeholder="Note for the chef"></textarea>
    <label><input type="checkbox" v-model="takeaway" /> Takeaway</label>
    <p>
      <label><input type="checkbox" value="chutney" v-model="extras" /> Chutney</label>
      <label><input type="checkbox" value="sambar" v-model="extras" /> Sambar</label>
    </p>
    <p>
      <label><input type="radio" value="mild" v-model="spice" /> Mild</label>
      <label><input type="radio" value="medium" v-model="spice" /> Medium</label>
      <label><input type="radio" value="hot" v-model="spice" /> Hot</label>
    </p>
    <select v-model="drink">
      <option disabled value="">Pick a drink</option>
      <option>Lassi</option>
      <option>Filter coffee</option>
    </select>
    <select v-model="sides" multiple>
      <option>Papad</option>
      <option>Pickle</option>
      <option>Curd</option>
    </select>
    <pre>{{ { note, takeaway, extras, spice, drink, sides } }}</pre>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

`v-model` ignores the `value`, `checked` and `selected` attributes written in the HTML. The ref is the one source of truth. Set the starting value in the script.

### Value Bindings

- By default a checkbox gives `true` or `false`, and a radio or option gives its text.
- `true-value` and `false-value` change what one checkbox gives.
- `:value` gives any JavaScript value, such as a number or an object.

const bill = ref<string>("no");
const size = ref<number>(1);
const table = ref<{ id: number; seats: number } | null>(null);

const tables = [
    { id: 1, seats: 2 },
    { id: 2, seats: 6 },
];
</script>

<template>
    <label><input type="checkbox" v-model="bill" true-value="yes" false-value="no" /> Print bill</label>
    <p>
      <label><input type="radio" :value="1" v-model="size" /> Half</label>
      <label><input type="radio" :value="2" v-model="size" /> Full</label>
    </p>
    <select v-model="table">
      <option :value="null">Any table</option>
      <option v-for="t in tables" :key="t.id" :value="t">Table {{ t.id }} ({{ t.seats }} seats)</option>
    </select>
    <p>bill = {{ bill }}, size = {{ size }} ({{ typeof size }}), table = {{ table }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**Simple lesson:** use `:value`, `true-value` and `false-value` when you need more than text.

## Modifiers

- `.lazy` = update on `change` (when you leave the box), not on every key.
- `.number` = turn the text into a number. Text that is not a number stays text.
- On `<input type="number">`, Vue applies `.number` for you.
- `.trim` = remove spaces at the start and the end.

const city = ref<string>("");
const plates = ref<number>(1);
const coupon = ref<string>("");
</script>

<template>
    <label>City (.lazy) <input v-model.lazy="city" /></label>
    <p>City: {{ city }}</p>
    <label>Plates (.number) <input v-model.number="plates" /></label>
    <p>Plates + 1 = {{ plates + 1 }}</p>
    <label>Coupon (.trim) <input v-model.trim="coupon" /></label>
    <p>Coupon: "{{ coupon }}"</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

- **What happened:** the city updated only after you left the box.
- `plates + 1` did maths, not text joining.
- The coupon lost its outer spaces.

**Simple lesson:** `.lazy` waits, `.number` makes numbers, `.trim` cuts spaces.

`.number` gives back the text when it cannot read a number, such as an empty box. Check with `typeof` or validation before doing maths with it.

## Submitting and Resetting

- Listen with `@submit.prevent` on the `<form>`, so the page does not reload.
- Read the refs. They already hold every value.
- To reset, put the starting values back into the refs.
- A `reactive` object is handy for many fields. Reset it with `Object.assign`.

const blank = { name: "", phone: "", guests: 2 };
const form = reactive({ ...blank });
const sent = ref<string>("");

function submit(): void {
    sent.value = \`Booked: \${form.name}, \${form.guests} guests, \${form.phone}\`;
    console.log("Sending", JSON.stringify(form));
    Object.assign(form, blank);
}
</script>

<template>
    <form @submit.prevent="submit">
      <input v-model.trim="form.name" placeholder="Name" />
      <input v-model.trim="form.phone" placeholder="Phone" />
      <input v-model.number="form.guests" type="number" min="1" />
      <button>Book</button>
    </form>
    <p>{{ sent }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

**Simple lesson:** `@submit.prevent`, read the state, then reset it.

## Validation

- **Validation** = checking input before you accept it.
- Common rules:
  - **required:** not empty
  - **length:** at least or at most some letters
  - **pattern:** matches a shape, such as 10 digits
  - **numeric:** a number in a range
  - **custom:** any rule you write, such as "not on Monday"
- Keep the errors in a `computed`. They update while people type.
- Turn the submit button off while there are errors.

const name = ref<string>("");
const phone = ref<string>("");
const guests = ref<number>(2);
const day = ref<string>("Tuesday");

const errors = computed(() => {
    const e: Record<string, string> = {};
    if (!name.value) e.name = "Name is required.";
    else if (name.value.length < 2) e.name = "Name needs at least 2 letters.";
    if (!/^\\d{10}$/.test(phone.value)) e.phone = "Enter a 10-digit phone number.";
    if (guests.value < 1 || guests.value > 12) e.guests = "Guests must be 1 to 12.";
    if (day.value === "Monday") e.day = "We are closed on Monday.";
    return e;
});

const valid = computed(() => Object.keys(errors.value).length === 0);
</script>

<template>
    <form @submit.prevent="console.log('Booked!')">
      <p><input v-model.trim="name" placeholder="Name" /> <small>{{ errors.name }}</small></p>
      <p><input v-model.trim="phone" placeholder="Phone" /> <small>{{ errors.phone }}</small></p>
      <p><input v-model.number="guests" type="number" /> <small>{{ errors.guests }}</small></p>
      <p>
        <select v-model="day"><option>Monday</option><option>Tuesday</option></select>
        <small>{{ errors.day }}</small>
      </p>
      <button :disabled="!valid">Book</button>
    </form>
</template>

<style>
small { color: crimson; }
</style>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

Show an error only after the person has touched a field, or tried to submit. Red text on an empty form before anyone types feels unfriendly. Keep a `touched` flag per field, set on `@blur`.

### Accessible Errors

- A screen reader must know a field is wrong and why.
- `aria-invalid="true"` marks the field as wrong.
- `aria-describedby="id"` links the field to its error text.
- Ids must be unique on the page. `useId()` (Vue 3.5) makes one for you.

const email = ref<string>("");
const touched = ref<boolean>(false);
const errorId = useId();

const error = computed(() =>
    touched.value && !email.value.includes("@") ? "Enter an email with @ in it." : "",
);
</script>

<template>
    <label>Email
      <input
        v-model.trim="email"
        :aria-invalid="error ? 'true' : 'false'"
        :aria-describedby="errorId"
        @blur="touched = true"
      />
    </label>
    <p :id="errorId" role="alert">{{ error }}</p>
</template>
`,
    "main.ts": `import { createApp } from "vue";

createApp(App).mount("#app");
`,
  }}
/>

### Validation Libraries

- Big forms use a library, so you do not write every rule by hand.
- **VeeValidate** = a Vue form library: fields, rules, touched state and errors.
- **Zod** = describes the shape of data once, then checks it.
- They are installed with npm in a real project. This course writes rules by hand to show the idea.

```ts
// A real project: npm install vee-validate zod @vee-validate/zod
import { useForm } from "vee-validate";
import { toTypedSchema } from "@vee-validate/zod";
import { z } from "zod";

const { errors, defineField } = useForm({
  validationSchema: toTypedSchema(
    z.object({ phone: z.string().regex(/^\d{10}$/, "Enter a 10-digit phone number.") }),
  ),
});
```

**Simple lesson:** keep errors in a computed, disable submit while there are errors, and link each error to its field.

## TL;DR

- **`@click="code"`** = short for `v-on:click`. Runs code on a click.
- **`$event`** = the event object, when you also pass arguments.
- **Key alias** = `@keyup.enter`, `.esc`, `.space`, `.up` ...
- **`.stop`** = stop bubbling to parents.
- **`.prevent`** = stop the browser's default action.
- **`v-model`** = two-way binding: `:value` plus `@input`.
- **`.lazy` / `.number` / `.trim`** = on change / as a number / no outer spaces.
- **Validation** = errors in a `computed`, `:disabled="!valid"`.
- **Accessible errors** = `aria-invalid`, `aria-describedby`, `useId()`.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| `@click="add()"` then reading the event | The event is still passed | Pass `$event`: `add($event)` | The event argument is undefined |
| Form without `.prevent` | Vue stops the reload | Write `@submit.prevent` | The page reloads and state is lost |
| Inner button triggers the card | Clicks stay on the button | Add `.stop` on the inner button | Two handlers run |
| `@click.ctrl` fires with Ctrl+Shift | It means Ctrl only | Add `.exact` | The handler runs on extra key combos |
| `value="..."` with `v-model` | The HTML value is the start value | Set the start value in the ref | The attribute is ignored |
| No `.number` | A number input gives a number | Add `.number` | `2 + 1` becomes `"21"` |
| Checkboxes with a string ref | Many boxes fill a string | Use an array ref | Only true or false is stored |
| No `.prevent` on submit | Vue stops the reload | `@submit.prevent` | The page reloads and the form clears |
| Errors shown before typing | Show every error at once | Wait for blur or a submit try | An unfriendly wall of red |

</div>

## Check Yourself

## Exercises

## What to Remember

- `@event` listens. Use a method name, or a call with arguments.
- Add `$event` when you pass arguments and still need the event.
- Key aliases and system keys pick exact keys. `.exact` rules out the rest.
- `v-model` binds a control and a ref both ways.
- Many checkboxes need an array. Use `:value` for non-text values.
- `.number` for numbers, `.trim` for clean text, `.lazy` to wait for change.

Next: **Components and Single-File Components**
