Where to Discuss?

Local Group

Preface

Goal: Explaining tabs component in Vue Application.

I assume you have read the previous article, the Vue component, and Vue router.


Application: The Vue2 Tabs

I already make a step by step example. So you can just run and examine the components, without rewriting all from scratch.

Source Examples

You can obtain source examples here:

Components

Step by step component is named as below:

  1. SimpleLayout: HTML only.
  2. SimpleInline: Inline (obtrusive) vue.js.
  3. SimpleMockup: Basic (limited) interactivity.
  4. SimpleTabs: Fully working simple tabs.
  5. EnhancedTabs: Fully working enhanced tabs.

You are going to see how vue2 component reduce code writing. The final code is smaller than the inline one.

Directory Structure

Consider to get organized.

I have refactor some component into TabList and TabItem. We can rearranged component with dependency, in each own directory for tidiness reason. No need to make a folder for component without any dependency.

$ tree src
src
├── App.vue
├── assets
│   └── css
│       ├── background-colors.css
│       ├── border-radius.css
│       ├── enhanced-layout.css
│       └── simple-layout.css
├── components
│   ├── enhanced
│   │   ├── EnhancedTabs.vue
│   │   ├── TabItemEnhanced.vue
│   │   └── TabListEnhanced.vue
│   ├── mockup
│   │   ├── SimpleMockup.vue
│   │   ├── TabItemMockup.vue
│   │   └── TabListMockup.vue
│   ├── simple
│   │   ├── SimpleTabs.vue
│   │   ├── TabItemSimple.vue
│   │   └── TabListSimple.vue
│   ├── SimpleInline.vue
│   ├── SimpleLayout.vue
│   ├── TemplateAbstract.vue
│   └── TitleHeading.vue
├── main.js
└── router
    └── index.js

7 directories, 20 files

Vue Tabs App: App Tree Structure

main.js

The main.js is exactly the same as previous vue2 router example.

App.vue

We should gather all these components above in src/App.vue.

<template>
  <div id="app">
    <section class="link-nav">
      <router-link to="simple-layout">Simple Layout</router-link>
      <router-link to="simple-inline">Simple Inline</router-link>
      <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. Just beware of the import directory.

import Vue from 'vue'
import Router from 'vue-router'
import SimpleLayout from '@/components/SimpleLayout'
import SimpleInline from '@/components/SimpleInline'
import SimpleMockup from '@/components/mockup/SimpleMockup'
import SimpleTabs from   '@/components/simple/SimpleTabs'
import EnhancedTabs from '@/components/enhanced/EnhancedTabs'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'default',
      component: EnhancedTabs,
      meta: { title: 'Default Page' }
    },
    {
      path: '/simple-layout',
      name: 'SimpleLayout',
      component: SimpleLayout,
      meta: { title: 'Simple Tabs - Layout Only' }
    },
    {
      path: '/simple-inline',
      name: 'SimpleInline',
      component: SimpleInline,
      meta: { title: 'Simple Tabs - Inline' }
    },
    {
      path: '/simple-mockup',
      name: 'SimpleMockup',
      component: SimpleMockup,
      meta: { title: 'Simple Tabs - Mockup' }
    },
    {
      path: '/simple-tabs',
      name: 'SimpleTabs',
      component: SimpleTabs,
      meta: { title: 'Simple Tabs - Component' }
    },
    {
      path: '/enhanced-tabs',
      name: 'EnhancedTabs',
      component: EnhancedTabs,
      meta: { title: 'Enhanced Tabs - Component' }
    }
  ]
})

Heading Component

The TitleHeading.vue component is, exactly the same as previous router example.

I think we are done with the application setup. We are ready for the element explanation.


Component: SimpleLayout

This component is just consist one single simple file. It is just a layout without any javaaxript interactivity yet.

<template>
  <main class="tabs">
    <div class="tab-headers">
      <div class="bg-gray">
          Home</div>
      <div class="active bg-orange">
          Team</div>
      <div class="bg-gray">
          Contact</div>
      <div class="bg-gray">
          About</div>
    </div>

    <div class="tab-contents">
      <div class="bg-gray">
        <h3>Lorem Ipsum</h3>
        <p>Lorem ipsum dolor sit amet,
           consectetur adipiscing elit.
           Quisque in faucibus magna.</p>
      </div>
    </div>
  </main>
