Tabs Widget page

Create a tabs widget using your own version of jQuery.

Overview

In this part, we will:

  • Create a $.fn.tabs widget.

Slides

Exercise: $.fn.tabs

The problem

Create a progressively enhanced tabs widget. It will be called like:

$("#breeds, #tech").tabs();

Notice that .tabs() can be called on multiple elements. An independent tabs widget should be created on each element in the collection. The elements in the collection should be <ul> elements with the following structure:

<ul id="tech">
  <li><a href="#canjs">CanJS</a></li>
  <li><a href="#stealjs">StealJS</a></li>
  <li><a href="#donejs">DoneJS</a></li>
</ul>
<div id="canjs">
  <a href="https://canjs.com">CanJS</a>
</div>
<div id="stealjs">
  <a href="https://stealjs.com">StealJS</a>
</div>
<div id="donejs">
  <a href="https://donejs.com">DoneJS</a>
</div>

The <ul> elements will have <li> children which serve as the buttons. Each <li> must contain an <a> element. The <a> element’s href attributes reference the id of a tab element to show when the corresponding <li> button element is clicked.

For example when this <li> is clicked:

<li><a href="#stealjs">StealJS</a></li>

The following tab element should be shown:

<div id="stealjs">
  <a href="https://stealjs.com">StealJS</a>
</div>

Finally:

  • Each <ul> should have tabs added to its className.
  • Each tab element should have tab added to its className.

The following CodePen can be used to complete the exercise.

<style>
/* adapted from https://codepen.io/talleyran/pen/XoPLvy */
body {
  font-family: Arial;
  font-size: 18px;
}
.tabs {
  display: flex;
  margin: 1rem 0 0.4rem 0;
  padding: 0;
  position: relative;
}
.tabs li {
  list-style: none;
  padding: 0;
  line-height: 1.3rem;
}
.tabs li a {
  color: #173e4a;
  padding: 1rem 1.7rem;
  margin-left: -1rem;
  text-decoration: none;
  position: relative;
}
.tabs li a:link, .tabs li a:visited, .tabs li a:hover, .tabs li a:active, .tabs li a::selection {
  outline: 0 !important;
  background: none !important;
}
.tabs li:first-child a {
  margin-left: 0.5rem;
}
.tabs li a:before {
  content: "";
  position: absolute;
  top: -0.5rem; left: 0;
  background: #8BC3E8;
  height: 100%;
  width: 100%;
  z-index: -1;
  border-top-left-radius: 1rem;
  border-top-right-radius: 1rem;
  transform: perspective(0.2rem) rotateX(2deg);
  transform-origin: bottom;
  box-shadow: 1px 2px 2px #293140;
}
.tabs li.active {
  z-index: 1;
}
.tabs li.active a {
  cursor: default;
}
.tabs li.active a:before {
  background: #98F1FF;
}
.tabs li:not(.active) a:hover:before {
  background: #A5C3FF;
}

.tab {
  min-width: 30rem;
  background: #98F1FF;
  color: #173e4a;
  border-radius: 0.2rem;
  padding: 1.5rem;
  z-index: 100;
  position: relative;
}
.tab:before {
  display: block;
  position: absolute;
  top: 1px; bottom: 0;
  left: 0; right: 0;
  z-index: -1;
  content: "";
  border-radius: 0.2rem;
  box-shadow: 1px 1px 1px #293140;
}
img {width: 400px;}
</style>
<script src="//bitovi.github.io/academy/static/scripts/my-jquery.js"></script>

<ul id='breeds'>
    <li><a href="#beagles">Beagles</a></li>
    <li><a href="#doberman">Doberman</a></li>
    <li><a href="#boxer">Boxer</a></li>
</ul>
<div id='beagles'>
  Beagle: <img src='//bitovi.github.io/academy/static/img/dom/beagle.jpg'/>
</div>
<div id='doberman'>
  Doberman: <img src='//bitovi.github.io/academy/static/img/dom/doberman.jpg'/>
</div>
<div id='boxer'>
  Boxer: <img src='//bitovi.github.io/academy/static/img/dom/boxer.jpg'/>
</div>

<ul id='tech'>
    <li><a href="#canjs">CanJS</a></li>
    <li><a href="#stealjs">StealJS</a></li>
    <li><a href="#donejs">DoneJS</a></li>
</ul>
<div id='canjs'>
  <a href="https://canjs.com">CanJS</a>
</div>
<div id='stealjs'>
  <a href="https://stealjs.com">StealJS</a>
</div>
<div id='donejs'>
  <a href="https://donejs.com">DoneJS</a>
</div>

<script type="module">
const $ = window.$;

$.fn.tabs = function(){

};

$("#breeds, #tech").tabs()
</script>

