Where to Discuss?

Local Group

Preface

Goal: Constructing component with Vue.js.

Article Steps

The Vue.js is divided into two parts.

  • Inline Vue.js, then

  • Extracted Vue.js.

The first one is very similar to Alpine.js counterpart, but the extracted component is, a whole new level.

Examples

For tutorial purpose we need two layouts:

  1. Simple layout, to explain Vue.js.

  2. Enhance layout, as real live example.


Simple Tabs : Inline Structure

Document: HTML Head

I don’t think I should explain it over and over again.

Document: HTML Body

It is very similar with alpine. In fact I copy-paste this vue from alpine. The difference is vue is using v- prefix, while alpine is using x- prefix.

  <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>

<script>
new Vue({
  el:   '.tabs',
  data: { selected: 'news' }
})
</script>

And vue doesn’t have x-data, but instead we do this at the bottom of the script:

new Vue({
  el:   '.tabs',
  data: { selected: 'news' }
})

It is clear.


Mockup Tabs : Constructing Component

We are going to go back to mockup mode. I mean I just want to explain how to render template, but without any functionality yet.

Document: HTML Head

We can reuse previous tailwind stylesheet.

  <title>Mockup Tabs - Vue.js (component)</title>
  <link rel="stylesheet" type="text/css" 
        href="../css/03-tailwind-simple.css">
  <script src="../js/vendors/vue.min.js"></script>
  <script src="../js/custom/vue-mockup.js"></script>

For simplicity I separate the vue part into external file. No inline javascript at all in HTML body.

Document: HTML Body

This is very different with all of the previous articles:

This is already a complete body.

  <div id="tabs">
     <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>
  </div>

<script>
new Vue({ el: '#tabs' })
</script>

We have this new 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 Template

Vue handle component template in this fashioned:

Component Template

Vue handle component template in this fashioned:

Vue.component('tabs', {
  template: `
  <main class="tabs">
  </main>
  `,

  

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

``

First we have the HTML part, then we have the declarative javascript parts.

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

Vue component require the data to be a method rather than object. This is why we have the data() { return … } format, instead of data: { … }.

The Parent Template: Tabs

The tab header part, could be transformed into something like this:

  <main class="tabs">
    <div class="tab-headers">
      <div v-for="tab in tabs">{{ tab.title }}
      </div>
    </div>
  </main>

Vue goes further with <slot>. Note that we talk about tabs element, not just tab-header only or tab-content only. Now the real template can be shown as below:

  <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>

Now we know how to pass content from the template to tab-content.

The Child Template: Tab

How to handle tab-content? We also need component to handle the child tab. For mockup reason I only show home content.

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

Note that this child component also handle data, for both tab-header and, tab-content.

Tab: Properties

We should start from properties from the child element. Because we are going to use the properties in parent method.

Vue.component('tab', {
  template: ,

  props: {
    name:  { required: true },
    title: { required: true }
  }
});

Confused?

It takes adaptation from the conventional thinking, to modern website logic. The next part will explain more.

Complete Code

Now we can gathered both as below code:

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
    };
  }
});

Vue.component('tab', {
  template: `
     <div class="bg-gray-700" v-show="name == 'home'">
       <slot></slot>
     </div>
  `,

  props: {
    name:  { required: true },
    title: { required: true }
  }
});

``

Preview

You can open your favorite browser to test the result.

Tabs Component: Mockup Template: Vue: Mobile

We still have no functionality yet. Nor fancy color.


Simple Tabs : Constructing Component

On the other hand, component is different. It looks like x-for template, but more advance.

Document: HTML Head

We can reuse previous tailwind stylesheet.

  <title>Simple Tabs - Vue.js (component)</title>
  <link rel="stylesheet" type="text/css" 
        href="../css/03-tailwind-simple.css">
  <script src="../js/vendors/vue.min.js"></script>
  <script src="../js/custom/vue-simple.js"></script>

Document: HTML Body

The same with mockup document. No change at all.

The Template: Tabs

The real template can be shown as below:

  <main class="tabs">
    <div class="tab-headers">
      <div
         v-for="tab in tabs"
         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>

We have v-on to handle event. And v-bind to handle class names.

The Template: Tab

How to handle tab-content? We also need component to handle the child tab.

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

Tab: Properties

Just like previous, but we need additional color. And we also need to manage active class.

Vue.component('tab', {
  template: ,

  props: {
    name:  { required: true },
    title: { required: true },
    color: { default: '' }
  },

  data() {
    return {
      isActive: false
    };
  }
});

We have these three name, title, color for each tab. That is defined in each ekement as example below:

        <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: On Click Event

As written in the template.

      <div
         v-for="tab in tabs"
         v-on:click="selectTabByName(tab.name)"
        >{{ tab.title }}
      </div>

The actual javascript can be written as below:

  methods: {
    selectTabByName(tabName) {
      this.selected = tabName;
      this.tabs.forEach(tab => {
        tab.isActive = (tab.name == tabName);
      });
    }
  }

