Templates That React
Vue templates are HTML with a small set of superpowers called directives - attributes starting
with v- that tell the compiler "this part is dynamic." There are fewer than you'd think, and five
of them cover essentially all daily work. This phase is that five, plus the gotcha each one carries.
Text and attributes: {{ }} and :
{{ product.name }} <!-- text interpolation -->
<!-- attribute binding -->
Buy
What just happened: {{ }} drops a reactive expression into text. For attributes, mustaches
don't work - src="{{ url }}" is a beginner classic that sets the literal string. Attributes bind
with the : prefix (shorthand for v-bind:): :src="product.imageUrl" means "this attribute's
value is a JavaScript expression, keep it in sync."
Everything inside {{ }} and : is a real expression - {{ price * quantity }},
:class="{ active: isActive }" (that object form toggles CSS classes by boolean, and you'll use it
weekly). Statements don't fit ({{ if (x) ... }} is a compile error); anything beyond a simple
expression belongs in a computed (phase 3).
Events: @
Add one
Add to cart
...
What just happened: @click (shorthand for v-on:click) attaches a handler - either an inline
expression or a function to call. The third line shows a modifier: .prevent is
event.preventDefault() as a suffix, so form-submit handlers don't start with boilerplate. The
modifiers you'll actually use: .prevent, .stop (stopPropagation), and key modifiers like
@keyup.enter="search".
📝 Terminology: contrast note for React readers - there's no "pass a function, don't call it"
trap here. @click="addToCart(product.id)" compiles to a handler that runs the expression on
click, because templates are compiled, not evaluated inline. One classic bug that doesn't exist in
this dialect.
Branching: v-if and v-show
Your cart is empty.
{{ cart.length }} items.
Bulk order!
...expensive details panel...
Two ways to hide things, with a real difference:
v-ifremoves the element from the DOM entirely. False means gone - not rendered, listeners detached, child component state destroyed (it unmounts, same semantics as conditional rendering anywhere).v-showalways renders but togglesdisplay: none. The element stays alive, state intact.
The rule of thumb: v-if for branches that rarely change or shouldn't exist when false (auth-gated
UI, error states); v-show for things toggled frequently where you want the flip to be instant and
the state preserved (tabs, dropdowns).
Lists: v-for and the :key contract
{{ todo.text }}
What just happened: one <li> per array item. The :key is not decoration: when the array
changes, Vue matches old and new items by key to decide what moved versus what's new - reuse the
moved, build the new, delete the gone. Give it a stable identity (todo.id), never the array index
for lists that can reorder or delete: index keys make Vue reuse the wrong element's DOM (and any
state attached to it, like a checkbox or an input) for a different item.
⚠️ Gotcha: v-if and v-for on the same element is a lint error in Vue 3 - v-if runs
first and can't see the loop variable, which surprises everyone. Filter in a computed instead
(doneTodos from phase 3), or nest: <template v-for="..."> wrapping an inner v-if. The
<template> tag renders nothing itself - it's the grouping element for exactly these cases.
Two-way forms: v-model
The pattern every form needs - value flows into the input, keystrokes flow back into state - has a dedicated directive:
I agree
Free
Pro
Signing up {{ email || '…' }} for the {{ plan }} plan.
What just happened: v-model="email" is sugar for :value="email" plus @input writing back -
binding in both directions with one attribute. It adapts per element: checkboxes bind the boolean
checked, selects bind the chosen value. Modifiers ride along here too: v-model.number casts
the string input to a number, v-model.trim strips whitespace, v-model.lazy syncs on change
instead of every keystroke.
Contrast note: this is the controlled-input pattern with the wiring pre-soldered. The trade is the
usual one for sugar - less typing, and one more layer to see through when something misbehaves.
Phase 4 opens the lid and shows exactly what v-model expands to, because the same directive works
on your own components once you know its parts.
Recap
{{ expr }}for text;:attr="expr"for attributes - mustaches never work inside attributes.@event="handler-or-expression", with modifiers (.prevent,.stop,@keyup.enter) for the boilerplate.v-ifunmounts (state dies);v-showhides with CSS (state survives). Choose by toggle frequency and whether state should persist.v-forneeds:keywith stable identity - index keys corrupt row state on reorder/delete.v-model= value-down + event-up in one attribute;.number,.trim,.lazyrefine it.
[
{
"q": "A tab panel with a search input inside it is toggled with v-if. Every time the user switches away and back, the typed search is gone. Why?",
"choices": [
"v-if clears form fields for security reasons",
"False v-if unmounts the element - its state is destroyed, not hidden; v-show would preserve it",
"The input is missing a :key attribute",
"v-model resets refs when the template re-renders"
],
"answer": 1,
"why": [
"There's no security policy involved - unmounting is just what v-if means.",
null,
":key identifies items in lists; a lone input doesn't need one and it wouldn't prevent the unmount.",
"v-model faithfully reflects the ref - but this input's DOM (and the component state around it) ceased to exist."
],
"explain": "v-if = element removed from the DOM, state and all. For frequently-toggled UI whose state should survive, v-show hides with display:none and keeps everything alive."
},
{
"q": "Why does <img src=\"{{ imageUrl }}\"> show a broken image?",
"choices": [
"Mustache syntax doesn't work inside attributes - the browser received the literal text as the URL; it needs :src=\"imageUrl\"",
"The image path must be absolute in Vue templates",
"img tags need v-model for dynamic sources",
"The ref wasn't unwrapped with .value in the template"
],
"answer": 0,
"why": [
null,
"Relative paths work fine once the binding is real.",
"v-model is for two-way form binding - an image source only flows one way, via :src.",
"Templates auto-unwrap refs - .value is a script-side concern (phase 3)."
],
"explain": "Interpolation is for text content only. Attributes bind with the : prefix, which evaluates the expression and keeps the attribute in sync."
}
]
← Phase 1: What Vue Actually Is · Guide overview · Phase 3: Reactivity for Real →
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 2 questions
1. A tab panel with a search input inside it is toggled with v-if. Every time the user switches away and back, the typed search is gone. Why?
2. Why does <img src="{{ imageUrl }}"> show a broken image?