JAVASCRIPT
Vue 3 Advanced Scoped Slots for Dynamic UI Rendering
Master Vue 3 scoped slots to build highly reusable components that let parent components define how specific parts of the child's content are rendered, enabling flexible UI structures.
// src/components/GenericTable.vue
<template>
<div class="generic-table-container">
<table>
<thead>
<tr>
<th v-for="header in headers" :key="header.key">
<slot :name="`header-${header.key}`" :header="header">
{{ header.label || header.key }}
</slot>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in data" :key="item.id || index">
<td v-for="header in headers" :key="header.key">
<slot :name="`item-${header.key}`" :item="item" :index="index" :value="item[header.key]">
{{ item[header.key] }}
</slot>
</td>
</tr>
</tbody>
<tfoot v-if="$slots.footer">
<tr>
<td :colspan="headers.length">
<slot name="footer"></slot>
</td>
</tr>
</tfoot>
</table>
<div v-if="!data || data.length === 0" class="no-data-message">
<slot name="no-data">
No data available.
</slot>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({
headers: {
type: Array,
required: true,
validator: (headers) => headers.every(h => h.key)
},
data: {
type: Array,
default: () => []
}
});
</script>
<style scoped>
.generic-table-container {
width: 100%;
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
margin: 1em 0;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
.no-data-message {
padding: 10px;
text-align: center;
color: #666;
background-color: #f9f9f9;
border: 1px dashed #ccc;
margin-top: 1em;
}
</style>
// MyParentComponent.vue usage:
/*
<template>
<GenericTable :headers="tableHeaders" :data="tableData">
<template #header-name="{ header }">
<strong>{{ header.label }} (Sorted)</strong>
</template>
<template #item-status="{ item }">
<span :style="{ color: item.status === 'active' ? 'green' : 'red' }">
{{ item.status.toUpperCase() }}
</span>
</template>
<template #item-actions="{ item }">
<button @click="editItem(item.id)">Edit</button>
<button @click="deleteItem(item.id)">Delete</button>
</template>
<template #footer>
<div style="text-align: right; font-weight: bold;">
Total items: {{ tableData.length }}
</div>
</template>
<template #no-data>
<p style="color: blue;">No records found. Please add new data.</p>
</template>
</GenericTable>
</template>
<script setup>
import { ref } from 'vue';
import GenericTable from './components/GenericTable.vue';
const tableHeaders = ref([
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Product Name' },
{ key: 'status', label: 'Status' },
{ key: 'price', label: 'Price' },
{ key: 'actions', label: 'Actions' }
]);
const tableData = ref([
{ id: 1, name: 'Laptop', status: 'active', price: 1200 },
{ id: 2, name: 'Mouse', status: 'inactive', price: 25 },
{ id: 3, name: 'Keyboard', status: 'active', price: 75 },
]);
const editItem = (id) => console.log('Edit item:', id);
const deleteItem = (id) => console.log('Delete item:', id);
</script>
*/
How it works: This snippet demonstrates building a highly flexible `GenericTable` component using advanced named and scoped slots. The parent component can completely customize the rendering of individual table headers (`header-<key>`), specific item cells (`item-<key>`), the table footer, and even the "no data" message. The `item`, `index`, and `value` props passed via scoped slots allow the parent to access data for each row and column, making the table extremely versatile without needing to rewrite its core structure.