What you need to know

  • An Immediately Invoked Function Expression (IFFE) can be used to prevent variables and functions from being added to the global scope:

    (function () {
      function activate(li) {
        // DO STUFF
        return li;
      }
    
      $.fn.tabs = function () {
        // can use activate
        activate();
      };
    })();
    
    // can NOT use activate
    activate(); //-> throws an error
    
  • The following jQuery functions will be useful:

    • $("#selector") - Get a collection of elements using a CSS selector.
    • $([element]) - Create a jQuery collection from an array of elements.
    • collection.children() - Return a collection of all direct descendants of elements in the source collection.
    • collection.find(selector) - Using a CSS selector, find elements descended from elements in the source collection.
    • collection.addClass(className) - Add a class name to elements in the collection.
    • collection.removeClass(className) - Remove elements in the collection.
    • collection.show() - Show elements in the collection.
    • collection.hide() - Hide elements in the collection.
    • collection.bind(event, handler) - Listen to an event.
    • $.each(collection, cb(i, value) ) - Loop through an array-like collection of elements.

Completed Solution

Click to see completed solution

<style>
/* adapted from https://codepen.io/talleyran/pen/XoPLvy */
body {
  font-family: Arial;
  font-size: 18px;
}
.tabs {
  display: flex;
  margin: 1rem 0 0.4rem 0;
  padding: 0;
  position: relative;
}
.tabs li {
  list-style: none;
  padding: 0;
  line-height: 1.3rem;
}
.tabs li a {
  color: #173e4a;
  padding: 1rem 1.7rem;
  margin-left: -1rem;
  text-decoration: none;
  position: relative;
}
.tabs li a:link, .tabs li a:visited, .tabs li a:hover, .tabs li a:active, .tabs li a::selection {
  outline: 0 !important;
  background: none !important;
}
.tabs li:first-child a {
  margin-left: 0.5rem;
}
.tabs li a:before {
  content: "";
  position: absolute;
  top: -0.5rem; left: 0;
  background: #8BC3E8;
  height: 100%;
  width: 100%;
  z-index: -1;
  border-top-left-radius: 1rem;
  border-top-right-radius: 1rem;
  transform: perspective(0.2rem) rotateX(2deg);
  transform-origin: bottom;
  box-shadow: 1px 2px 2px #293140;
}
.tabs li.active {
  z-index: 1;
}
.tabs li.active a {
  cursor: default;
}
.tabs li.active a:before {
  background: #98F1FF;
}
.tabs li:not(.active) a:hover:before {
  background: #A5C3FF;
}

.tab {
  min-width: 30rem;
  background: #98F1FF;
  color: #173e4a;
  border-radius: 0.2rem;
  padding: 1.5rem;
  z-index: 100;
  position: relative;
}
.tab:before {
  display: block;
  position: absolute;
  top: 1px; bottom: 0;
  left: 0; right: 0;
  z-index: -1;
  content: "";
  border-radius: 0.2rem;
  box-shadow: 1px 1px 1px #293140;
}
img {width: 400px;}
</style>
<script src="//bitovi.github.io/academy/static/scripts/my-jquery.js"></script>

<ul id='breeds'>
    <li><a href="#beagles">Beagles</a></li>
    <li><a href="#doberman">Doberman</a></li>
    <li><a href="#boxer">Boxer</a></li>
</ul>
<div id='beagles'>
  Beagle: <img src='//bitovi.github.io/academy/static/img/dom/beagle.jpg'/>
</div>
<div id='doberman'>
  Doberman: <img src='//bitovi.github.io/academy/static/img/dom/doberman.jpg'/>
</div>
<div id='boxer'>
  Boxer: <img src='//bitovi.github.io/academy/static/img/dom/boxer.jpg'/>
</div>

<ul id='tech'>
    <li><a href="#canjs">CanJS</a></li>
    <li><a href="#stealjs">StealJS</a></li>
    <li><a href="#donejs">DoneJS</a></li>
</ul>
<div id='canjs'>
  <a href="https://canjs.com">CanJS</a>
</div>
<div id='stealjs'>
  <a href="https://stealjs.com">StealJS</a>
</div>
<div id='donejs'>
  <a href="https://donejs.com">DoneJS</a>
</div>

<script type="module">
const $ = window.$;

(function(){
  function tabContent(li) {
    return $(li.find("a").attr("href"));
  }

  function activate(li) {
    li.addClass("active");
    tabContent(li).show();
    return li;
  }
  function deactivate(li) {
    li.removeClass("active");
    tabContent(li).hide();
    return li;
  }

  $.fn.tabs = function(){
    this.addClass("tabs");
    return $.each(this, function(i, element) {
      var active,
          $lis = $([ element ]).children();

      $.each($lis, function(i, li) {
        var $li = $([ li ]);
        var $tab = tabContent($li);
        $tab.addClass("tab");

        if (i === 0) {
          active = activate($li);
        } else {
          $tab.hide();
        }
      });

      $lis.bind("click", function(event) {
        deactivate( active );
        active = activate( $([ this ]) );
        event.preventDefault();
      });
    });
  };
})();

$("#breeds, #tech").tabs()
</script>