</template>

<script>
export default {
  name: 'SimpleLayout'
}
</script>

<style scoped>
  @import '../assets/css/simple-layout.css';
</style>

Pretty simple right 🙂? It is just like an ordinary html page, but with script and style in different place.

Stylesheet

You can either write the style as

<style scoped src="@/assets/css/simple-layout.css">

or

<style scoped>
  @import '../assets/css/simple-layout.css';
</style>

Both works.

Scoped

Beware of the interpretation

I thought that, the scoped means, the style only loaded for this component only. But it behaves different than my first expectation. After a while, I know that my past interpretation is not quite right.

You should read the official documentation

Preview

You can test in your favorite browser:

Vue App: Component Preview: Simple: Layout


Component: SimpleInline

This component require only onefile. And this component works well with full functionality.

The template is as below code:

<template>
  <main class="tabs">
    <div class="tab-headers">
      <div v-on:click="selected = 'home'"
           v-bind:class="{ 
             'active bg-blue-500' : selected == 'home', 
             'bg-gray-700' : selected != 'home' }"
          >Home
      </div>
      …
    </div>

    <div class="tab-spacer"></div>

    <div class="tab-contents">
      <div v-show="selected == 'home'"
           v-bind:class="{ 'bg-blue-500' : selected == 'home' }">
          <h3>Home</h3>
          <p>Lorem ipsum dolor sit amet,
             consectetur adipiscing elit.
             Quisque in faucibus magna.</p>
      </div>
      …
    </div>
  </main>
</template>

And the rest follow as below code:

<script>
export default {
  name: 'SimpleInline',
  data() {
    return {
      selected: 'news'
    };
  }
}
</script>

<style scoped>
  @import '../assets/css/simple-layout.css';
</style>

It is long. But also pretty simple right 🙂? We can just dump our previous regular inline vue page, into a vue component.

Preview

You can test in your favorite browser:

Vue App: Component Preview: Simple: Inline


Mixin: Reusable Template

Do not repeat yourself.

For the next three components, we are going to use the same template structure, but different render structure.

Main Component: Template

We have outer <tabs> tag, and inner <tab> tag.

The template is as below code:

<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>
      <tab name="team" title="Team" color="bg-teal-500">
        <h3>Team</h3>
        <p>Nulla luctus nisl in venenatis vestibulum.
           Nam consectetur blandit consectetur.</p>
      </tab>
      <tab name="news" title="News" color="bg-red-500">
        <h3>News</h3>
        <p>Phasellus vel tempus mauris,
           quis mattis leo.
           Mauris quis velit enim.</p>
      </tab>
      <tab name="about" title="About" color="bg-orange-500">
        <h3>About</h3>
        <p>Interdum et malesuada fames ac ante
           ipsum primis in faucibus.
           Nulla vulputate tortor turpis,
           at eleifend eros bibendum quis.</p>
      </tab>
  </tabs>
</template>

Here we have these two unique tags:

  1. <tabs> component, and

  2. <tab> component.

And also there is no distinction, between tab headers and tab contents. We are going to transform this later with vue.


Component: SimpleMockup

This time we refactor into three Vue component. Remember taht this is just a mockup, with very little javascript interativity.

Main Component: Template

Using mixin.

Main Component: Script

We import template, and then both components, as below code here:

<script>
import template from '@/components/TemplateAbstract.vue'
import tabs from '@/components/mockup/TabListMockup.vue'
import tab  from '@/components/mockup/TabItemMockup.vue'

export default {
  name: 'SimpleMockup',
  mixins:[template],
  components: {
    tabs, tab
  }
}
</script>

We are going to use this pattern for the next two main components.

Main Component: Style

The same as before:

<style scoped>
  @import '../../assets/css/simple-layout.css';
</style>

Notice the path, now it use ‘../../assets’, instead of ‘../assets’.

Tab List Component

It has one single file.

<template>
  <main class="tabs">
    <div class="tab-headers">
      <div class="bg-gray-700"
           v-for="tab in tabs" v-bind:key="tab.name"
          >{{ tab.title }}
      </div>
    </div>

    <div class="tab-spacer"></div>

    <div class="tab-contents">
      <slot></slot>
    </div>
  </main>
</template>

