Exploding Tabs With Fete

AnyWhichWay
7 min readApr 27, 2017

Fete, the tiny (3.6K) front-end template engine and router based on JavaScript string literals, supports the use of custom tags, template transformations and first class JavaScript components. This tutorial demonstrates the functionality by implementing a exploding tab control.

“What the heck is an exploding tab control?”, you ask. An exploding tab control is a tab control that can be exploded into its component tabs for vertical display on a mobile device or to meet user interaction preferences.

An Exploding Tab Control In Normal Mode
An Exploding Tab Control Fully Exploded With Tab Two Selected

The tutorial will cover:

  1. Defining the tab control as HTML
  2. Styling the tab control for layout
  3. Styling the tab control for state display
  4. Reducing the HTML required to define the tab control
  5. Making the tab control active
  6. Binding the tab control to data and testing it
  7. Turning the tab control into a reusable component

Links to https://jsfiddle.net/ code samples will be provided along the way.

Defining The Tab Control As HTML

Contemporary web browsers automatically support the use of custom tags. They are treated like a div in with display:inline. So lets dive right in and express a tab control in HTML we just make up.

<tabcontrol style="width:50%" id="persontabs">
<tabbar>
<taboption>
<input type="checkbox" name="personal" title="Explode">
<a href="#personal">Personal Info</a>
</taboption>
<taboption>
<input type="checkbox" name="address" title="Explode">
<a href="#address">Address</a>
</taboption>
<taboption>
<input type="checkbox" name="view" title="Explode">
<a href="#view">View</a>
</taboption>
</tabbar>
<tabcontent>
<tab class="tab" name="personal" label="Personal Info" bind="${personal}">
First Name: <input value="${firstName}" data-two-way="true">
Last Name: <input value="${lastName}" data-two-way="true">
</tab>
<tab class="tab" name="address" label="Address" bind="${address}">
City: <input value="${city}" data-two-way="true">
State: <input value="${state}" data-two-way="true">
</tab>
<tab class="tab" name="view" label="View">
${personal.firstName} ${personal.lastName} lives in
${address.city}, ${address.state}.
</tab>
</tabcontent>
</tabcontrol>

Styling The Tab Control For Layout

If we display the above, it is a bit messy:

Un-styled Tab Control jsfiddle

This is simply because we have not provided any style information to override the default inline display or provide lines around the edges. There is nothing particularly sophisticated about the required style definitions:

<style>
tabcontrol {
display:block;
padding:5px;
margin:5px;
}
tabcontrol tabbar taboption {
border-left-style:solid;
border-top-style:solid;
border-right-style:solid;
border-width:1px;
border-top-left-radius:5px;
border-top-right-radius:5px;
padding:5px;
}
tabcontrol tabcontent {
display:block;
border-width: 1;
border-style: solid;
border-top-right-radius:5px;
margin-top: 3px;
}
tab {
display:block;
padding:5px;
margin-top: 5px;
}
</style>
Partially Styled Tab Control jsfiddle

Styling The Tab Control For State Display

We need to define styles that support:

  • italics for selected labels
  • visibility for certain tabs
  • a box for the selected tab content
tabcontrol tabbar taboption[selected="true"] {
font-style:italic;
}
tab[visible="false"] {
display:none;
}
tab[selected="true"] {
border-width: 1;
border-style: solid;
}

The reason the tab style is defined at top level will become evident when we go to reduce the amount of HTML required later in the tutorial.

Below is a modified HTML definition to test the state display styles:

<tabcontrol style="width:50%" id="persontabs">
<tabbar>
<taboption selected="false">
<input type="checkbox" name="personal" title="Explode" checked>
<a href="#personal">Personal Info</a>
</taboption>
<taboption selected="true">
<input type="checkbox" name="address" title="Explode">
<a href="#address">Address</a>
</taboption>
<taboption selected="false">
<input type="checkbox" name="view" title="Explode">
<a href="#view">View</a>
</taboption>
</tabbar>
<tabcontent>
<tab class="tab" name="personal" label="Personal Info" bind="${personal}" selected="false" visible="true" exploded="true">
First Name: <input value="${firstName}" data-two-way="true">
Last Name: <input value="${lastName}" data-two-way="true">
</tab>
<tab class="tab" name="address" label="Address" bind="${address}" visible="true" exploded="false" selected="true">
City: <input value="${city}" data-two-way="true">
State: <input value="${state}" data-two-way="true">
</tab>
<tab class="tab" name="view" label="View" visible="false" exploded="false" selected="false">
${personal.firstName} ${personal.lastName} lives in
${address.city}, ${address.state}.
</tab>
</tabcontent>
</tabcontrol>
Fully Styled Tab Control With State jsfiddle

Now for the fun part, reducing the amount of HTML and making things live! When we are done the section of code required to define and render a reusable person editor will look like this (excluding the definition of the controller and transformTabConrol):

const fete = new Fete(),
html = document.getElementById("persontabs").innerHTML,
model = {
personal:
{firstName:'Joe',lastName:'Jones'},
address:
{city:'Seattle',state:'WA'}
};
fete.define("tabcontrol",{transform:transformTabControl});
const PersonControl = fete.createComponent("PersonControl",
html,controller);
new HTMLTabControl().render("#app",model);

Reducing The HTML Required To Define The Tab Control

It should be evident that there is sufficient info in the tab content definitions to generate the control options and labels at the top of the tab control. Fete provides a custom tag transformation capability just for this type of thing. To associate an HTML tag with a transformation, the tag name is associated with a function that takes an HTMLElement as an argument. The function may modify and return the same element or return a new element. The transformation function will be called for each occurrence of the tag when Fete compiles a page the first time the page is loaded. Below is the transformation function for tabcontrol.

const transformTabControl = function(view) {
// see if there is a tabcontent section
let tabcontent = view.querySelector("tabcontent");
// create one if there isn't
tabcontent || (tabcontent=document.createElement("tabcontent"));
// find all the tabs in the view
const tabs = [].slice.call(view.querySelectorAll("tab"));
// enable tracking is one is defined as selected
let hasselected = 0;
// move the tabs to the tabcontent area and note which is selected
tabs.forEach((tab,i) => {
tab.classList.add("tab");
const selected = tab.getAttribute("selected");
hasselected = ((selected==="" || selected==="true")
? i
: hasselected);
if(tabs.parentElement!==tabcontent) tabcontent.appendChild(tab);
});
// set the style driving attributes
tabs[hasselected].setAttribute("selected",true);
tabs[hasselected].setAttribute("visible",true);
// find the first input element in the selected tab
const input = tabs[hasselected].querySelector("input");
// set the focus to the first input when rendering is complete
if(input) setTimeout(() => input.focus());
// see if there is a tabbar
let tabbar = view.querySelector("tabbar");
// if there is no tabbar
if(!tabbar) {
// create one
tabbar = document.createElement("tabbar");
// add selector option tabs
for(let i=0;i<tabs.length;i++) {
const taboption = document.createElement("taboption"),
// use Fete convenience function to generate innerHTML
html = fete.interpolate("<input type='checkbox' name='${name}' title='Explode'><a href='#${name}'>${label}</a>",tabs[i]);
taboption.innerHTML = html;
// set selected if current index is the same as selected tab
if(i===hasselected) taboption.setAttribute("selected",true);
// append the option to the tabbar
tabbar.appendChild(taboption);
}
}
// if content area did not exist, it will need to be appended
if(tabcontent.parentElement!==view) view.appendChild(tabcontent);
// put the tabbar above the content
view.insertBefore(tabbar,tabcontent);
// loop through the tabs and make visible if selected or exploded
tabs.forEach(tab => {
if(tab.getAttribute("selected")==="true" ||
tab.getAttribute("exploded")==="true") {
tab.setAttribute("visible",true);
} else tab.setAttribute("visible",false);
});
// return the modified view
return view;
};

With this function in place, the HTML to define the control can be reduced to:

<tabcontrol style="width:50%" id="persontabs">
<tab name="personal" label="Personal Info" bind="${personal}">
First Name: <input value=${firstName}>
Last Name: <input value=${lastName}>
</tab>
<tab name="address" label="Address" bind="${address}">
City: <input value="${city}">
State: <input value="${state}">
</tab>
<tab class="tab" name="view" label="View">
${personal.firstName} ${personal.lastName} lives in
${address.city}, ${address.state}.
</tab>
</tabcontrol>

Making The Tab Control Active

Fete provides multiple options for routing and control including cascading lookup tables, custom routers and functional pipelines. For this control a function will be used. The function can have the signature (event,model, property,value) to support passing modified data beyond the bounds of Fete. For this tutorial we will be using two way data binding, so our controller only needs to know about the event that invoked it.

const controller = function(event) {
// grab the tabs
const tabs = event.currentTarget.querySelectorAll("tab");
// the event will be a hash if a tab option has been clicked
if(event.target.hash) {
// the option control will be an ancestor of the link
const taboption = event.target.closest("taboption");
// the name of the tab will be the same as the hash
selected = event.target.hash.substring(1);
// handle the case where an embedded link may have been clicked
// rather than a tab, i.e. taboption will be null
if(taboption) {
// set the option to selected
taboption.setAttribute("selected",true);
// find the tabbar so we can set other options to unselected
const tabbar = taboption.closest("tabbar");
// updates selcted states for tabs and options
tabs.forEach((tab,i) => {
if(tab.getAttribute("name")===selected) {
tab.setAttribute("selected",true);
tabbar.children[i].setAttribute("selected",true);
const input = tab.querySelector("input");
if(input) setTimeout(() => input.focus());
} else {
tab.setAttribute("selected",false);
tabbar.children[i].setAttribute("selected",false);
}
});
}
// the event will be a checkbox if exploding
} else if(event.target.type==="checkbox") {
tabs.forEach(tab => {
if(tab.getAttribute("name")===event.target.name) {
tab.setAttribute("exploded",event.target.checked);
}
});
// user may have clicked on a section of the displayed tabs
} else {
const closest = (event.target.closest
? event.target.closest("tab") : null);
// if a tab is found above the click
if(closest) {
// find the bar and set the selection state
const tabbar =
closest.closest("tabcontrol")
.querySelector("tabbar");
tabs.forEach((tab,i) => {
if(tab===closest) {
tab.setAttribute("selected",true);
tabbar.children[i].setAttribute("selected",true);
} else {
tab.setAttribute("selected",false);
tabbar.children[i].setAttribute("selected",false);
}
});
}
}
// update the visibility
tabs.forEach(tab => {
if(tab.getAttribute("selected")==="true" ||
tab.getAttribute("exploded")==="true") {
tab.setAttribute("visible",true);
} else tab.setAttribute("visible",false);
});
}

Binding The Tab Control To Data And Testing It

Prior to creating a reusable component, we can bind the existing HTML to a model and the controller to test it:

const fete = new Fete(),
model = {
personal:
{firstName:’Joe’,lastName:’Jones’},
address:
{city:’Seattle’,state:’WA’}
};
fete.define(“tabcontrol”,{transform:transformTabControl});
fete.mvc(model,”#persontabs”,controller,{reactive:true});

The a working example is available on jsfiddle.

Turning The Tab Control Into A Reusable Component

Turning the tab control into a reusable component requires wrapping the HTML in a template element so it does no display directly and adding just one line of code to create the component and another to instantiate and render it. createComponent takes a name, the HTML defining the component and a controller as arguments. render takes the target div selector and a model to bind as arguments.

const fete = new Fete(),
html = document.getElementById("persontabs").innerHTML,
model = {
personal:
{firstName:’Joe’,lastName:’Jones’},
address:
{city:’Seattle’,state:’WA’}
};
fete.define("tabcontrol",{transform:transformTabControl});
const PersonControl =
fete.createComponent("PersonControl",html,controller);
new PersonControl().render("#app",model);

So our working example on jsfiddle is complete!

For more information on Fete visit https://anywhichway.github.io/fete/

--

--

AnyWhichWay

Changing Possible ... currently from the clouds around Seattle.