Dynamic Content Injection with Functional Slots in Vue.js
Vue.js slots are a powerful mechanism for component composition, allowing parent components to inject content into child components. This challenge focuses on a more advanced use case: using functional slots to dynamically render content based on data or logic provided by the parent. This is particularly useful when you need to control the presentation of injected content in a granular way.
Problem Description
Your task is to create a Card component in Vue.js using TypeScript that accepts a functional slot. This functional slot will be provided by the parent component and will receive specific data to render dynamic content within the Card.
Requirements:
-
CardComponent:- The
Cardcomponent should accept a single named slot, let's call itcontent. - This
contentslot should be a functional slot. This means the parent component will pass a function as the slot's content. - The
Cardcomponent should call this function and render whatever it returns. - The
Cardcomponent should also accept atitleprop (string) which will be displayed prominently.
- The
-
Functional Slot Implementation:
- The parent component will define a function that takes an object of data as an argument.
- This function will return a Vue render function or a VNode.
- The
Cardcomponent will pass specific data (e.g.,item: { id: number; name: string }) to this function.
-
Parent Component Usage:
- The parent component will use the
Cardcomponent. - It will provide a
titleprop to theCard. - It will define a function for the
contentslot that utilizes the data passed from theCardto render specific elements (e.g., a list item with an ID and name).
- The parent component will use the
Expected Behavior:
The Card component should display its title and then render the content generated by the functional slot. The functional slot, when called by the Card component, should use the provided data to dynamically generate and return the desired VNodes.
Edge Cases:
- What happens if the
contentslot is not provided? TheCardshould render without any content below the title. - Consider how to handle cases where the data passed to the functional slot might be null or undefined. (For this challenge, assume valid data will be passed.)
Examples
Example 1: Basic Usage
Parent Component (Vue Template - TypeScript):
<template>
<div>
<Card title="User Information">
<template #content="{ item }">
{{ item.name }} (ID: {{ item.id }})
</template>
</Card>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import Card from './Card.vue'; // Assume Card.vue is in the same directory
export default defineComponent({
components: {
Card,
},
data() {
return {
userData: { id: 1, name: 'Alice' },
};
},
// The function passed to the slot will receive `userData`
// but the Card component is responsible for passing it.
// For simplicity in the template, we show the result.
// The actual implementation will involve passing the data to the render function.
});
</script>
Card.vue (Conceptual):
<template>
<div class="card">
<h2>{{ title }}</h2>
<div class="card-content">
<!-- This is where the functional slot's output will be rendered -->
<slot name="content" :item="dataToPassToSlot"></slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, h, renderSlot } from 'vue';
interface UserData {
id: number;
name: string;
}
export default defineComponent({
props: {
title: {
type: String,
required: true,
},
},
// This is a simplified view of how the data is passed.
// The core challenge is understanding how the `slot` API handles functions.
setup(props, { slots }) {
const dataToPassToSlot = { id: 1, name: 'Alice' }; // Data from the Card or its own state
// The actual rendering logic for the slot happens when Vue processes the template.
// The key is that the `slot` component itself can accept props, which are then passed to the provided slot function.
return {};
},
});
</script>
Expected Output (Rendered HTML):
<div class="card">
<h2>User Information</h2>
<div class="card-content">
Alice (ID: 1)
</div>
</div>
Explanation: The Card component receives a title. The parent provides a content slot. Crucially, the Card component exposes data (e.g., dataToPassToSlot) via the v-bind on the slot tag. The parent's slot function (implicitly defined by the template content using item) receives this data and renders it.
Example 2: Using a Render Function for the Slot
Parent Component (App.vue - TypeScript):
<template>
<div>
<Card title="Product Details">
<template #content="slotProps">
<ProductDisplay :product="slotProps.product" />
</template>
</Card>
</div>
</template>
<script lang="ts">
import { defineComponent, h, PropType, computed } from 'vue';
import Card from './Card.vue';
import ProductDisplay from './ProductDisplay.vue'; // A separate component
interface Product {
id: number;
name: string;
price: number;
}
export default defineComponent({
components: {
Card,
ProductDisplay,
},
setup() {
const productData: Product = { id: 101, name: 'Laptop', price: 1200 };
// The `content` slot here directly receives `productData`
// via the `product` prop bound to the slot.
return {
productData,
};
},
});
</script>
Card.vue (TypeScript - Render Function approach):
<template>
<div class="card">
<h2>{{ title }}</h2>
<div class="card-content">
<!--
The slot component in Vue 3 can be used to render content.
The key here is how we pass data to the slot.
The `slots.content` itself is a function that can be called.
We pass `product` as a prop to this slot function.
-->
<slot name="content" :product="productData"></slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, h } from 'vue';
interface Product {
id: number;
name: string;
price: number;
}
export default defineComponent({
props: {
title: {
type: String,
required: true,
},
},
setup(props) {
// Data that the Card component wants to make available to the slot
const productData: Product = { id: 101, name: 'Laptop', price: 1200 };
return {
productData,
};
},
});
</script>
ProductDisplay.vue (for demonstration):
<template>
<div>
<h3>{{ product.name }}</h3>
<p>Price: ${{ product.price }}</p>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
interface Product {
id: number;
name: string;
price: number;
}
export default defineComponent({
props: {
product: {
type: Object as PropType<Product>,
required: true,
},
},
});
</script>
Expected Output (Rendered HTML):
<div class="card">
<h2>Product Details</h2>
<div class="card-content">
<div>
<h3>Laptop</h3>
<p>Price: $1200</p>
</div>
</div>
</div>
Explanation: The Card component passes productData to the content slot via :product="productData". The parent component's slot template then uses this product prop (slotProps.product) and passes it to a ProductDisplay component. This demonstrates passing complex data structures and using them within the slot's rendered output.
Constraints
- Vue.js 3 is required.
- TypeScript must be used for all component definitions.
- The
Cardcomponent should be a standard.vuefile component. - The functional slot itself will be implemented using the
<template #slotName="..."syntax. - Focus on the mechanism of passing data from the child (
Card) to the parent's slot function and rendering that function's output within the child.
Notes
- This challenge requires understanding how Vue's slot API works, specifically how to bind data to slots that are then accessible to the parent's template.
- Think about how the
setupfunction in Vue 3 can be used to prepare data that will be exposed to the template, including slots. - The "functional slot" aspect refers to the ability for the parent to provide a function-like structure (via template content that uses bound props) that the child component can invoke or render. Vue handles the underlying VNode creation.
- Consider the
slotsobject available in thesetupfunction's second argument. While we are not directly calling a slot function fromCard'ssetupin this specific implementation (Vue handles rendering the template), understandingslotsis crucial for advanced slot manipulation. The key is thev-bindon the<slot>tag in the template ofCard.vue.