<script>
export default {
  name: 'TabItemMockup',
  data() {
    return {
      tabs: this.$children
    };
  }
}
</script>

It was transformed from previous article’s code, as written below:

Vue.component('tabs', {
  template: `
  <main class="tabs">
    <div class="tab-headers">
      <div class="bg-gray-700" v-for="tab in tabs">{{ tab.title }}
      </div>
    </div>

    <div class="tab-spacer"></div>

    <div class="tab-contents">
      <slot></slot>
    </div>
  </main>
  `,

  data() {
    return {
      tabs: this.$children
    };
  }
});

``

I guess the Vue CLI style is easier to be read.

Tab Item Component

With the same transformation, we have this code below:

<template>
     <div class="bg-gray-700" v-show="name == 'home'">
       <slot></slot>
     </div>
</template>

<script>
export default {
  name: 'TabItemMockup',
  props: {
    name:  { required: true },
    title: { required: true }
  }
}
</script>

No matter what left tab you click. This will show the same home content.

Preview

You can test in your favorite browser:

Vue App: Component Preview: Simple: Mockup


Component: SimpleTabs

Main Component: Template

Using mixin.

Main Component: Script

We import both components, as below code here:

<script>
import template from '@/components/TemplateAbstract.vue'
import tabs from '@/components/simple/TabListSimple.vue'
import tab  from '@/components/simple/TabItemSimple.vue'

export default {
  name: 'SimpleTabs',
  mixins:[template],
  components: {
    tabs, tab
  }
}
</script>

Main Component: Style

The stylesheet is exactly the same as previous code:

Tab List Component

It is basically the same as the mockup. But with a bunch of interactivity declaration in vue javascript.

The template part:

<template>
  <main class="tabs">
    <div class="tab-headers">
      <div
         v-for="tab in tabs" v-bind:key="tab.name"
         v-on:click="selectTabByName(tab.name)"
         v-bind:class="[activeClass(tab), colorClass(tab)]"
        >{{ tab.title }}
      </div>
    </div>

    <div class="tab-spacer"></div>

    <div class="tab-contents">
      <slot></slot>
    </div>
  </main>
</template>

The javascript part:

export default {
  name: 'TabListSimple',
  data() {
    return {
      selected: 'news',
      tabs: this.$children
    };
  },  
  mounted() {
    this.selectTabByName(this.selected);
  },
  methods: {
    selectTabByName(tabName) {
      this.selected = tabName;
      this.tabs.forEach(tab => {
        tab.isActive = (tab.name == tabName);
      });
    },
    activeClass : function (tab) {
      return tab.name == this.selected ? 'active' : '';
    },
    colorClass  : function (tab) {
      return tab.name == this.selected ? tab.color : 'bg-gray-700';
    }
  }
}

The Immutable Props

One-Way Data Flow

We should not be tempted to use props for mutable data.

export default {
  name: 'TabListSimple',
  props: {
    selected: { default: 'news' }
  },
  data() {
    return {
      tabs: this.$children
    };
  }
  
}

Mutating a prop locally is now considered an anti-pattern.

The props is designed to form a one-way-down binding, between the child property and the parent one. Then you should this code below instead.

export default {
  name: 'TabListSimple',
  data() {
    return {
      selected: 'news',
      tabs: this.$children
    };
  },
   
}

Everytime you click left time, the right content will be changed, to reflect the choice.

How does it works? We have already discussed how it works in previous article.

Tab Item Component

The same applied with tab item. We add vue declaration, to obtain javacript interactivity.

The template part:

<template>
     <div
       v-show="isActive"
       v-bind:class="this.color">
       <slot></slot>
     </div>
</template>

The javascript part:

export default {
  name: 'TabItemSimple',
  props: {
    name:  { required: true },
    title: { required: true },
    color: { default: '' }
  },
  data() {
    return {
      isActive: false
    };
  },
}

How does it works? We have already discussed how it works in previous article.

Preview

You can test in your favorite browser:

Vue App: Component Preview: Simple: Fully Working


Component: EnhancedTabs

This is the final step.

Main Component: Template

Using mixin.

No need to change a bit. This the beauty of designing component. We can just put required data, and the looks can be done later.

Main Component: Script

We this imported component here as below:

