Preface
Goal: Porting to Vue3 using different pattern. Solving this.$children removal issue.
In order to select tab in click event, all the children data should be available in all child components, This is why I need to pass all tabs data in parent component, along with the children data, to all child components.
I assume you have read the previous article, the Vue component, and Vue router, and Vue2 application.
Pattern Changes
From the
vue2
tabs tovue3
tabs.
We are going to overhaul the previous pattern, with these changes:
-
Using composition API instead of options API. Following the newly released
vue3
. -
Using javascript data instead of template slot. Using
provide
andinject
mechanism, instead of$children
.
The $children Removal
There must be another way 🙂.
As you might already know, the $children
API,
has been deprecated in vue3
.
The issue is my vue2
tabs component,
rely heavily with this.$children
.
I spent two days, trying to find out,
how to emulate this.$children
, and find no luck.
I started to think that I might be trapped in XY problem
.
Then I suddenly realize that I can achieve similar result, if I could change entirely, the way I write this component pattern.
The Vue2 Tabs
Data is provided as html
template, in a component.
This way is comfortable when we have a lot of html
tags.
<template>
<tabs>
<tab name="home" title="Home" color="bg-blue-500">
<h3>Home</h3>
<p>Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
Quisque in faucibus magna.</p>
</tab>
…
</tabs>
</template>
We can just easily passing the html
data through slots
.
The Vue3 Tabs
Data is provided in javascript
array, in external file.
This way, we can programatically passing data, between component.
By using basic types means, an easy way without black magic.
let tabsArray = [
{ name: 'home', title: 'Home', color: 'bg-blue-500',
text: `Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
Quisque in faucibus magna.` },
{ name: 'team', title: 'Team', color: 'bg-teal-500',
text: `Nulla luctus nisl in venenatis vestibulum.
Nam consectetur blandit consectetur.` },
{ name: 'news', title: 'News', color: 'bg-red-500',
text: `Phasellus vel tempus mauris,
quis mattis leo.
Mauris quis velit enim.` },
{ name: 'about', title: 'About', color: 'bg-orange-500',
text: `Interdum et malesuada fames ac ante
ipsum primis in faucibus.
Nulla vulputate tortor turpis,
at eleifend eros bibendum quis.` }
];
export { tabsArray };
This new component might not be the best practice, but the new component works well. This should be enough for me.
Application: The Vue3 Tabs
Before we discuss about component, consider refresh our memory, to get the big picture onhow this application works.
Source Examples
You can obtain source examples here:
Preview
You can test in your favorite browser:
Components
Step by step component is named as below:
- MainMockup: Basic (limited) interactivity.
- MainSimple: Fully working simple tabs.
- MainEnhanced: Fully working enhanced tabs.
You are going to see how elegant vue3
composition,
in solving issue, compared with vue2
counterpart.
The provide
and inject
mechanism is already happened in vue2
,
and it is getting better in vue3
.
Directory Structure
Consider to get organized.
I have refactor each component into
Main
,Tabs
,TabHeader
,TabContent
$ tree src
src
├── App.vue
├── assets
│  └── css
│  ├── background-colors.css
│  ├── border-radius.css
│  ├── enhanced-layout.css
│  └── simple-layout.css
├── components
│  ├── enhanced
│  │  ├── MainEnhanced.vue
│  │  ├── TabContentEnhanced.vue
│  │  ├── TabHeaderEnhanced.vue
│  │  └── TabsEnhanced.vue
│  ├── mockup
│  │  ├── MainMockup.vue
│  │  ├── TabContentMockup.vue
│  │  ├── TabHeaderMockup.vue
│  │  └── TabsMockup.vue
│  ├── simple
│  │  ├── MainSimple.vue
│  │  ├── TabContentSimple.vue
│  │  ├── TabHeaderSimple.vue
│  │  └── TabsSimple.vue
│  └── TitleHeading.vue
├── main.js
├── router
│  └── index.js
└── tabsArray.js
7 directories, 21 files
We do not really need Main*.vue
,
and we can instead use Tabs*.vue
directly.
My intention is explaining an alternative implementation,
without using this.$children
.
So we can compare with original code.
main.js
The main.js
is exactly the same as previous vue3
router example.
import { createApp, reactive } from 'vue'
import App from './App.vue'
import router from './router'
const Title = reactive({
computed: {
pageTitle: function() {
return this.$route.meta.title;
}
},
created () {
document.title = this.$route.meta.title;
},
watch: {
$route(to) {
document.title = to.meta.title;
},
}
})
createApp(App)
.use(router)
.mixin(Title)
.mount('#app')
App.vue
We should gather all these three components above in src/App.vue
.
<template>
<div id="app">
<section class="link-nav">
<router-link to="simple-mockup">Simple Mockup</router-link>
<router-link to="simple-tabs" >Simple Tabs</router-link>
<router-link to="enhanced-tabs">Enhanced Tabs</router-link>
</section>
<TitleHeading/>
<router-view></router-view>
</div>
</template>
<script>
import TitleHeading from './components/TitleHeading.vue'
export default {
name: 'App',
components: {
TitleHeading
}
}
</script>
<style>
body { font-family: Arial, Helvetica, sans-serif; }
#app { margin: 1rem; }
section.link-nav { margin-bottom: 1rem; }
a { padding-right: 1rem; color: #00796b; }
</style>
Router: index.js
And the index is also similar.
import { createWebHistory, createRouter } from "vue-router";
import MainMockup from '@/components/mockup/MainMockup'
import MainSimple from '@/components/simple/MainSimple'
import MainEnhanced from '@/components/enhanced/MainEnhanced'
const routes = [
{
path: '/',
name: 'default',
component: MainEnhanced,
meta: { title: 'Default Page' }
},
{
path: '/simple-mockup',
name: 'MainMockup',
component: MainMockup,
meta: { title: 'Simple Tabs - Mockup' }
},
{
path: '/simple-tabs',
name: 'MainSimple',
component: MainSimple,
meta: { title: 'Simple Tabs - Component' }
},
{
path: '/enhanced-tabs',
name: 'MainEnhanced',
component: MainEnhanced,
meta: { title: 'Enhanced Tabs - Component' }
}
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
Heading Component
The TitleHeading.vue
component is,
exactly the same as previous router example.
<template>
<section>
<h1>{{ $route.meta.title }}</h1>
</section>
</template>
<script>
export default {
name: 'TitleHeading'
}
</script>
I think we are done with the application setup. We are ready for the element explanation.
Component: MainMockup
Main Component: Complete
Without using mixin
, we can have the main component as below:
<template>
<tabs />
</template>
<script>
import { provide } from 'vue'
import { tabsArray } from '@/tabsArray.js'
import tabs from '@/components/mockup/TabsMockup.vue'
export default {
name: 'MainMockup',
components: { tabs },
setup() {
provide('tabsArray', tabsArray)
}
}
</script>
Main Component: Passing Data
The template is simply without data:
<template>
<tabs />
</template>
The data passed using provide
mechanism as below:
setup() {
provide('tabsArray', tabsArray)
}
Since the data is reusable between three components, It is better to put the source in external file.
import { tabsArray } from '@/tabsArray.js'
Of course you can embed the data inside the component instead.
Tabs Component: The Container
This component do all the horse works.
<template>
…
</template>
<script>
import { inject } from 'vue'
import tabheader from '@/components/mockup/TabHeaderMockup.vue'
import tabcontent from '@/components/mockup/TabContentMockup.vue'
export default {
name: 'TabsMockup',
components: { tabheader, tabcontent },
setup() {
const tabs = inject('tabsArray')
return { tabs }
}
}
</script>
<style scoped>
@import '../../assets/css/simple-layout.css';
</style>
Tabs Component: Getting Data from The Parent
We can utilize the provided data in child component.
setup() {
const tabs = inject('tabsArray')
return { tabs }
}
You can spot the difference between examples.
In contrast with the previous vue2
tabs mechanism, as below:
data() {
return {
tabs: this.$children
}
}
Tabs Component: The Template
We have two separated v-for
running in two different components.
Feel free to bind each props
in element for each iteration.
-
tabheader
, -
tabcontent
.
<template>
<main class="tabs">
<div class="tab-headers">
<tabheader
v-for="tab in tabs" v-bind:key="tab.name"
:title="tab.title" :color="tab.color"
:tabname="tab.name"
/>
</div>
<div class="tab-spacer"></div>
<div class="tab-contents">
<tabcontent
v-for="tab in tabs" v-bind:key="tab.name"
:text="tab.text" :title="tab.title" :color="tab.color"
:tabname="tab.name"
/>
</div>
</main>
</template>
I utilize :tabname="tab.name"
instead of :name="tab.name"
,
so this identifier does not mistakenly collide,
with this.name
(component name).
Tab Header Component
We do not have any javascript interactivity yet.
This is just contain basic props
definition.
<template>
<div class="bg-gray-700">{{ title }}</div>
</template>
<script>
export default {
name: 'TabHeaderMockup',
props: {
tabname: { type: String, required: true },
title: { type: String, required: true },
color: { type: String, required: true }
}
}
</script>
You can turn the tab into colourful tab header,
by using v-bind
as below:
<template>
<div v-bind:class="color">{{ title }}</div>
</template>
Tab Content Component
For this mockup, we only show team
content.
<template>
<div v-bind:class="color"
v-show="this.tabname == 'team'">
<h3>{{ title }}</h3>
<p>{{ text }}</p>
</div>
</template>
<script>
export default {
name: 'TabContentMockup',
props: {
tabname: { type: String, required: true },
title: { type: String, required: true },
text: { type: String, required: true },
color: { type: String, required: true }
}
}
</script>
Preview
You can enjoy testing in your favorite browser:
Component: MainSimple
Main Component
Very similar with previous mockup, we only change the component name.
import tabs from '@/components/simple/TabsSimple.vue'
Tabs Component: The Template
Nothing is changed. It is exactly the same with previous mockup template.
This component also use the same stylesheet.
Tabs Component: The Script
Manage selected tab.
Since we want to manage selected tab, for both header and content,
we need to provide
the variable, for both child components.
We also need this variable to be reactive
too, based on click event.
import { inject } from 'vue'
import { provide, ref } from 'vue'
import tabheader from '@/components/simple/TabHeaderSimple.vue'
import tabcontent from '@/components/simple/TabContentSimple.vue'
export default {
name: 'TabsSimple',
components: { tabheader, tabcontent },
setup() {
const tabs = inject('tabsArray')
const tabSelected = ref('team')
provide('tabSelected', tabSelected)
return { tabs }
}
}
You can spot the improvement in above script.
Tab Header Component
We setup the javascript interactivity for this tab header component.
<template>
<div
v-on:click="selectTabByName(this.tabname)"
v-bind:class="[activeClass(), colorClass()]"
>{{ title }}</div>
</template>
<script>
import { inject } from 'vue'
export default {
name: 'TabHeaderSimple',
props: {
tabname: { type: String, required: true },
title: { type: String, required: true },
color: { type: String, required: true }
},
setup() {
const tabSelected = inject('tabSelected')
return { tabSelected }
},
mounted() {
this.selectTabByName(this.tabSelected);
},
methods: {
selectTabByName(tabName) {
this.tabSelected = tabName
},
activeClass : function () {
return this.tabname == this.tabSelected ? 'active' : '';
},
colorClass : function () {
return this.tabname == this.tabSelected ?
this.color : 'bg-gray-700';
}
}
}
</script>
Tab Header Component: Selecting Tab
How does it works?
It is similar wih previous vue2
example,
but this time using tabSelected
with provide/inject
mechanism.
The tabname
has been bound using v-for
.
v-on:click="selectTabByName(this.tabname)"
The methods is just oneliner.
setup() {
const tabSelected = inject('tabSelected')
return { tabSelected }
},
methods: {
selectTabByName(tabName) {
this.tabSelected = tabName
}
}
With this script we also initialize the default tab using mounted
.
mounted() {
this.selectTabByName(this.tabSelected);
},
Tab Header Component: Binding Color
We are no longer rely on tab
parameter anymore,
as we did with vue2
examples.
v-bind:class="[activeClass(tab), colorClass(tab)]"
But instead it is simpler.
<template>
<div
v-on:click="selectTabByName(this.tabname)"
v-bind:class="[activeClass(), colorClass()]"
>{{ title }}</div>
</template>
This can be happened,
because we already have tabname
bound to each tab.
methods: {
activeClass : function () {
return this.tabname == this.tabSelected ? 'active' : '';
},
colorClass : function () {
return this.tabname == this.tabSelected ?
this.color : 'bg-gray-700';
}
}
Tab Content Component
We also have a few improvement here:
<template>
<div v-bind:class="this.color"
v-show="this.tabname == this.tabSelected">
<h3>{{ title }}</h3>
<p>{{ text }}</p>
</div>
</template>
<script>
import { inject } from 'vue'
export default {
name: 'TabContentMockup',
props: {
tabname: { type: String, required: true },
title: { type: String, required: true },
text: { type: String, required: true },
color: { type: String, required: true }
},
setup() {
const tabSelected = inject('tabSelected')
return { tabSelected }
}
}
</script>
We can choose which tab shown by already bound tabname
.
<div v-bind:class="this.color"
v-show="this.tabname == this.tabSelected">
…
</div>
The tabSelected
is also using provide/inject
mechanism.
setup() {
const tabSelected = inject('tabSelected')
return { tabSelected }
}
Preview
Again, you can enjoy testing in your favorite browser:
Component: MainEnhanced
This is our fully functional tabs component.
Main Component
Very similar with previous mockup, we only change the component name.
import tabs from '@/components/enhanced/TabsEnhanced.vue'
Tabs Component: The Template
We have different class name using -enh
,
to avoid stylesheet collision.
<template>
<main class="tabs-enh">
<div class="tab-enh-headers">
<tabheader
v-for="tab in tabs" v-bind:key="tab.name"
:title="tab.title" :color="tab.color"
:tabname="tab.name"
/>
</div>
<div class="tab-enh-spacer"></div>
<div class="tab-enh-contents">
<tabcontent
v-for="tab in tabs" v-bind:key="tab.name"
:text="tab.text" :title="tab.title" :color="tab.color"
:tabname="tab.name"
/>
</div>
</main>
</template>
The rest is the same. No difference.
Tabs Component: The Stylesheet
This component use different stylesheet.
@import '../../assets/css/enhanced-layout.css';
@import '../../assets/css/background-colors.css';
@import '../../assets/css/border-radius.css';
Tabs Component: The Script
Manage selected tab.
Since we want to also manage hovered tab,
we also need to provide
the tabHovered
variable.
We also need this variable to be reactive
too,
based on mouse event.
import { inject } from 'vue'
import { provide, ref } from 'vue'
import tabheader from '@/components/enhanced/TabHeaderEnhanced.vue'
import tabcontent from '@/components/enhanced/TabContentEnhanced.vue'
export default {
name: 'TabsEnhanced',
components: { tabheader, tabcontent },
setup() {
const tabs = inject('tabsArray')
const tabSelected = ref('team')
const tabHovered = ref('')
provide('tabSelected', tabSelected)
provide('tabHovered', tabHovered)
return { tabs }
}
}
You can spot both tabSelected
and tabHovered
in above script.
Both contain the name of the tab as string
,
instead of just boolean
value.
There is no need to return value, for provide
.
But we need to return value, for inject
in both child component.
Tab Header Component
We setup more javascript interactivity for this tab header component.
<template>
<div
v-on:click="selectTabByName(this.tabname)"
v-on:mouseenter="this.tabHovered = this.tabname"
v-on:mouseleave="this.tabHovered = ''"
v-bind:class="[activeClass(), colorClass()]">
<div
v-bind:class="{ 'bg-white' : this.tabname == this.tabSelected }"
>{{ title }}</div>
</div>
</template>
<script>
import { inject } from 'vue'
export default {
name: 'TabHeaderSimple',
props: {
tabname: { type: String, required: true },
title: { type: String, required: true },
color: { type: String, required: true }
},
setup() {
const tabSelected = inject('tabSelected')
const tabHovered = inject('tabHovered')
return { tabSelected, tabHovered }
},
mounted() {
this.selectTabByName(this.tabSelected);
},
methods: {
selectTabByName(tabName) {
this.tabSelected = tabName
},
activeClass : function () {
return this.tabname == this.tabSelected ? 'active' : '';
},
colorClass : function () {
return this.tabname == this.tabSelected ?
this.color : 'bg-gray-700';
}
}
}
</script>
Tab Header Component: Template
We have more <div>
:
<template>
<div …>
<div
v-bind:class="{ 'bg-white' : this.tabname == this.tabSelected }"
>{{ title }}</div>
</div>
</template>
Tab Header Component: Synching Hover
How does it works?
It is similar wih previous vue2
example,
but this time using tabHovered
with provide/inject
mechanism.
The tabname
has been bound using v-for
.
v-on:mouseenter="this.tabHovered = this.tabname"
v-on:mouseleave="this.tabHovered = ''"
And in script we also initialize the default tab using mounted
.
setup() {
const tabSelected = inject('tabSelected')
const tabHovered = inject('tabHovered')
return { tabSelected, tabHovered }
}
Tab Content Component
We also have a few improvement here:
<template>
<div v-bind:class="this.color"
v-show="this.tabname == this.tabSelected">
<div class="tab-enh-content"
v-bind:class="{ 'is-hovered' : this.tabHovered == this.tabname}">
<h3>{{ title }}</h3>
<p>{{ text }}</p>
</div>
</div>
</template>
<script>
import { inject } from 'vue'
export default {
name: 'TabContentMockup',
props: {
tabname: { type: String, required: true },
title: { type: String, required: true },
text: { type: String, required: true },
color: { type: String, required: true }
},
setup() {
const tabSelected = inject('tabSelected')
const tabHovered = inject('tabHovered')
return { tabSelected, tabHovered }
}
}
</script>
Tab content Component: Template
We have more <div>
:
<template>
<div …>
<div class="tab-enh-content" …>
…
</div>
</div>
</template>
Tab Content Component: Synching Hover
How does it works?
We can alter the color of the hovered tab header using bound tabname
.
<div class="tab-enh-content"
v-bind:class="{ 'is-hovered' : this.tabHovered == this.tabname}">
<h3>{{ title }}</h3>
<p>{{ text }}</p>
</div>
The tabHovered
is also using provide/inject
mechanism.
setup() {
const tabSelected = inject('tabSelected')
const tabHovered = inject('tabHovered')
return { tabSelected, tabHovered }
}
Preview
At last, our fully functional, tabs component in browser.
I must admit that my vue
knowledge is still limited.
I’m open to other possibity,
and also other issue comes with this component pattern.
I still have so much to learn.
What’s Next?
Later.