# Dynamic Rendering

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Rendering
Source: https://www.devtutorials.ai/learn/vue/rendering/dynamic-rendering

> Class bindings with objects and arrays, classes on components, style bindings with units and CSS variables, v-if, v-else-if and v-else, truthy and falsy conditions, how v-if destroys state, forcing a fresh copy with :key, template groups, v-show, and when to choose v-if or v-show.

## Changing the Look from State

- **Dynamic rendering** = the same template changes its look, or which elements it shows, when the state changes.
- A page often looks different depending on state.
- A sold-out dish is grey. A selected tab is bold. An error message appears only on errors.
- Vue does this with three tools:
  - `:class` and `:style` change how an element looks
  - `v-if` adds or removes an element
  - `v-show` hides or shows an element

**Simple lesson:** state decides how things look and whether they are there.

### A Real-World Example

- Meera's menu marks spicy dishes in red.
- Sold-out dishes turn grey, and their button disappears.
- Click the buttons to change the state and watch the card change.

const spicy = ref<boolean>(true);
const soldOut = ref<boolean>(false);
</script>

<template>
    <div class="card" :class="{ spicy: spicy, 'sold-out': soldOut }">
      <h3>Paneer Tikka</h3>
      <p v-if="soldOut">Sold out today</p>
      <button v-else>Order now</button>
    </div>
    <button @click="spicy = !spicy">Toggle spicy</button>
    <button @click="soldOut = !soldOut">Toggle sold out</button>
</template>

