#vue #javascript #frontend

Day 8: Slots & Dynamic Components

Welcome to Day 8! Props are great for passing data, but what if you want to pass HTML or template content? That’s where Slots come in.

Slots: Content Projection

Imagine a <Card> component. You want it to wrap whatever you put inside it with a nice shadow and border.

Child (Card.vue)

<template>
  <div class="card">
    <!-- The content from parent goes here -->
    <slot>Default content if nothing provided</slot>
  </div>
</template>

<style scoped>
.card { border: 1px solid #ddd; padding: 20px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
</style>

Parent (App.vue)

<Card>
  <h2>My Title</h2>
  <img src="..." />
  <button>Click me</button>
</Card>

Named Slots

What if a component has multiple “holes”? (e.g., Header, Body, Footer).

Child (Layout.vue)

<template>
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot> <!-- Default slot -->
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</template>

Parent

<Layout>
  <template #header>
    <h1>The Header</h1>
  </template>

  <p>The main content...</p>

  <template #footer>
    <p>Copyright 2025</p>
  </template>
</Layout>

Note: # is shorthand for v-slot:.

Dynamic Components

Sometimes you want to switch between components based on a variable (like Tabs).

<script setup lang="ts">
import { ref } from 'vue'
import Home from './Home.vue'
import About from './About.vue'
import Contact from './Contact.vue'

// Shallow Ref is better for components to avoid Reactivity overhead
import { shallowRef } from 'vue'

const activeComponent = shallowRef(Home)
</script>

<template>
  <button @click="activeComponent = Home">Home</button>
  <button @click="activeComponent = About">About</button>
  <button @click="activeComponent = Contact">Contact</button>

  <div class="view">
    <!-- Special built-in element -->
    <component :is="activeComponent" />
  </div>
</template>

KeepAlive

By default, when you switch away from Home, it is destroyed. If you want to “cache” it (keep scroll position, form input):

<KeepAlive>
  <component :is="activeComponent" />
</KeepAlive>

Challenge for Day 8

  1. Create a Modal component.
  2. It should have two slots: header and body.
  3. Add a generic “Close” button in the Modal that emits a close event.
  4. In Parent, start with a button “Show Modal”.
  5. Clicking it shows the Modal (via v-if). Clicking “Close” inside Modal hides it.

Solution:

Child (Modal.vue)

<script setup lang="ts">
defineEmits(['close'])
</script>

<template>
  <div class="overlay" @click="$emit('close')">
    <div class="modal" @click.stop>
      <header>
        <slot name="header">Default Header</slot>
      </header>
      <div class="body">
        <slot></slot>
      </div>
      <button @click="$emit('close')">Close</button>
    </div>
  </div>
</template>

<style scoped>
.overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: grid; place-items: center; }
.modal { background: white; padding: 2rem; border-radius: 8px; }
</style>

Parent

<script setup lang="ts">
import { ref } from 'vue'
import Modal from './Modal.vue'

const show = ref(false)
</script>

<template>
  <button @click="show = true">Open Modal</button>

  <Teleport to="body">
    <Modal v-if="show" @close="show = false">
      <template #header>My Custom Modal</template>
      <p>This is the important message content.</p>
    </Modal>
  </Teleport>
</template>

Wait, what is <Teleport>? It moves the Modal to the <body> tag so styles don’t conflict! A free bonus tip for today. Tomorrow, we clean up logic with Composables.