JAVASCRIPT
Advanced Content Projection with Vue 3 Scoped Slots
Master Vue 3's scoped slots to allow parent components to customize the rendering of child components by passing data back to the slot content.
// App.vue
<template>
<div>
<h1>App Root</h1>
<UserList :users="activeUsers">
<!-- Default slot content -->
<template #header>
<h2>Currently Active Users</h2>
</template>
<!-- Scoped slot for customizing each user item -->
<template #user-item="{ user, index, toggleStatus }">
<li :class="{ 'inactive': !user.isActive }">
{{ index + 1 }}. {{ user.name }} ({{ user.email }})
<button @click="toggleStatus(user.id)">
{{ user.isActive ? 'Deactivate' : 'Activate' }}
</button>
</li>
</template>
<!-- Default slot for footer -->
<template #footer>
<p>Total active users: {{ activeUsers.length }}</p>
</template>
</UserList>
<hr>
<h2>Another User List Example</h2>
<UserList :users="inactiveUsers">
<template #header>
<h2>Inactive Members</h2>
</template>
<template #user-item="{ user }">
<div class="card">
<h3>{{ user.name }}</h3>
<p>Status: {{ user.isActive ? 'Active' : 'Inactive' }}</p>
</div>
</template>
</UserList>
</div>
</template>
<script setup>
import { ref } from 'vue';
import UserList from './components/UserList.vue';
const activeUsers = ref([
{ id: 1, name: 'Alice', email: '[email protected]', isActive: true },
{ id: 2, name: 'Bob', email: '[email protected]', isActive: true },
{ id: 3, name: 'Charlie', email: '[email protected]', isActive: false }
]);
const inactiveUsers = ref([
{ id: 4, name: 'David', email: '[email protected]', isActive: false },
{ id: 5, name: 'Eve', email: '[email protected]', isActive: true } // Example for toggling
]);
</script>
<style>
.inactive {
text-decoration: line-through;
color: gray;
}
.card {
border: 1px solid #ccc;
padding: 10px;
margin: 5px;
border-radius: 5px;
}
</style>
// components/UserList.vue
<template>
<div class="user-list-container">
<slot name="header"></slot>
<ul>
<li v-if="users.length === 0">No users to display.</li>
<slot name="user-item" v-for="user in users" :key="user.id" :user="user" :index="users.indexOf(user)" :toggleStatus="toggleUserStatus"></slot>
</ul>
<slot name="footer"></slot>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
const props = defineProps({
users: {
type: Array,
required: true
}
});
// We'll mutate the users array here for simplicity,
// in a real app you might emit an event to the parent
// to update the user status if `users` is a prop.
const toggleUserStatus = (userId) => {
const user = props.users.find(u => u.id === userId);
if (user) {
user.isActive = !user.isActive;
console.log(`User ${user.name} status toggled to ${user.isActive}`);
}
};
</script>
<style scoped>
.user-list-container {
border: 1px solid #eee;
padding: 15px;
margin: 20px 0;
border-radius: 8px;
}
</style>
How it works: Vue 3's scoped slots allow a parent component to render content within a child component, while also providing data from the child component back to the parent for use in that content. In this example, `UserList` provides `user`, `index`, and a `toggleStatus` method to its `user-item` slot. The parent component then uses `template #user-item="{ user, index, toggleStatus }"` to destructure these props and define how each user item should be displayed and interacted with, creating highly customizable and reusable components like lists or grids.