Where to Discuss?

Local Group

Preface

Goal: Attaching event to element with Alpine.js.

Article Steps

The alpine.js is divided into two parts.

  • Inline Alpine.js, then

  • Extracted Alpine.js.

The first one is a little bit of messy and so obstrusive, but once we extract properly, the document would looks tidy.

Examples

For tutorial purpose we need two layouts:

  1. Simple layout, to explain Alpine.js.

  2. Enhance layout, as real live example.

Credits

For my friend ‘Sholeh Udin, who taught me to write proper alpine.js`.

Preview

Tabs Component: Enhanced Layout: Alpine


Simple Tabs : Inline Structure

Document: HTML Head

We can reuse previous tailwind stylesheet.

  <title>Simple Tabs - jQuery</title>
  <link rel="stylesheet" type="text/css" 
        href="../css/03-tailwind-simple.css">
  <script src="../js/vendors/alpine.min.js"></script>

Yes, we do not need any external javascript yet. All will be written inline.

Document: HTML Body: Tab Headers

Compared with plain javascript and jQuery, alpine.js is radically different. In fact most modern web framework using this style.

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

    …
  </main>

So simple, that there is no need to explore DOM at all.

How Does It Works?

We are going to initialize stuff with x-data, so we do not need to create logic to initialize stuff. In contrast with click event in previous unobtrusive example.

Then there is this x-bind:class to manage class addiition, class removal, and class toggling.

The beauty of the event x-on:click is, we just set variable. And that’s all it takes to get this code works.

Javascript: Handling Headers Event

      <div x-on:click="tab = 'home'"
           x-bind:class="{ 
             'active bg-blue-500' : tab == 'home', 
             'bg-gray-700' : tab != 'home' }">
        Home
      </div>

Document: HTML Body: Tab Contents

The same writing style, also applied the tab contents.

    <div class="tab-contents">
      <div x-show="tab == 'home'"
           x-bind:class="{ 'bg-blue-500' : tab == 'home' }">
          <h3>Home</h3>
          <p>Lorem ipsum dolor sit amet,
             consectetur adipiscing elit.
             Quisque in faucibus magna.</p>
      </div>
      <div x-show="tab == 'team'"
           x-bind:class="{ 'bg-teal-500' : tab == 'team' }">
          <h3>Team</h3>
          <p>Nulla luctus nisl in venenatis vestibulum.
             Nam consectetur blandit consectetur.</p>
      </div>
      <div x-show="tab == 'news'"
           x-bind:class="{ 'bg-red-500' : tab == 'news' }">
          <h3>News</h3>
          <p>Phasellus vel tempus mauris,
             quis mattis leo.
             Mauris quis velit enim.</p>
      </div>
      <div x-show="tab == 'about'"
           x-bind:class="{ 'bg-orange-500' : tab == 'about' }">
          <h3>About</h3>
          <p>Interdum et malesuada fames ac ante
             ipsum primis in faucibus.
             Nulla vulputate tortor turpis,
             at eleifend eros bibendum quis.</p>
      </div>
    </div>

I know it is long. But that is all.

Your code should be working by now.

Javascript: Handling Headers Event to Tab Content

Consider this example below, for home tab:

  <main class="tabs" x-data="{ tab: 'news' }">
    <div class="tab-headers">
      <div x-on:click="tab = 'home'">
        Home
      </div>
      …
    </div>

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

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

Preview

The same as previous example.


Simple Tabs : Using Template

We can go further with <template x-for="">.

The Riddle: Issue with Alpine!

Can we make this simpler?

We have to write down the declaration, for each element such as below:

    <div class="tab-headers">
      <div x-on:click="tab = 'home'"
           x-bind:class="{ 
             'active bg-blue-500' : tab == 'home', 
             'bg-gray-700' : tab != 'home' }"
          >Home
      </div>
      <div x-on:click="tab = 'team'"
           x-bind:class="{ 
             'active bg-teal-500' : tab == 'team', 
             'bg-gray-700' : tab != 'team' }"
          >Team
      </div>
      <div x-on:click="tab = 'news'"
           x-bind:class="{ 
             'active bg-red-500' : tab == 'news', 
             'bg-gray-700' : tab != 'news' }"
          >News
      </div>
      <div x-on:click="tab = 'about'"
           x-bind:class="{ 
             'active bg-orange-500' : tab == 'about', 
             'bg-gray-700' : tab != 'about' }"
          >About
      </div>
    </div>

Common Solution

Consider transform from code above into template.

First we setup the data:

    x-data="{ 
      tab: 'news',
      list: {
        1: { name: 'home',  title: 'Home' },
        2: { name: 'team',  title: 'Team' },
        3: { name: 'news',  title: 'News' },
        4: { name: 'about', title: 'About' }
      }
    }"

Then we define the template.

    <div class="tab-headers">
    <template x-for="item in Object.values(list)" :key="item">
      <div
         x-on:click="tab = item.name"
         x-bind:class="{
           'active bg-blue-500' : tab == item.name,
           'bg-gray-700' : true
         }"
         x-text="item.title"></div>
    </template>
    </div>

As shown above, we cannot process dynamic class. We have to use active bg-blue-500. While all I wanted is something like:

         x-bind:class="{
           item.color : tab == item.name,
           'bg-gray-700' : true
         }"
  </main>

The color result is not match, between left and right, as shown below:

Tabs Component: Simple Template: Hardcoded Color

Solving Dynamic Class

Actually we have a workaround for this. Use your friendly neighbourhood tools, the brain and the logic. All we need to do is replacing with these lines below:

         x-bind:class="{ 
           'active bg-blue-500'   : item.name == 'home'  && tab =='home',
           'active bg-teal-500'   : item.name == 'team'  && tab =='team', 
           'active bg-red-500'    : item.name == 'news'  && tab =='news', 
           'active bg-orange-500' : item.name == 'about' && tab =='about', 
           'bg-gray-700' : true
         }"

And voila, we have the right color, right.

Tabs Component: Simple Template: Dynamic Class Workaround

Complete Template

There are limitation for my requirement. But there is also workaround. I guess alpine.js is good enough for simple project.

The complete template can written as below codes:

  <main class="tabs" x-data="{ 
      tab: 'news',
      list: {
        1: { name: 'home',  title: 'Home' },
        2: { name: 'team',  title: 'Team' },
        3: { name: 'news',  title: 'News' },
        4: { name: 'about', title: 'About' }
      }
    }">
    <div class="tab-headers">
    <template x-for="item in Object.values(list)" :key="item">
      <div
         x-on:click="tab = item.name"
         x-bind:class="{ 
           'active bg-blue-500'   : item.name == 'home'  && tab =='home',
           'active bg-teal-500'   : item.name == 'team'  && tab =='team', 
           'active bg-red-500'    : item.name == 'news'  && tab =='news', 
           'active bg-orange-500' : item.name == 'about' && tab =='about', 
           'bg-gray-700' : true
         }"
         x-text="item.title"></div>
    </template>
    </div>

   …
  </main>

Document: HTML Body: Tab Contents

By this time of this writing, there is no improvement for tab contents.


Enhanced Tabs: Structure

How about real live component? The features that we add are just hover and bg-white. But it is going to be messier than above.

Document: HTML Head

We can add the javascript at the beginning. We still can reuse previous stylesheet.

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Tabs - Alpine.js (inline)</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/alpine.min.js"></script>

Document: HTML Body: Initialization

  <main class="tabs" x-data="{ tab: 'news', hover: false }">
    /* … elements here … */
  </main>
  • .

Document: HTML Body: Tab Headers

We have the third level div. along with the x-on:mouseenter and x-on:mouseleave event.

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

Whoaaa… This looks complex.

Document: HTML Body: Tab Content

We also have the third level div here.

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

I also feel, not comfortable with this. But we have templates right?


Enhanced Tabs : Using Template

Apply the <template x-for=""> further

The Solution

Of course we can make this simpler.

  <main class="tabs" x-data="{ 
      tab: 'news',
      hover: false,
      list: {
        1: { name: 'home',  title: 'Home' },
        2: { name: 'team',  title: 'Team' },
        3: { name: 'news',  title: 'News' },
        4: { name: 'about', title: 'About' }
      }
    }">
    <div class="tab-headers">
    <template x-for="item in Object.values(list)" :key="item">
      <div
         x-on:click="tab = item.name; hover = tab;"
         x-on:mouseenter="hover = (item.name == tab) ? tab : false"
         x-on:mouseleave="hover = false"
         x-bind:class="{ 
           'active bg-blue-500'   : item.name == 'home'  && tab =='home',
           'active bg-teal-500'   : item.name == 'team'  && tab =='team', 
           'active bg-red-500'    : item.name == 'news'  && tab =='news', 
           'active bg-orange-500' : item.name == 'about' && tab =='about', 
           'bg-gray-700' : true
         }">
        <div
          x-bind:class="{ 'bg-white' : item.name == tab, '': true }"
          x-text="item.title"></div>
       </div>
    </template>
    </div>

    …
  </main>

There is nothing new here except this line below:

        <div
          x-bind:class="{ 'bg-white' : item.name == tab, '': true }"
          x-text="item.title"></div>
       </div>

Logic is your friend.

Preview

You can open your favorite browser to test the result.

Tabs Component: Enhanced Template: Alpine: Tablet


What’s Next?

We need to extract the alpine.js attributes to separate javascript class.

Consider continue reading [ Tabs - JS - Alpine.js (extracted) ].