import template from '@/components/TemplateAbstract.vue'
import tabs from '@/components/enhanced/TabListEnhanced.vue'
import tab  from '@/components/enhanced/TabItemEnhanced.vue'

export default {
  name: 'EnhancedTabs',
  mixins:[template],
  components: {
    tabs, tab
  }
}

Main Component: Style

We have different style, we manage different looks, and also with different DOM.

  @import '../../assets/css/enhanced-layout.css';
  @import '../../assets/css/background-colors.css';
  @import '../../assets/css/border-radius.css';

Tab List Component

We have a bunch of vue-tags here 😁:

The template part:

<template>
  <main class="tabs-enh">
    <div class="tab-enh-headers">
      <div
         v-for="tab in tabs" v-bind:key="tab.name"
         v-on:click="selectTabByName(tab.name)"
         v-on:mouseenter="tab.isHovered = true"
         v-on:mouseleave="tab.isHovered = false"
         v-bind:class="[activeClass(tab), colorClass(tab)]">
        <div v-bind:class="{ 'bg-white' : tab.name == selected }"
          >{{ tab.title }}</div>
      </div>
    </div>

    <div class="tab-enh-spacer"></div>

    <div class="tab-enh-contents">
      <slot></slot>
    </div>
  </main>
</template>

The javascript part:

export default {
  name: 'TabItemEnhanced',
  data() {
    return {
      selected: 'news',
      tabs: this.$children
    };
  },
  mounted() {
    this.selectTabByName(this.selected);
  },
  methods: {
    selectTabByName(tabName) {
      this.selected = tabName;
      this.tabs.forEach(tab => {
        tab.isActive = (tab.name == tabName);
      });
    },
    activeClass : function (tab) {
      return tab.name == this.selected ? 'active' : '';
    },
    colorClass  : function (tab) {
      return tab.name == this.selected ? tab.color : 'bg-gray-700';
    }
  }
}

How does it works? We have already discussed how it works in previous article.

Tab Item Component

The template part:

<template>
     <div
       v-show="isActive"
       v-bind:class="this.color">
       <div class="tab-enh-content"
            v-bind:class="{ 'is-hovered' : this.isHovered }">
         <slot></slot>
       </div>
     </div>
</template>

The javascript part:

export default {
  name: 'TabItemEnhanced',
  props: {
    name:    { required: true },
    title:   { required: true },
    color:   { default: '' }
  },
  data() {
    return {
      isHovered: false,
      isActive: false
    };
  }
}

How does it works? We have already discussed how it works in previous article.

Preview

You can test in your favorite browser:

Vue App: Component Preview: Enhanced: Fully Working

Stylesheet Issue

We need to be carefully manage stylesheet, in an application with multicomponent.

If you just notice, there is a little class name difference here:

<template>
  <main class="tabs-enh">
    <div class="tab-enh-headers">
      …
    </div>

    <div class="tab-enh-spacer"></div>

    <div class="tab-enh-contents">
      …
    </div>
  </main>
</template>

I append addtional -enh as class namespace, so the stylesheet for enhanced tabs is now completely different, with simple tabs counterpart.


Tailwind

If your notebook processor do not mind being a heavy worker, you can also setup Tailwind. There are already many tutorial in th internet, so I wont' repeat my self here.

You can just dropoff the styles:

SimpleLayout

<style scoped>
  @import '../assets/styles/tailwind-simple.css';
</style>

If required, you should adjust the color to match Tailwind color. Such as bg-gray to bg-gray-700. And also beware of purgecss.

SimpleInline

<style scoped>
  @import '../assets/styles/tailwind-simple.css';
</style>

SimpleMockup

<style scoped>
  @import '../../assets/styles/tailwind-simple.css';
</style>

SimpleTabs

<style scoped>
  @import '../../assets/styles/tailwind-simple.css';
</style>

EnhancedTabs

<style scoped>
  @import '../../assets/styles/tailwind-enhanced.css';
  @import '../../assets/css/border-radius.css';
</style>

Preview

You can test in your favorite browser:

Vue App: Component Preview: Enhanced: Tailwind Mobile

Source Examples

You can obtain source examples here:

And that is all.

Conclusion

I think that is all for now.


What’s Next?

Are there other way to do this? We are going to fix the pattern above in the next article.

Consider continue reading [ Tabs - JS - Vue3 Composition ].