<style>
.card { border: 2px solid #ccc; border-radius: 10px; padding: 8px 12px; margin-bottom: 10px; }
.spicy { border-color: crimson; }
.sold-out { opacity: 0.45; }
</style>
`,
    "main.ts": `import { createApp } from "vue";

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

- **What happened:** `:class` added `spicy` and `sold-out` when they were true.
- `v-if` and `v-else` swapped the button for the "Sold out" text.

### What You Will Learn

1. How to switch classes with an object.
2. How to list classes with an array.
3. What happens to classes on a component.
4. How to set styles from state, with units and CSS variables.
5. How `v-if`, `v-else-if` and `v-else` work together, and what counts as true.
6. Why `v-if` resets state, and how `:key` forces a fresh copy.
7. How `v-show` is different.
8. When to pick `v-if` and when to pick `v-show`.

## Class Bindings

### Object Syntax

- `:class="{ name: condition }"` adds the class when the condition is true.
- You can list many classes in one object.
- Names with a dash need quotes: `'sold-out': soldOut`.
- A normal `class="..."` can sit next to `:class`. Vue joins them.

const selected = ref<string>("veg");
</script>

<template>
    <button class="tab" :class="{ active: selected === 'veg' }" @click="selected = 'veg'">Veg</button>
    <button class="tab" :class="{ active: selected === 'nonveg' }" @click="selected = 'nonveg'">Non-veg</button>
    <p>Showing: {{ selected }}</p>
</template>

<style>
.tab { margin-right: 6px; }
.active { background: #2f9e6e; color: white; font-weight: bold; }
</style>
`,
    "main.ts": `import { createApp } from "vue";

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

### Object from a computed

- Long class objects make the template hard to read.
- Move them into a `computed` value.

const stock = ref<number>(3);

const stockClass = computed(() => ({
    low: stock.value > 0 && stock.value < 5,
    empty: stock.value === 0,
}));
</script>

<template>
    <p :class="stockClass">Dosa batter left: {{ stock }}</p>
    <button @click="stock = Math.max(0, stock - 1)">Use one</button>
</template>

<style>
.low { color: darkorange; }
.empty { color: crimson; text-decoration: line-through; }
</style>
`,
    "main.ts": `import { createApp } from "vue";

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

### Array Syntax

- `:class="[a, b]"` adds the class names stored in `a` and `b`.
- You can mix in a choice: `[size, isNew ? 'new' : '']`.
- You can also put an object inside the array.

const size = ref<string>("big");
const isNew = ref<boolean>(true);
</script>

<template>
    <p :class="[size, { 'new-dish': isNew }]">Mango Lassi</p>
    <button @click="size = size === 'big' ? 'small' : 'big'">Change size</button>
    <button @click="isNew = !isNew">Toggle new</button>
</template>

<style>
.big { font-size: 22px; }
.small { font-size: 13px; }
.new-dish::after { content: " NEW"; color: #2f9e6e; font-weight: bold; }
</style>
`,
    "main.ts": `import { createApp } from "vue";

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

### Classes on Components

- You can put `class` or `:class` on your own component, too.
- Vue adds those classes to the component's root element.
- If the component has **several** root elements, Vue does not know where to put them. The class is dropped, and Vue warns. The child must place it with `v-bind="$attrs"`.
- You will learn more in **Slots, Attributes and Provide/Inject**.

</script>

<template>
</template>

<style>
.hot { background: crimson; color: white; }
</style>
`,
    "Badge.vue": `<template>
    <span class="badge">Chef's pick</span>
</template>

<style>
.badge { padding: 2px 8px; border-radius: 6px; border: 1px solid #999; }
</style>
`,
    "main.ts": `import { createApp } from "vue";

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

- **What happened:** the `span` got both `badge` and `hot`.

**Simple lesson:** use an object to switch classes on and off. Use an array to list them.

With `<style scoped>`, a parent's class on a child component still styles the child's root element. Inner elements need `:deep()`. You will see this in **Components and Single-File Components**.

## Style Bindings

- `:style` sets inline styles from state.
- Use an object. Write names in camelCase (`fontSize`) or in quotes (`'font-size'`).
- **Add units.** `{ width: size }` with a plain number does nothing. Write `size + 'px'` or a template string.
- An array joins many style objects.
- Vue adds browser prefixes for you when a property needs them.

const heat = ref<number>(1);
const base = { padding: "6px 10px", borderRadius: "8px" };
</script>

<template>
    <p :style="[base, { background: heat > 2 ? 'crimson' : 'gold', fontSize: 12 + heat * 4 + 'px' }]">
      Spice level {{ heat }}
    </p>
    <button @click="heat = heat < 4 ? heat + 1 : 1">Hotter</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

Prefer `:class` over `:style`. Classes keep the CSS in one place. Use `:style` for values that really come from data, such as a width in pixels or a colour picked by the user.

### CSS Variables from State

- A **CSS variable** (custom property) has a name starting with `--`, like `--accent`.
- Set it with `:style="{ '--accent': colour }"` on a parent.
- Every rule inside can use it with `var(--accent)`. One value themes many elements.

const width = ref<number>(120);
const accent = ref<string>("#2f9e6e");
</script>

<template>
    <div class="card" :style="{ '--accent': accent }">
      <div class="bar" :style="{ width: width + 'px' }">{{ width }}px</div>
      <button>Order</button>
      <p class="note">Every accent here comes from one variable.</p>
    </div>
    <input type="range" min="60" max="260" v-model.number="width" />
    <input type="color" v-model="accent" />
</template>

<style>
.card { border: 2px solid var(--accent); padding: 10px; border-radius: 10px; }
.bar { background: var(--accent); color: white; padding: 4px; }
.card button { background: var(--accent); color: white; border: 0; padding: 4px 10px; margin-top: 6px; }
.note { color: var(--accent); }
</style>
`,
    "main.ts": `import { createApp } from "vue";

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

- **What happened:** the slider changed one width. The colour picker changed one variable, and the border, bar, button and text all followed.

**Simple lesson:** `:style` takes an object of CSS properties (with units), or an array of them. CSS variables theme many elements at once.

## Conditional Rendering

### v-if, v-else-if and v-else

- `v-if="condition"` adds the element only when the condition is true.
- `v-else-if` gives another condition to try.
- `v-else` is used when nothing above was true.
- They must sit right after each other, with nothing in between.

const hour = ref<number>(8);
</script>

<template>
    <p>Time: {{ hour }}:00</p>
    <p v-if="hour < 11">Breakfast menu</p>
    <p v-else-if="hour < 16">Lunch menu</p>
    <p v-else-if="hour < 22">Dinner menu</p>
    <p v-else>Kitchen closed</p>
    <button @click="hour = (hour + 3) % 24">Three hours later</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

### Grouping with template

- To show or hide many elements together, wrap them in `<template v-if>`.
- The `<template>` tag itself is not added to the page.

const loggedIn = ref<boolean>(false);
</script>

<template>
    <template v-if="loggedIn">
      <h3>Welcome back, Meera</h3>
      <p>You have 3 new orders.</p>
    </template>
    <p v-else>Please log in.</p>
    <button @click="loggedIn = !loggedIn">{{ loggedIn ? "Log out" : "Log in" }}</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

### What Counts as True

- The condition does not have to be `true` or `false`. Vue checks if it is **truthy**.
- **Falsy** values: `false`, `0`, `""` (empty text), `null`, `undefined` and `NaN`. Everything else is truthy, even `"0"`, `[]` and `{}`.
- Trap: `v-if="count"` hides the element when the count is **0**.
- Trap: `v-if="items"` is true even for an **empty** array. Check `items.length` instead.
- Write the real question: `count > 0`, `items.length > 0`, `name !== ''`.

const count = ref<number>(0);
const items = ref<string[]>([]);
</script>

<template>
    <p v-if="count">A: v-if="count" → shown</p>
    <p v-if="count >= 0">B: v-if="count >= 0" → {{ count }} orders</p>
    <p v-if="items">C: v-if="items" → shown, even when empty!</p>
    <p v-if="items.length === 0">D: v-if="items.length === 0" → the cart is empty</p>
    <button @click="count++">Add an order</button>
    <button @click="items.push('Dosa')">Add to cart</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

- **What happened:** line A was missing while the count was 0. Line C showed for an empty array.

### v-if Destroys and Rebuilds

- When a `v-if` turns false, Vue **destroys** the element and every component inside it.
- Their state is gone: typed text, counters, timers. `onUnmounted` runs.
- When it turns true again, Vue builds a **new** copy. `onMounted` runs again, and the state starts fresh.
- `v-show` keeps the same copy, so its state survives.
- Use this on purpose: `v-if` to reset a form, `v-show` to keep what the user typed.

const props = defineProps({ label: { type: String, required: true } });
const note = ref<string>("");

onMounted(() => console.log(\`\${props.label}: mounted\`));
onUnmounted(() => console.log(\`\${props.label}: destroyed\`));
</script>

<template>
    <p>{{ label }} <input v-model="note" placeholder="Type here" /></p>
</template>
`,
    "App.vue": `<script setup lang="ts">

const open = ref<boolean>(true);
</script>

<template>
    <button @click="open = !open">{{ open ? "Hide" : "Show" }} both</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

- Type in both boxes, then press **Hide** and **Show**.
- **What happened:** the `v-if` box came back empty, and the Console shows it was destroyed and mounted again. The `v-show` box kept your text.

### Forcing a Fresh Copy with :key

- In Vue 3, each `v-if` / `v-else` branch gets its own key automatically. Two branches are always two separate elements.
- (Old Vue 2 tutorials say you must add `key` to `v-if` branches. In Vue 3 you do not.)
- But when the **same** element stays on the page and only its data changes, Vue keeps the old copy and its state.
- Give it `:key="value"`. When the value changes, Vue destroys the old copy and builds a fresh one.
- Typical use: reset a form when the user picks another table, order or customer.

const props = defineProps({ table: { type: Number, required: true } });
const note = ref<string>("");

onMounted(() => console.log(\`form for table \${props.table}: fresh copy\`));
</script>

<template>
    <p>Table {{ table }} note: <input v-model="note" placeholder="Less spicy" /></p>
</template>
`,
    "App.vue": `<script setup lang="ts">

const table = ref<number>(1);
</script>

<template>
    <h4>Without key: the note stays</h4>

    <h4>With :key="table": a fresh form per table</h4>

    <button @click="table++">Next table</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

- Type a note in both forms, then press **Next table**.
- **What happened:** the first form kept the old table's note. The keyed form came back empty, and the Console shows a fresh copy.

**Simple lesson:** `v-if` adds or removes elements, and a removed element loses its state. Write clear conditions, and change `:key` to get a fresh copy.

## v-show

- `v-show="condition"` always keeps the element on the page.
- It only switches `display: none` on and off.
- It does not work with `<template>` or with `v-else`.

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 200" role="img" aria-labelledby="fig-vue-ifshow" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-ifshow">Animation: when the condition turns false, v-if removes the element from the page tree, while v-show keeps the element but sets display none</title>
  <g style={{ fontFamily: "var(--sans)", fontSize: 14, fontWeight: 700 }}>
    <text x="160" y="24" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>v-if</text>
    <text x="480" y="24" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>v-show</text>
  </g>
  <g style={{ fontFamily: "var(--mono)", fontSize: 13 }}>
    <rect x="100" y="40" width="120" height="34" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="160" y="62" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>div</text>
    <path d="M160 74 V104" style={{ stroke: "var(--ngc-blue)", strokeWidth: 2 }}><animate attributeName="opacity" values="1;1;0;0;1" keyTimes="0;0.45;0.5;0.95;1" dur="6s" repeatCount="indefinite" /></path>
    <g><animate attributeName="opacity" values="1;1;0;0;1" keyTimes="0;0.45;0.5;0.95;1" dur="6s" repeatCount="indefinite" /><rect x="100" y="104" width="120" height="34" rx="8" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }} /><text x="160" y="126" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>p (menu)</text></g>
    <rect x="420" y="40" width="120" height="34" rx="8" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }} /><text x="480" y="62" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>div</text>
    <path d="M480 74 V104" style={{ stroke: "var(--ngc-purple)", strokeWidth: 2 }} />
    <rect x="420" y="104" width="120" height="34" rx="8" style={{ fill: "var(--ngc-win)", stroke: "var(--ngc-purple)", strokeWidth: 1.5, strokeDasharray: "0" }}><animate attributeName="stroke-dasharray" values="0;0;5 4;5 4;0" keyTimes="0;0.45;0.5;0.95;1" dur="6s" repeatCount="indefinite" /></rect>
    <text x="480" y="126" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>p (menu)</text>
    <text x="480" y="160" textAnchor="middle" opacity="0" style={{ fill: "var(--ngc-orange)", fontSize: 12 }}><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.45;0.5;0.95;1" dur="6s" repeatCount="indefinite" />style="display: none"</text>
    <text x="160" y="160" textAnchor="middle" opacity="0" style={{ fill: "var(--ngc-orange)", fontSize: 12 }}><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.45;0.5;0.95;1" dur="6s" repeatCount="indefinite" />removed from the page</text>
  </g>
  <text x="320" y="190" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 13 }}>The condition turns false: v-if removes, v-show hides.</text>
</svg>
<figcaption>v-if takes the element out. v-show keeps it but hides it.</figcaption>
</figure>

const open = ref<boolean>(true);

function count(): void {
    console.log("v-if items on page:", document.querySelectorAll(".by-if").length);
    console.log("v-show items on page:", document.querySelectorAll(".by-show").length);
}
</script>

<template>
    <p v-if="open" class="by-if">Menu (v-if)</p>
    <p v-show="open" class="by-show">Menu (v-show)</p>
    <button @click="open = !open">Toggle</button>
    <button @click="count">Count elements</button>
</template>
`,
    "main.ts": `import { createApp } from "vue";

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

- **What happened:** after a toggle, both lines disappear.
- But "Count elements" shows the `v-show` line is still on the page, just hidden.

### Which One to Pick

<div className="tscroll">

| Point | `v-if` | `v-show` |
|---|---|---|
| When false | Element is removed | Element stays, `display: none` |
| Start cost | Cheap if false at the start | Always builds the element |
| Toggle cost | Higher: build and remove | Low: just a style change |
| Works with `v-else` and `<template>` | Yes | No |
| State inside when hidden | Lost, rebuilt fresh | Kept |
| Best for | Rarely changes, or should reset | Changes often, like a dropdown |

</div>

Do not put `v-if` and `v-for` on the same element. You will see why in **List Rendering**.

**Simple lesson:** `v-if` for things that rarely change. `v-show` for things that toggle often.

## TL;DR

- **`:class="{ a: cond }"` / `:class="[x, y]"`** = switch classes on and off / list class names.
- **Class on a component** = goes to the component's root element.
- **`:style="{ width: w + 'px', '--accent': c }"`** = inline styles and CSS variables. Add units.
- **`v-if` / `v-else-if` / `v-else`** = add or remove elements. Must come right after each other.
- **Truthy** = `0`, `""`, `null`, `undefined` are false. `[]` and `{}` are true.
- **`v-if` and state** = false destroys the element and its state. A new `:key` forces a fresh copy.
- **`<template v-if>`** = groups elements without adding a tag.
- **`v-show`** = keeps the element, toggles `display: none`.
- **Pick** = `v-if` for rare changes, `v-show` for frequent toggles.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Dash class without quotes | `{ sold-out: x }` is fine | Write `{ 'sold-out': x }` | A compile error |
| `class="{ active: x }"` | Plain class reads objects | Use `:class` | The class is the text `{ active: x }` |
| Something between `v-if` and `v-else` | `v-else` finds its `v-if` anywhere | Put them right next to each other | A compile error |
| `v-show` with `v-else` | They work together | Use `v-if` with `v-else` | Compile error: `v-else` has no adjacent `v-if` |
| `v-if="count"` for a number | Any number is true | Write `count > 0` | Hidden when the count is 0 |
| `v-if="items"` for a list | Empty lists are false | Check `items.length` | Shown even when empty |
| `{ width: size }` | Vue adds px | Write `size + 'px'` | The style is ignored |
| Same component, new data, old state | New props reset the state | Add `:key` that changes with the data | Old typed text stays |
| `v-show` on `<template>` | It hides the group | Use `v-if`, or a real wrapper element | Nothing is hidden |
| `v-if` on a fast toggle | It is always best | Use `v-show` for frequent toggles | Extra work on every toggle |
| Long objects in the template | It is fine in the template | Move them to a computed | Hard to read |

</div>

## Check Yourself

## Exercises

## What to Remember

- `:class` takes an object (switch on and off) or an array (list of names).
- `:style` takes an object of CSS properties, or an array of objects.
- `v-if` adds and removes. `v-else-if` and `v-else` must follow it directly.
- `<template v-if>` groups elements without an extra tag.
- `v-show` hides with CSS. Use it for frequent toggles.

Next: **List Rendering**
