Enhance RsTab Component with New Props and Improved Slot Handling

- Added support for tabs to be passed as props, allowing for a more flexible tab configuration.
- Implemented a watch on modelValue to synchronize selected tab changes.
- Enhanced slot handling to accommodate both named slots and default slots for better usability.
- Refactored tab selection logic to utilize helper functions for improved readability and maintainability.
This commit is contained in:
Md Afiq Iskandar 2025-05-26 17:21:26 +08:00
parent 9ea4e18672
commit 373d4fbeda

View File

@ -1,4 +1,6 @@
<script setup> <script setup>
import { ref, provide, useSlots, onMounted, watch } from 'vue';
const props = defineProps({ const props = defineProps({
variant: { variant: {
type: String, type: String,
@ -20,20 +22,76 @@ const props = defineProps({
type: String, type: String,
default: "left", default: "left",
}, },
tabs: {
type: Array,
default: null,
},
modelValue: {
type: String,
default: null,
},
}); });
const emit = defineEmits(['update:modelValue']);
// Slots // Slots
const slots = useSlots(); const slots = useSlots();
const tabs = ref(slots.default().map((tab) => tab.props)); // Handle cases where slots.default might not exist or not be a function
const selectedTitle = ref(tabs.value[0]["title"]); const tabs = ref([]);
const selectedTitle = ref('');
tabs.value.forEach((tab) => { // Initialize tabs from slots or props
if (typeof tab.active !== "undefined") { onMounted(() => {
selectedTitle.value = tab.title; if (props.tabs) {
// If tabs are provided via props (new pattern)
tabs.value = props.tabs;
selectedTitle.value = props.modelValue || props.tabs[0]?.key || props.tabs[0]?.label || '';
} else if (slots.default && typeof slots.default === 'function') {
// If tabs are provided via slots (original pattern)
try {
const slotContent = slots.default();
if (slotContent && Array.isArray(slotContent)) {
tabs.value = slotContent.map((tab) => tab.props).filter(Boolean);
selectedTitle.value = tabs.value[0]?.title || '';
// Check for active tab
tabs.value.forEach((tab) => {
if (typeof tab.active !== "undefined") {
selectedTitle.value = tab.title;
}
});
}
} catch (error) {
console.warn('Error processing tab slots:', error);
tabs.value = [];
}
} }
}); });
// Watch for changes in modelValue
watch(() => props.modelValue, (newValue) => {
if (newValue && newValue !== selectedTitle.value) {
selectedTitle.value = newValue;
}
});
// Watch for changes in selectedTitle and emit update
watch(selectedTitle, (newValue) => {
if (props.modelValue !== undefined) {
emit('update:modelValue', newValue);
}
});
// Helper function to get the correct key/title from tab object
const getTabKey = (tab) => {
return tab.key || tab.title || tab.label || '';
};
const getTabLabel = (tab) => {
return tab.label || tab.title || tab.key || '';
};
provide("selectedTitle", selectedTitle); provide("selectedTitle", selectedTitle);
</script> </script>
@ -69,10 +127,10 @@ provide("selectedTitle", selectedTitle);
border: type === 'border', border: type === 'border',
'border-horizontal': type === 'border' && !vertical, 'border-horizontal': type === 'border' && !vertical,
'border-horizontal-active': 'border-horizontal-active':
selectedTitle === val.title && type === 'border' && !vertical, selectedTitle === getTabKey(val) && type === 'border' && !vertical,
'border-vertical': type === 'border' && vertical, 'border-vertical': type === 'border' && vertical,
'border-vertical-active': 'border-vertical-active':
selectedTitle === val.title && type === 'border' && vertical, selectedTitle === getTabKey(val) && type === 'border' && vertical,
// Variant Color for Border Type // Variant Color for Border Type
'border-hover-primary': type === 'border' && variant == 'primary', 'border-hover-primary': type === 'border' && variant == 'primary',
@ -85,34 +143,34 @@ provide("selectedTitle", selectedTitle);
// Variant Color for Border Type Active // Variant Color for Border Type Active
'border-active-primary': 'border-active-primary':
selectedTitle === val.title && selectedTitle === getTabKey(val) &&
type === 'border' && type === 'border' &&
variant == 'primary', variant == 'primary',
'border-active-secondary': 'border-active-secondary':
selectedTitle === val.title && selectedTitle === getTabKey(val) &&
type === 'border' && type === 'border' &&
variant == 'secondary', variant == 'secondary',
'border-active-info': 'border-active-info':
selectedTitle === val.title && selectedTitle === getTabKey(val) &&
type === 'border' && type === 'border' &&
variant == 'info', variant == 'info',
'border-active-success': 'border-active-success':
selectedTitle === val.title && selectedTitle === getTabKey(val) &&
type === 'border' && type === 'border' &&
variant == 'success', variant == 'success',
'border-active-warning': 'border-active-warning':
selectedTitle === val.title && selectedTitle === getTabKey(val) &&
type === 'border' && type === 'border' &&
variant == 'warning', variant == 'warning',
'border-active-danger': 'border-active-danger':
selectedTitle === val.title && selectedTitle === getTabKey(val) &&
type === 'border' && type === 'border' &&
variant == 'danger', variant == 'danger',
}" }"
role="presentation" role="presentation"
v-for="(val, index) in tabs" v-for="(val, index) in tabs"
:key="index" :key="index"
@click="selectedTitle = val.title" @click="selectedTitle = getTabKey(val)"
> >
<a <a
class="tab-item-link" class="tab-item-link"
@ -120,9 +178,9 @@ provide("selectedTitle", selectedTitle);
default: type === 'default' && !vertical, default: type === 'default' && !vertical,
'default-vertical': type === 'default' && vertical, 'default-vertical': type === 'default' && vertical,
'default-active': 'default-active':
selectedTitle === val.title && type === 'default' && !vertical, selectedTitle === getTabKey(val) && type === 'default' && !vertical,
'default-vertical-active': 'default-vertical-active':
selectedTitle === val.title && type === 'default' && vertical, selectedTitle === getTabKey(val) && type === 'default' && vertical,
// Variant hover for default type // Variant hover for default type
'default-hover-primary': 'default-hover-primary':
@ -138,27 +196,27 @@ provide("selectedTitle", selectedTitle);
// Variant Color for default type Active // Variant Color for default type Active
'default-primary': 'default-primary':
selectedTitle === val.title && selectedTitle === getTabKey(val) &&
type === 'default' && type === 'default' &&
variant == 'primary', variant == 'primary',
'default-secondary': 'default-secondary':
selectedTitle === val.title && selectedTitle === getTabKey(val) &&
type === 'default' && type === 'default' &&
variant == 'secondary', variant == 'secondary',
'default-info': 'default-info':
selectedTitle === val.title && selectedTitle === getTabKey(val) &&
type === 'default' && type === 'default' &&
variant == 'info', variant == 'info',
'default-success': 'default-success':
selectedTitle === val.title && selectedTitle === getTabKey(val) &&
type === 'default' && type === 'default' &&
variant == 'success', variant == 'success',
'default-warning': 'default-warning':
selectedTitle === val.title && selectedTitle === getTabKey(val) &&
type === 'default' && type === 'default' &&
variant == 'warning', variant == 'warning',
'default-danger': 'default-danger':
selectedTitle === val.title && selectedTitle === getTabKey(val) &&
type === 'default' && type === 'default' &&
variant == 'danger', variant == 'danger',
@ -175,27 +233,27 @@ provide("selectedTitle", selectedTitle);
// Variant Color for card type Active // Variant Color for card type Active
'link-card-primary-active': 'link-card-primary-active':
selectedTitle === val.title && selectedTitle === getTabKey(val) &&
type === 'card' && type === 'card' &&
variant == 'primary', variant == 'primary',
'link-card-secondary-active': 'link-card-secondary-active':
selectedTitle === val.title && selectedTitle === getTabKey(val) &&
type === 'card' && type === 'card' &&
variant == 'secondary', variant == 'secondary',
'link-card-info-active': 'link-card-info-active':
selectedTitle === val.title && selectedTitle === getTabKey(val) &&
type === 'card' && type === 'card' &&
variant == 'info', variant == 'info',
'link-card-success-active': 'link-card-success-active':
selectedTitle === val.title && selectedTitle === getTabKey(val) &&
type === 'card' && type === 'card' &&
variant == 'success', variant == 'success',
'link-card-warning-active': 'link-card-warning-active':
selectedTitle === val.title && selectedTitle === getTabKey(val) &&
type === 'card' && type === 'card' &&
variant == 'warning', variant == 'warning',
'link-card-danger-active': 'link-card-danger-active':
selectedTitle === val.title && selectedTitle === getTabKey(val) &&
type === 'card' && type === 'card' &&
variant == 'danger', variant == 'danger',
@ -203,7 +261,7 @@ provide("selectedTitle", selectedTitle);
'link-justify-center': justify == 'center', 'link-justify-center': justify == 'center',
'link-justify-right': justify == 'right', 'link-justify-right': justify == 'right',
}" }"
>{{ val.title }}</a >{{ getTabLabel(val) }}</a
> >
</li> </li>
</ul> </ul>
@ -223,7 +281,17 @@ provide("selectedTitle", selectedTitle);
'content-border-danger': type === 'border' && variant === 'danger', 'content-border-danger': type === 'border' && variant === 'danger',
}" }"
> >
<slot></slot> <!-- New pattern: Named slots for each tab -->
<template v-if="props.tabs">
<div v-for="tab in props.tabs" :key="tab.key" v-show="selectedTitle === tab.key">
<slot :name="tab.key"></slot>
</div>
</template>
<!-- Old pattern: Default slot with RsTabItem components -->
<template v-else>
<slot></slot>
</template>
</div> </div>
</div> </div>
</client-only> </client-only>