In part 9 (the last part) of converting jQuery to vanilla JavaScript, we create a tabs and a panels object, and organise all of the functions in there instead.
A clear division between the functions
Here is the code that we’re starting with:
/*jslint browser */
(function iife() {
function removeAllActive() {
const tabs = document.querySelectorAll(".tabs a");
tabs.forEach(function removeClass(tab) {
tab.classList.remove("active");
});
}
function updateTabs(tab) {
removeAllActive();
tab.classList.add("active");
tab.blur();
}
function getBackgroundColor(tab) {
const tabStyle = window.getComputedStyle(tab);
return tabStyle["background-color"];
}
function setBackgroundColor(containerSelector, cssColor) {
const containers = document.querySelectorAll(containerSelector);
containers.forEach(function setBackgroundColor(container) {
container.style.setProperty("background-color", cssColor);
});
}
function hidePanels() {
const panels = document.querySelectorAll(".panel");
panels.forEach(function (panel) {
panel.classList.add("hide");
});
}
function showPanel(tab) {
const tabColor = getBackgroundColor(tab);
setBackgroundColor(".panelContainer", tabColor);
const panel = document.querySelector(tab.hash);
panel.classList.remove("hide");
panel.classList.add("fade-in");
}
function tabClickHandler(evt) {
var tab = evt.target;
evt.preventDefault();
hidePanels();
updateTabs(tab);
showPanel(tab);
}
const tabLinks = document.querySelectorAll(".tabs a");
tabLinks.forEach(function addClickHandler(tab) {
tab.addEventListener("click", tabClickHandler);
});
tabLinks[0].click();
}());
Looking at the functions we’ve been collecting, there are two main types of functions. Some that are to do with tabs, and others that are to do with panels.
- tab functions: removeAllActive, updateTabs, getBackgroundColor, tabClickHandler
- panel functions: setBackgroundColor, hidePanels, showPanel
The getBackgroundColor and setBackgroundColor functions are perhaps the strongest reason to separate these two groups, because at first glance they seem related but one deals with tabs, and the other deals with panels.
They don’t belong together as their function names imply, so dividing the functions into tabs and panels objects helps to give us a better sense about things.
Create a tabs object
Because we want to group those tab-related functions together, and the panel-related functions together, we can start with the tabs.
There’s a fast way and a slow way to do this. The slow way is to create a separate tabs object and move things in one by one, checking every step as you go.The faster way is to create a tabs module, and move all of the related functions in there at once.
I’ll use the fast way here because I don’t want these article parts to extend into double digits.
Creating the tabs object
The fast way of creating a tabs object is to move all of the tab-related functions into a tabs object, remove the function keyword, and comma-separate the functions.
const tabs = {
// function removeAllActive() {
removeAllActive() {
const tabs = document.querySelectorAll(".tabs a");
tabs.forEach(function removeClass(tab) {
tab.classList.remove("active");
});
// }
},
// function updateTabs(tab) {
updateTabs(tab) {
removeAllActive();
tab.classList.add("active");
tab.blur();
// }
},
// function getBackgroundColor(tab) {
getBackgroundColor(tab) {
const tabStyle = window.getComputedStyle(tab);
return tabStyle["background-color"];
}
};
Redefinition of ‘tabs’ from line 2.
removeAllActive() {
const tabs = document.querySelectorAll(".tabs a");
tabs.forEach(function removeClass(tab) {
JSLint is right. That tabs variable contains tab links anyway, so we can rename tabs to a more appropriate name of tabLinks.
removeAllActive() {
const tabLinks = document.querySelectorAll(".tabs a");
tabLinks.forEach(function removeClass(tab) {
That does cause several problems though, so let’s use JSLint to help us find them all.
Undeclared ‘removeAllActive’.
updateTabs(tab) {
removeAllActive();
The updateTabs function is now in the tabs object.
updateTabs(tab) {
// removeAllActive();
tabs.removeAllActive();
Undeclared ‘getBackgroundColor’.
function showPanel(tab) {
const tabColor = getBackgroundColor(tab);
That getBackgroundColor function is now in the tabs object too.
function showPanel(tab) {
// const tabColor = getBackgroundColor(tab);
const tabColor = tabs.getBackgroundColor(tab);
Undeclared ‘updateTabs’.
hidePanels();
updateTabs(tab);
showPanel(tab);
The updateTabs function is in the tabs object, so let’s update that too.
hidePanels();
// updateTabs(tab);
tabs.updateTabs(tab);
showPanel(tab);
Because the updateTabs function is in the tabs object, it makes sense now to rename that to just update instead.
// updateTabs(tab) {
update(tab) {
tabs.removeAllActive();
tab.classList.add("active");
tab.blur();
},
...
hidePanels();
// tabs.updateTabs(tab);
tabs.update(tab);
showPanel(tab);
JSLint has no further complaints about the code, and it works well with the test page too.
Let’s do that with now with the panels.
Creating the panels object
We can now use a similar technique to create the panels object.
const panels = {
setBackgroundColor(containerSelector, cssColor) {
const containers = document.querySelectorAll(containerSelector);
containers.forEach(function setBackgroundColor(container) {
container.style.setProperty("background-color", cssColor);
});
},
hidePanels() {
const panels = document.querySelectorAll(".panel");
panels.forEach(function (panel) {
panel.classList.add("hide");
});
},
showPanel(tab) {
const tabColor = tabs.getBackgroundColor(tab);
setBackgroundColor(".panelContainer", tabColor);
const panel = document.querySelector(tab.hash);
panel.classList.remove("hide");
panel.classList.add("fade-in");
}
};
and use JSLint to help us connect things back together again.
Redefinition of ‘panels’ from line 19.
hidePanels() {
const panels = document.querySelectorAll(".panel");
panels.forEach(function (panel) {
Because we are already using panels in a higher scope, it’s best to not duplicate that variable name. I’m going to give this a temporary rename of allPanels
, and come back to dealing with this when the rest of the code is working again.
// const panels = document.querySelectorAll(".panel");
const allPanels = document.querySelectorAll(".panel");
// panels.forEach(function (panel) {
allPanels.forEach(function (panel) {
Undeclared ‘setBackgroundColor’.
showPanel(tab) {
const tabColor = tabs.getBackgroundColor(tab);
setBackgroundColor(".panelContainer", tabColor);
...
That setBackgroundColor function is now in the panels object.
showPanel(tab) {
const tabColor = tabs.getBackgroundColor(tab);
// setBackgroundColor(".panelContainer", tabColor);
panels.setBackgroundColor(".panelContainer", tabColor);
...
Undeclared ‘hidePanels’.
hidePanels();
tabs.update(tab);
showPanel(tab);
The hidePanels function is now in the panels object.
// hidePanels();
panels.hidePanels();
tabs.update(tab);
showPanel(tab);
Now, panels.hidePanels is a bit of double-speak, so we should rename hidePanels to be just hide.
// hidePanels() {
hide() {
...
},
...
// panels.hidePanels();
panels.hide();
tabs.update(tab);
showPanel(tab);
Undeclared ‘showPanel’.
And, the showPanel function is also in the panels object.
panels.hide();
tabs.update(tab);
// showPanel(tab);
panels.showPanel(tab);
The code is now working, but we should rename showPanel to show as well.
// showPanel(tab) {
show(tab) {
...
}
...
panels.hide();
tabs.update(tab);
// panels.showPanel(tab);
panels.show(tab);
tabs and panels
We now have a good and reliable set of functions for tabs and panels.
const tabs = {
removeAllActive() {
const tabLinks = document.querySelectorAll(".tabs a");
tabLinks.forEach(function removeClass(tab) {
tab.classList.remove("active");
});
},
update(tab) {
tabs.removeAllActive();
tab.classList.add("active");
tab.blur();
},
getBackgroundColor(tab) {
const tabStyle = window.getComputedStyle(tab);
return tabStyle["background-color"];
}
};
const panels = {
setBackgroundColor(containerSelector, cssColor) {
const containers = document.querySelectorAll(containerSelector);
containers.forEach(function setBackgroundColor(container) {
container.style.setProperty("background-color", cssColor);
});
},
hide() {
const allPanels = document.querySelectorAll(".panel");
allPanels.forEach(function (panel) {
panel.classList.add("hide");
});
},
show(tab) {
const tabColor = tabs.getBackgroundColor(tab);
panels.setBackgroundColor(".panelContainer", tabColor);
const panel = document.querySelector(tab.hash);
panel.classList.remove("hide");
panel.classList.add("fade-in");
}
};
I want to move this remaining code into the tabs object too.
function tabClickHandler(evt) {
var tab = evt.target;
evt.preventDefault();
panels.hide();
tabs.update(tab);
panels.show(tab);
}
const tabLinks = document.querySelectorAll(".tabs a");
tabLinks.forEach(function addClickHandler(tab) {
tab.addEventListener("click", tabClickHandler);
});
tabLinks[0].click();
Moving tabClickHandler into tabs object
The tabClickHandler function refers to panels too, so this one is going to get interesting when it moves into the tabs object.
Here is the tabClickHandler moved into the tabs object.
const tabs = {
...
tabClickHandler(evt) {
var tab = evt.target;
evt.preventDefault();
panels.hide();
tabs.update(tab);
panels.show(tab);
}
};
JSLint now tells us what needs to be done to get this working.
Undeclared ‘tabClickHandler’.
We need to use panels.tabClickHandler now.
const tabLinks = document.querySelectorAll(".tabs a");
tabLinks.forEach(function addClickHandler(tab) {
// tab.addEventListener("click", tabClickHandler);
tab.addEventListener("click", panels.tabClickHandler);
});
Now even though JSLint is happy with the code, the test code doesn’t work yet. The tabClickHandler function wants to access panels, but that isn’t accessible by it.
A way to deal with this is to use an event wrapper instead, which gives access to what is needed.
tabClickHandler(evt) {
var tab = evt.target;
evt.preventDefault();
panels.hide();
tabs.update(tab);
panels.show(tab);
},
tabClickWrapper(panels) {
return tabs.tabClickHandler;
}
And we can create a tabClickHandler variable down at the end of the code.
tabLinks.forEach(function addClickHandler(tab) {
const tabClickHandler = tabs.tabClickWrapper(panels);
// tab.addEventListener("click", tabs.tabClickHandler);
tab.addEventListener("click", tabClickHandler);
});
Now while that works, JSLint has good concerns that the tabClickWrapper doesn’t seem to be using the panels variable.
Unused ‘panels’.
We know that panels is being used, but it’s hard for the linter to know.
So instead of having the wrapper return a reference to the handler function, we can have the wrapper return the whole handler function itself.
// tabClickHandler(evt) {
// var tab = evt.target;
// evt.preventDefault();
// panels.hide();
// tabs.update(tab);
// panels.show(tab);
// };
tabClickWrapper(panels) {
// return tabs.tabClickHandler;
return function tabClickHandler(evt) {
var tab = evt.target;
evt.preventDefault();
panels.hide();
tabs.update(tab);
panels.show(tab);
};
}
And everything works well once again.
tabs init function
The last thing to do is to move the event assignment code into a tabs init function.
const tabs = {
...
init(panels) {
const tabLinks = document.querySelectorAll(".tabs a");
tabLinks.forEach(function addClickHandler(tab) {
const tabClickHandler = tabs.tabClickWrapper(panels);
tab.addEventListener("click", tabClickHandler);
});
tabLinks[0].click();
}
};
...
// const tabLinks = document.querySelectorAll(".tabs a");
// tabLinks.forEach(function addClickHandler(tab) {
// const tabClickHandler = tabs.tabClickWrapper(panels);
// tab.addEventListener("click", tabClickHandler);
// });
tabs.init(panels);
And the code is a lot clearer now. We have a tabs object, and a panels object. And the tabs are initialized using the panels.
The final code
Here is the final code that’s been converted from jQuery to vanilla JavaScipt. As well as being converted, it’s also been restructured to make it easier to understand what it does.
/*jslint browser */
(function iife() {
const tabs = {
removeAllActive() {
const tabLinks = document.querySelectorAll(".tabs a");
tabLinks.forEach(function removeClass(tab) {
tab.classList.remove("active");
});
},
update(tab) {
tabs.removeAllActive();
tab.classList.add("active");
tab.blur();
},
getBackgroundColor(tab) {
const tabStyle = window.getComputedStyle(tab);
return tabStyle["background-color"];
},
tabClickWrapper(panels) {
return function tabClickHandler(evt) {
var tab = evt.target;
evt.preventDefault();
panels.hide();
tabs.update(tab);
panels.show(tab);
};
},
init(panels) {
const tabLinks = document.querySelectorAll(".tabs a");
tabLinks.forEach(function addClickHandler(tab) {
const tabClickHandler = tabs.tabClickWrapper(panels);
tab.addEventListener("click", tabClickHandler);
});
tabLinks[0].click();
}
};
const panels = {
setBackgroundColor(containerSelector, cssColor) {
const containers = document.querySelectorAll(containerSelector);
containers.forEach(function setBackgroundColor(container) {
container.style.setProperty("background-color", cssColor);
});
},
hide() {
const allPanels = document.querySelectorAll(".panel");
allPanels.forEach(function (panel) {
panel.classList.add("hide");
});
},
show(tab) {
const tabColor = tabs.getBackgroundColor(tab);
panels.setBackgroundColor(".panelContainer", tabColor);
const panel = document.querySelector(tab.hash);
panel.classList.remove("hide");
panel.classList.add("fade-in");
}
};
tabs.init(panels);
}());
Even though the code restructure wasn’t required as a part of converting from jQuery to vanilla JavaScript, it’s important to make the code easier to understand by anyone that reads it.
Next steps
There are no more steps. We are done
Celebrations!