Skip to content

Instantly share code, notes, and snippets.

@cathrinevaage
Last active April 17, 2024 04:40
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cathrinevaage/4eed410b31826ce390153d6834909436 to your computer and use it in GitHub Desktop.
Save cathrinevaage/4eed410b31826ce390153d6834909436 to your computer and use it in GitHub Desktop.
Tabs in Vue 3 / Composition API
<!-- Usage -->
<template>
<Tabs>
<Tab name="Foo">Content of tab Foo</Tab>
<Tab name="Bar">Content of tab Bar</Tab>
</Tabs>
</template>
<script>
import Tabs from './components/Tabs.vue'
import Tab from './components/Tab.vue'
export default {
name: 'App',
components: {
Tabs,
Tab,
}
}
</script>
<!-- components/Tab.vue -->
<script lang="ts">
import { defineComponent, toRef, reactive } from 'vue'
import { useTab } from '../composables/tabs'
export default defineComponent({
name: 'Tab',
props: {
name: {
type: String,
required: true,
},
},
setup (props) {
const name = toRef(props, 'name')
const tabData = reactive({ name })
const { isActive } = useTab(tabData)
return {
isActive,
}
},
})
</script>
<template>
<div
v-if="isActive"
class="Tab"
>
<slot />
</div>
</template>
// composables/tabs.ts
import { reactive, ref, readonly, InjectionKey, provide, inject, Ref, onUnmounted, computed } from 'vue'
interface TabData {
name: string,
}
const tabsInjectionKey = Symbol('tabs') as InjectionKey<{
registerTab: (identifier: symbol, tabData: TabData) => void,
deregisterTab: (identifier: symbol) => void
activeTab: Readonly<Ref<symbol>>,
}>
export const useTabs = () => {
const tabs = reactive(new Map<symbol, TabData>())
const registerTab = (identifier: symbol, tabData: TabData) => {
tabs.set(identifier, tabData)
}
const deregisterTab = (identifier: symbol) => {
tabs.delete(identifier)
}
const activeTab = ref<symbol>()
provide(tabsInjectionKey, {
registerTab,
deregisterTab,
activeTab: readonly(activeTab),
})
const setActiveTab = (identifier: symbol) => {
activeTab.value = identifier
}
return {
tabs: readonly(tabs),
setActiveTab,
}
}
export const useTab = (tabData: TabData) => {
const tabsInjection = inject(tabsInjectionKey)
if (!tabsInjection) {
throw new Error('Tabs were not provided')
}
const { registerTab, deregisterTab, activeTab } = tabsInjection
const tabSymbol = Symbol(tabData.name)
registerTab(tabSymbol, tabData)
onUnmounted(() => {
deregisterTab(tabSymbol)
})
const isActive = computed(() => (
activeTab.value === tabSymbol
))
return {
isActive,
}
}
<!-- components/Tabs.vue -->
<script lang="ts">
import { defineComponent } from 'vue'
import { useTabs } from '../composables/tabs'
export default defineComponent({
name: 'Tabs',
setup () {
const { tabs, setActiveTab } = useTabs()
return {
tabs,
setActiveTab,
}
},
})
</script>
<template>
<div class="Tabs">
<div class="Tabs__tabButtons">
<button
v-for="[identifier, tabData] in tabs.entries()"
:key="identifier"
@click="setActiveTab(identifier)"
>
{{ tabData.name }}
</button>
</div>
<div class="Tabs__tabs">
<slot />
</div>
</div>
</template>
@cathrinevaage
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment