Hone logo
Hone
Problems

Implementing Dynamic Content with Vue Slot Props

Vue's slots are a powerful mechanism for passing content from a parent component to a child component. Slot props take this further by allowing the child component to pass data back to the parent component within the slot's scope. This challenge focuses on implementing and utilizing slot props to create flexible and reusable components.

Problem Description

You are tasked with building a reusable Card component in Vue that can display a title, a footer, and custom content in its body. The Card component should accept the title and footer content directly as props. However, the main content area of the card should be powered by a slot that accepts props. This means the parent component will provide the content for the card's body, and the Card component itself will be able to pass data (like a theme or a loading state) to that slot content.

Key Requirements:

  1. Card Component Structure:
    • It should have a predefined structure (e.g., a div with classes for title, body, and footer).
    • Accepts title and footer as plain string props.
  2. Named Slot for Body:
    • Use a named slot (e.g., #default or #body) for the main content area.
  3. Slot Props from Card:
    • The Card component should expose at least one prop to its slot. For this challenge, let's make it a theme prop (e.g., 'light' or 'dark').
  4. Using Slot Props in Parent:
    • The parent component should be able to receive the theme prop from the Card component within the slot's scope.
    • The parent component should conditionally render its slot content based on the theme prop.
  5. Basic Styling:
    • Apply minimal CSS to visually distinguish the title, body, and footer, and to demonstrate the theme prop (e.g., changing background color).

Expected Behavior:

When a parent component uses the Card component, it should be able to:

  • Pass a simple string for the title.
  • Pass a simple string for the footer.
  • Provide custom content for the card's body, which can then react to the theme prop passed down by the Card component.

Edge Cases to Consider:

  • What happens if the title or footer props are not provided? (They should gracefully render empty or with default content).
  • Ensure the theme prop is correctly passed and received.

Examples

Example 1: Basic Usage

Parent Component (App.vue)

<template>
  <div>
    <Card title="Welcome!" footer="Learn more">
      <template #default="{ theme }">
        <p :class="theme">This is the main content of the card.</p>
        <p>Current theme: {{ theme }}</p>
      </template>
    </Card>
  </div>
</template>

<script setup lang="ts">
import Card from './Card.vue';
</script>

<style scoped>
/* Basic styling for demonstration */
.card {
  border: 1px solid #ccc;
  padding: 15px;
  margin-bottom: 20px;
  border-radius: 5px;
}
.card-title {
  font-weight: bold;
  margin-bottom: 10px;
}
.card-footer {
  font-size: 0.9em;
  color: #666;
  margin-top: 10px;
}

/* Theme specific styles */
.light {
  background-color: #f9f9f9;
  color: #333;
}
.dark {
  background-color: #333;
  color: #f9f9f9;
}
</style>

Card Component (Card.vue)

<template>
  <div class="card">
    <div class="card-title">{{ title }}</div>
    <div class="card-body">
      <slot :theme="currentTheme">
        <!-- Fallback content if no slot is provided -->
        <p>Default content.</p>
      </slot>
    </div>
    <div class="card-footer">{{ footer }}</div>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue';

interface Props {
  title?: string;
  footer?: string;
}
defineProps<Props>();

// In a real app, this might come from context or another prop
const currentTheme = computed(() => 'light'); // For this example, always 'light'
</script>

<style scoped>
/* Styling within Card component as well */
.card {
  border: 1px solid #ccc;
  padding: 15px;
  margin-bottom: 20px;
  border-radius: 5px;
}
.card-title {
  font-weight: bold;
  margin-bottom: 10px;
}
.card-footer {
  font-size: 0.9em;
  color: #666;
  margin-top: 10px;
}
</style>

Output (Conceptual):

The Card component will render with "Welcome!" as the title, "Learn more" as the footer, and the provided paragraph content. The paragraph will have the class light applied to it, changing its background and text color according to the light theme styles defined in App.vue. The "Current theme: light" text will also be displayed.

Example 2: Dark Theme

Parent Component (App.vue - modification)

<template>
  <div>
    <Card title="Advanced Settings" footer="Save changes">
      <template #default="{ theme }">
        <div :class="theme" style="padding: 10px; border-radius: 3px;">
          <p>Here are some advanced options.</p>
          <p>The applied theme is: {{ theme }}</p>
        </div>
      </template>
    </Card>
  </div>
</template>

<script setup lang="ts">
import Card from './Card.vue';
</script>

<style scoped>
/* ... (same styles as Example 1) ... */
</style>

Card Component (Card.vue - modification to expose 'dark' theme)

<template>
  <div class="card">
    <div class="card-title">{{ title }}</div>
    <div class="card-body">
      <slot :theme="currentTheme">
        <p>Default content.</p>
      </slot>
    </div>
    <div class="card-footer">{{ footer }}</div>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue';

interface Props {
  title?: string;
  footer?: string;
  themeProp?: 'light' | 'dark'; // Added prop to control theme from parent if needed
}
const props = defineProps<Props>();

// In a real app, this might come from context or another prop
// For this example, we'll allow the parent to suggest a theme, but Card decides
const currentTheme = computed(() => props.themeProp === 'dark' ? 'dark' : 'light');
</script>

<style scoped>
/* ... (same styles as Example 1) ... */
</style>

Output (Conceptual):

Modify App.vue to pass themeProp="dark" to the Card component:

<template>
  <div>
    <Card title="Advanced Settings" footer="Save changes" themeProp="dark">
      <template #default="{ theme }">
        <div :class="theme" style="padding: 10px; border-radius: 3px;">
          <p>Here are some advanced options.</p>
          <p>The applied theme is: {{ theme }}</p>
        </div>
      </template>
    </Card>
  </div>
</template>

The Card component will now render with a dark theme applied to its slot content. The div inside the slot in App.vue will have the dark class, resulting in a dark background and light text for the "Advanced Settings" card. The output will show "The applied theme is: dark".

Constraints

  • Vue Version: Vue 3 with the Composition API.
  • Language: TypeScript.
  • The Card component must be a standalone, reusable component.
  • The theme prop exposed by the slot should be a string literal type: 'light' | 'dark'.

Notes

  • Think about how to define the slot's interface in TypeScript for better type safety.
  • Consider how a component might dynamically decide which theme to pass down. For this challenge, you can hardcode it in Card.vue or derive it from a prop passed to Card.
  • The goal is to demonstrate the concept of slot props, so keep the styling and logic relatively simple.
  • Focus on the <template> structure and the <script setup> for type safety and composition.
Loading editor...
typescript