If you wonder what each variable are, you can have a look at this complete javascript declaration:

Vue.component('tabs', {
  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);
      });
    }
  }
});

As shown above, all the logic written in a more declarative way.

Tabs: Class Binding

Along with the click event, we have class binding. As written in the template.

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

This can also be written in declarative way.

Vue.component('tabs', {
  data() {
    return {
      selected: 'news'
    };
  }, 
  methods: {
    activeClass : function (tab) {
      return tab.name == this.selected ? 'active' : '';
    },
    colorClass  : function (tab) {
      return tab.name == this.selected ? tab.color : 'bg-gray-700';
    }
  }
});

Preview

You can open your favorite browser to test the result.

Tabs Component: Simple Template: Vue: Mobile

Complete Code

As a summary, here is the complete code:

Vue.component('tabs', {
  template: `
  <main class="tabs">
    <div class="tab-headers">
      <div
         v-for="tab in tabs"
         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>
  `,
  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';
    }
  }
});

Vue.component('tab', {
  template: `
     <div
       v-show="isActive"
       v-bind:class="this.color">
       <slot></slot>
     </div>
  `,
  props: {
    name:  { required: true },
    title: { required: true },
    color: { default: '' }
  },
  data() {
    return {
      isActive: false
    };
  },
});

``

I give the complete code, just in case you miss something.


Enhanced Tabs: Inline Structure

How about real live component?

Document: HTML Head

I don’t think I should explain it over and over again.

Document: HTML Body

It is very similar with previous one. But with more attached complexity.

  <main class="tabs">
    <div class="tab-headers">
      <div v-on:click="selected = 'home'; hovered = selected;"
           v-on:mouseenter="hovered = (selected == 'home') ? selected : false"
           v-on:mouseleave="hovered = false"
           v-bind:class="{ 
             'active bg-blue-500' : selected == 'home', 
             'bg-gray-700' : selected != 'home' }">
        <div v-bind:class="{ 'bg-white' : selected == 'home' }"
          >Home</div>
      </div>
      …
    </div>

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

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

<script>
new Vue({
  el:   '.tabs',
  data: { selected: 'news', hovered: false }
})
</script>

Do not worry. We are not going to use it anyway. We are going to jump right into templates.


Enhanced Tabs : Constructing Component

Scale up without complexity.

Just like before, but unlike small lightweight alpine, we can add functionality without too many complexity.

How do we implement these two features 🤔?

  • hover and,

  • bg-white.

Document: HTML Head

We can reuse previous tailwind stylesheet.

  <title>Enhanced Tabs - Vue.js (component)</title>
  <link rel="stylesheet" type="text/css" href="../css/04-tailwind-enhanced.css">
  <link rel="stylesheet" type="text/css" href="../css/02-border-radius.css">
  <script src="../js/vendors/vue.min.js"></script>
  <script src="../js/custom/vue-enhanced.js"></script>

Document: HTML Body

Surprise?

This is exactly the same with the simple component. Nothing has been changed at all.

  <div id="tabs">
     <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>
  </div>

<script>
new Vue({ el: '#tabs' })
</script>

Now you can imagine how scalable Vue is.

The Template: Tabs

Vue handle component template in this fashioned:

The tab header part, could be transformed into something like this:

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

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

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

That is all. The bg-white feature ha been solved.

Tab: Properties

We need to add isHovered data.

Vue.component('tab', {
  data() {
    return {
      isHovered: false,
      isActive: false
    };
  }
});

Then write the child tab template, so the content tab background color also be changed, when the header tab get hovered.

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

And also add the events in parent tabs template.

    <div class="tab-headers">
      <div
         v-for="tab in tabs"
         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>

And that is all. Done.

Preview

You can open your favorite browser to test the result.

Tabs Component: Enhanced Template: Vue: Mobile

Complete Code

It is a good time to compare the code with jQuery or plain javascript.

As a summary, here is the complete code:

Vue.component('tabs', {
  template: `
  <main class="tabs">
    <div class="tab-headers">
      <div
         v-for="tab in tabs"
         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-spacer"></div>

    <div class="tab-contents">
      <slot></slot>
    </div>
  </main>
  `,
  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';
    }
  }
});

Vue.component('tab', {
  template: `
     <div
       v-show="isActive"
       v-bind:class="this.color">
       <div class="tab-content"
            v-bind:class="{ 'is-hovered' : this.isHovered }">
         <slot></slot>
       </div>
     </div>
  `,
  props: {
    name:    { required: true },
    title:   { required: true },
    color:   { default: '' }
  },
  data() {
    return {
      isHovered: false,
      isActive: false
    };
  }
});

``

Writing Vue.js is like writing configuration, while jQuery or plain javascript is writing logic.

Since Vue.js has systematically manage the logic, all we need is to declare the configuration.


What’s Next?

It is a good time to embrace modern web development. Starting with vue-cli along with NPM bundler.

Consider continue reading [ Tabs - JS - Vue Router ].