Converting jQuery to vanilla JavaScript, a step by step guide

Frequently I see on here people asking to deal with jQuery, for example with converting it to vanilla JavaScript.

As a result I thought that I’d provide a worked example of converting code from jQuery to vanilla JavaScript, using jQuery that is a bit more complex than the standard standard conversion tutorials. Along with event handlers this guide uses custom backgrounds that change to match the different tab colors, and deals with animation too.

The original code is from a tabbed panel on codePen, as a starting point from which to perform the conversion.

The Initial Code

Here’s the HTML, CSS, and JavaScript that we’re starting with:

The HTML for the tabs and content panels are:

<div class="tabbedPanels">
  <ul class="tabs">
    <li><a href="#panel1" class = "tabOne">About</a></li>
    <li><a href="#panel2" class = "tabTwo inactive">Details</a></li>
    <li><a href="#panel3" class = "tabThree inactive">Contact Us</a></li>
  </ul>
  <div class="panelContainer">
    <div class="panel" id="panel1">
      <h1 class = "panelContent">About</h1>
      <p class = "panelContent"><span>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ea illum magni error enim labore facere dolore obcaecati voluptate inventore nemo. Dolorum ipsam fuga nesciunt eos incidunt eum beatae quisquam enim.</span><span>Veniam velit quibusdam pariatur et autem veritatis nesciunt minima! Voluptas impedit voluptates amet dolores debitis labore asperiores quis libero est magnam voluptatum alias praesentium magni deserunt beatae optio quam itaque!</span></p>
      
    </div>
        <div class="panel" id="panel2">
      <h1 class = "panelContent">Details</h1>
      <p class = "panelContent"><span>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ea illum magni error enim labore facere dolore obcaecati voluptate inventore nemo. Dolorum ipsam fuga nesciunt eos incidunt eum beatae quisquam enim.</span><span>Veniam velit quibusdam pariatur et autem veritatis nesciunt minima! Voluptas impedit voluptates amet dolores debitis labore asperiores quis libero est magnam voluptatum alias praesentium magni deserunt beatae optio quam itaque!</span></p>
                <p class = "panelContent"><span>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ea illum magni error enim labore facere dolore obcaecati voluptate inventore nemo. Dolorum ipsam fuga nesciunt eos incidunt eum beatae quisquam enim.</span><span>Veniam velit quibusdam pariatur et autem veritatis nesciunt minima! Voluptas impedit voluptates amet dolores debitis labore asperiores quis libero est magnam voluptatum alias praesentium magni deserunt beatae optio quam itaque!</span></p>
    </div>
        <div class="panel" id="panel2">
      <h1 class = "panelContent">Details</h1>
      <p class = "panelContent"><span>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ea illum magni error enim labore facere dolore obcaecati voluptate inventore nemo. Dolorum ipsam fuga nesciunt eos incidunt eum beatae quisquam enim.</span><span>Veniam velit quibusdam pariatur et autem veritatis nesciunt minima! Voluptas impedit voluptates amet dolores debitis labore asperiores quis libero est magnam voluptatum alias praesentium magni deserunt beatae optio quam itaque!</span></p>
    </div>
      <div class="panel" id="panel3">
      <h1 class = "panelContent">Contact Us</h1>
      <p class = "panelContent"><span>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ea illum magni error enim labore facere dolore obcaecati voluptate inventore nemo. Dolorum ipsam fuga nesciunt eos incidunt eum beatae quisquam enim.</span><span>Veniam velit quibusdam pariatur et autem veritatis nesciunt minima! Voluptas impedit voluptates amet dolores debitis labore asperiores quis libero est magnam voluptatum alias praesentium magni deserunt beatae optio quam itaque!</span></p>
    </div>
  </div>
</div>

The CSS from codePen is SCSS code. To help make this a bit easier to follow, I’ve used an Online SCSS Compiler to convert it, and have slightly tweaked the colors by giving them proper names. While I couldn’t find close enough CSS-color-names, I’ve used the HTML CSS Color Picker to find matching colors with more meaningful names.

These colors are:

Here’s the CSS code that we’ll be starting with.

.tabbedPanels {
    width: 75%;
    margin: 10px auto;
}
@media only screen and (max-width: 700px) {
    .tabbedPanels {
        width: 90%;
    }
}
.tabs {
    margin: 0;
    padding: 0;
}
.tabs li {
    list-style-type: none;
    float: left;
    text-align: center;
}
.inactive {
    position: relative;
    top: 0;
}
.tabs a {
    display: block;
    text-decoration: none;
    padding: 10px 15px;
    width: 8rem;
    color: black;
    border-radius: 10px 10px 0 0;
    font-family: 'Raleway';
    font-weight: 700;
    font-size: 1.2rem;
    color: white;
    letter-spacing: 2px;
}
@media only screen and (max-width: 700px) {
    .tabs a {
        width: 8rem;
        padding: 10px 12px;
    }
}
@media only screen and (max-width: 500px) {
    .tabs a {
        letter-spacing: 0;
        width: 7rem;
    }
}
.tabs a.active {
    border-radius: 10px 10px 0 0;
    position: relative;
    top: 1px;
    z-index: 100;
}
.tabOne {
    background-color: #4498c6; /* Curious Blue */
}
.tabTwo {
    background-color: #296586; /* Bahamas Blue */
}
.tabThree {
    background-color: #1d475f; /* Astronaut Blue */
}
.panel {
    width: 85%;
    margin: 1rem auto;
    background-color: white;
    border-radius: 20px;
    padding: 20px;
}
.panelContainer {
    clear: left;
    padding: 20px;
    background-color: #4498c6; /* Curious Blue */
    border-radius: 0 20px 20px 20px;
}
.panelContent {
    line-height: 1.5;
    font-family: Raleway;
    padding: 0 1rem;
    font-size: 1.2rem;
}
h1.panelContent {
    font-size: 2.2rem;
}
@media only screen and (max-width: 700px) {
    html {
        font-size: 14px;
    }
}
@media only screen and (max-width: 450px) {
    html {
        font-size: 12px;
    }
}

And lastly, the jQuery code that we’re starting with is:

  $('.tabs a').click(function(){
  $this = $(this);

  $('.panel').hide();
  $('.tabs').removeClass('active').addClass('inactive');
  $this.addClass('active').blur();
  
  var panelContainerColor = $this.css('background-color');

  $('.panelContainer').css({backgroundColor: panelContainerColor});
  
  var panel = $this.attr('href');
  
  $(panel).fadeIn(350);
  
  return false;
 
});//end click

$('.tabs li:first a').click();

That’s the initial code that we’re working with from the tabbed panel on Codepen, and will convert to remove all jQuery code.

Next steps

It helps when the code is easy to work with and modify, so next up is tests and linting.
Then we divide up the code to make it easier to convert with fewer mistakes and errors.

7 Likes

Sorry, but I don’t understand this thread unless you are going to do a follow up on it? My best piece of advice for people learning javascript is to learn vanilla javascript instead of jQuery. I had a college instructor teach of javascript by showing a little vanilla javascrit then switching over to jQuery. That was a big mistake on my part. Then you’ll never have to learn how to convert jQuery to javascript or be able to convert jQuery easier.

Paul rarely does single post pieces of this kind. I suspect more will be along very shortly.

4 Likes

This is Part 2 of a many post series. In this post I attempt to convert code and learn that tests are needed. Actually converting jQuery to vanilla JavaScript will happen from around Part 4.

I did start to convert jQuery code without tests, but when making a few conversions to the code I found a subtle bug in the tabs, that is in the initial code too. The currently active tab is slightly shorter than the other tabs, and the bug is that an active tab remains shorter when it’s no longer active instead of returning back to its initial size.

I was going to just fix the original code and carry on, but . . .

Strictly speaking you’re supposed to create tests to ensure that the expected behaviour occurs, and that it doesn’t change as you convert the code.

/me cogitates, thinking about things.
time passes
Oh all right. kick aimlessly at stones on ground - I’ll write some tests.

The reason for my hesitation is that it’s always harder to write tests after the code exists, and the motivation really isn’t there because the code is already working.

However, I have better motivation now because I found a fault that already exists in the original code, and the tests will ensure that as I convert code, that it continues to behave exactly as it already does. Tests are a reliable way to know that the code properly behaves, and gives instant feedback when it doesn’t.

Tests for the code

I’ve used embedded Mocha+Chai for the tests, where I have a mocha div at the top of the page for showing test results, use mocha styles and mocha+chai script code, and of course the tests too.

<head>
    ...
    <link rel="stylesheet" href="css/mocha.min.css">
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <div id="mocha"></div>
    <div id="tabbedpanels">
        ...
    </div>
    <script src="js/jquery.min.js"></script>
    <script src="js/script.js"></script>
    <script src="js/mocha.min.js"></script>
    <script src="js/chai.min.js"></script>
    <script src="js/tests.js"></script>
</body>

The basic setup for doing the tests is to use bdd (behaviour driven development), which gives us easy access to the describe and it functions. Tests are put in the describe section of code.

tests.js

mocha.setup("bdd");
const expect = chai.expect;
describe("tab tests", function () {
    it("initial test", function () {
        expect(true).to.be.true;
    });
});

mocha.run();

It always helps to start with a simple test to begin with, to check that everything is plumbed together properly. On seeing that the test successfully passes, I can delete that initial test and then put in a proper test.

Investigating the bug

The error in the initial code is to do with the active tab. The active tab is visually shorter by one pixel than the remaining tabs. When a tab is no longer active it should return to being taller, but it currently doesn’t.

Investing the HTML code, I notice that the class names on the tab are responsible for the problem.

  • before selected: “inactive”
  • when selected: “inactive active”
  • other selected: “inactive active”

A tab shouldn’t remain active when some other tab is selected.

I’m now tempted to write a test to find this bug, and fix the code, but I won’t know if fixing this will cause other things to break. Before touching any code, I need to have tests in place to confirm that everything that currently works, remains working.

Creating tests

I will spare you the details of creating the tests. Basically, I want to test for two types of things, the starting state of the page and how things look when it’s first loaded, and for what happens when another tab is clicked.

describe("Tab tests", function () {
    function getBackgroundColor(el) {
        const styles = window.getComputedStyle(el);
        return styles.backgroundColor;
    }
    let tabs, tab1, tab2, tab3;
    beforeEach(function () {
        tabs = document.querySelectorAll(".tabs a");
        tab1 = tabs[0];
        tab2 = tabs[1];
        tab3 = tabs[2];
    });
    describe("Starting state", function () {
        it("has an active first tab", function () {
            expect(tab1.classList.contains("active")).to.equal(true);
        });
        it("has an inactive second tab", function () {
            expect(tab2.classList.contains("active")).to.equal(false);
        });
        it("has inactive second and third tabs", function () {
            expect(tab2.classList.contains("active")).to.equal(false);
            expect(tab3.classList.contains("active")).to.equal(false);
        });
        it("has a different background color for second tab", function () {
            const backgroundColor1 = getBackgroundColor(tab1);
            const backgroundColor2 = getBackgroundColor(tab2);
            expect(backgroundColor1).to.not.equal(backgroundColor2);
        });
        it("has panel matching first tab background color", function () {
            const panel = document.querySelector(".panelContainer");
            const tabBackground = getBackgroundColor(tab1);
            const panelBackground = getBackgroundColor(panel);
            expect(panelBackground).to.equal(tabBackground);
        });
    });
    describe("Going from first tab to second tab", function () {
        beforeEach(function () {
            tab1.click();
        });
        it("has an active second tab", function () {
            expect(tab1.classList.contains("active")).to.be.true;
            expect(tab2.classList.contains("active")).to.be.false;
            tab2.click();
            expect(tab2.classList.contains("active")).to.be.true;
        });
        it("changes panel background color when changing tabs", function () {
            const tabBackground1 = getBackgroundColor(tab1);
            const tabBackground2 = getBackgroundColor(tab2);
            const panel = document.querySelector(".panelContainer");
            const panelBackgroundBefore = getBackgroundColor(panel);
            expect(panelBackgroundBefore).to.equal(tabBackground1);
            tab2.click();
            const panelBackgroundAfter = getBackgroundColor(panel);
            expect(panelBackgroundBefore).to.not.equal(panelBackgroundAfter);
            expect(panelBackgroundAfter).to.equal(tabBackground2);
        });
        after(function () {
            // reset after tests so that first tab is selected
            tab1.click();
        });
    });
});

Those tests check that everything important is working. I have them automatically running too by using live-server so that I can splitscreen things, with the code on the left side of the screen, and the webpage open on the right half of the screen. Using live-server ensures that the webpage automatically refreshes itself causing the tests to run again whenever a file is changed.

None of those tests yet deal with the bug, that’s to come in my next post.

Next Steps

There are now checks and balances in place to help us rapidly learn if we break things when updating the code.

The plan for the next posts is:

  • fix the error in the initial code
  • lint the code to give us a good starting point for conversion
  • divide up the code to make it easier to convert
  • convert jQuery to vanilla javascript
5 Likes

Part 3 of converting jQuery to vanilla JavaScript is all about cleaning up problems with the existing code, before doing the convertion.

Fixing the inactive tabs bug

Now that we have tests that warn us when the tabs code stops doing what is expected, I can add a test for the tabs bug that I found. The test expects that an inactive tab no longer has the active class.

describe("tab tests", function () {
    const tabs = document.querySelectorAll(".tabs a");
    it("Fix: Inactive tab shouldn't remain active", function () {
        const tab1 = tabs[0];
        const tab2 = tabs[1];
        tab2.click();
        expect(tab1.classList.contains("active")).to.be.false;
    });
});

That results in a failed test, which is excellent for I know that it’s found the problem that I want to fix.

Looking at the code I see that the inactive class and the active one are removed from the tabs container, yet active is added to separate tab anchors.

That needs to occur commonly to the same things. I’m going update the code so that it’s anchors within the tabs container that have the class removed.

$('.tabs a').click(function(){
    $this = $(this);
   
    $('.panel').hide();
    // $('.tabs').removeClass('active').addClass('inactive');
    $('.tabs a').removeClass('active').addClass('inactive');
    $this.addClass('active').blur();

Doing that causes the broken test to work, and a manual test of the tabs confirms that they now work properly, where the active tab is shorter than the other inactive tabs.

Linting the code

Now that the code is correctly working, it helps to clean the code, making it easier to work with that code when we convert jQuery to vanilla JavaScript.

Using JSLint to lint the code, we see that the first warning is about $.

Undeclared ‘$’.

Normally with jQuery it should only be the jQuery variable that’s first used, with $ being made available via a DOM-is-ready function.

jQuery(function domIsReady($) {
  $('.tabs a').click(function(){
    ...
  });

  $('.tabs li:first a').click();
});

That’s better written, but it causes test issues. We need to temporarily delay the tests until after the domIsReady has occurred.

A simple way of doing that is to delay the running of the tests for a small period of time:

setTimeout(function () {
    mocha.run();
}, 100);

Which we can undo when all jQuery is removed from the code.

The next lint problem is about jQuery.

Undeclared ‘jQuery’.

We have reduced many $ problems to just a single jQuery problem. It’s appropriate to tell JSLint that jQuery is a global variable, so we’ll add jQuery to the Global Variables section near the bottom of the linting page.

The next warning is about formatting.

Use double quotes, not single quotes.

Code linters are designed to enforce standards. In this case it’s using double quotes instead of single quotes.

While it is possible to replace them one by one when the linter complains about them, it’s a lot more efficient to work through all of the code and fix what’s being warned about all at the same time.

So, we should make all of those quote replacements in the code.

For example:

jQuery(function domIsReady($) {
  $(".tabs a").click(function(){
    $this = $(this);
   
    $(".panel").hide();
    $(".tabs a").removeClass("active").addClass("inactive");
    $this.addClass("active").blur();
    ...

Undeclared ‘$this’.

Currently the $this variable is a global variable. We should fix that by using the var declaration instead.

    // $this = $(this);
    var $this = $(this);

Also, $this and this are extremely bad names, because they tell you nothing about what’s happening. We’ll come to that later.

Unexpected ‘this’.

Ahh, we won’t come to it later, we’ll come to it now.

The this keyword should be replaced with something more meaningful.

  $(".tabs a").click(function(){
    var $this = $(this);

Instead of using this, we can add a function parameter of evt for the event object. The this keyword here refers to the element that was clicked on, so let’s make that more explicit by using evt.target.

  // $(".tabs a").click(function(){
  $(".tabs a").click(function(evt){
    // var $this = $(this);
    var $this = $(evt.target);

This is also a good time to rename $this to something else more meaningful too, such as $tab where the dollar symbol tells us that it’s a jQuery object.

    // var $this = $(evt.target);
    var $tab = $(evt.target);
    ...
    // $this.addClass("active").blur();
    $tab.addClass("active").blur();
    ...
    // var panelContainerColor = $this.css("background-color");
    var panelContainerColor = $tab.css("background-color");
    ...
    // var panel = $this.attr("href");
    var panel = $tab.attr("href");

It’s used in a lot of places, and is now much easier to understand now that we have a clear context for what is being worked with.

Unexpected trailing space.

We are going to get lots of warnings about formatting issues, such as spacing and indenting of code.

All of those problems are easily taken care of at the same time using the Online JavaScript Beautifier.

This is also a good time to check the horizontal line breaks, and group things together more appropriately.

jQuery(function domIsReady($) {
    $(".tabs a").click(function(evt) {
        var $tab = $(evt.target);
        $(".panel").hide();
        $(".tabs a").removeClass("active").addClass("inactive");
        $tab.addClass("active").blur();
        var panelContainerColor = $tab.css("background-color");
        $(".panelContainer").css({
            backgroundColor: panelContainerColor
        });
        var panel = $tab.attr("href");
        $(panel).fadeIn(350);
        return false;
    }); //end click

    $(".tabs li:first a").click();
});

Expected one space between ‘function’ and ‘(’.

This warning occurs because the beautifer formats anonymous functions differently than named functions.

It’s a good time to consider if we really want anonymous functions. Normally we don’t because naming them gives big benefits when exploring the code and understanding the Call Stack.

In this case, the click function has a good standard name we can give it, of tabClickHandler.

    // $(".tabs a").click(function(evt) {
    $(".tabs a").click(function tabClickHandler(evt) {

End of linting

The code is all linted now, and copying the linted code back to our code example, the tests all correctly pass telling us that nothing has broken the code.

The code is in a good state now for us to go ahead with converting jQuery to vanilla JavaScript.

We have used a test to ensure that a problem is fixed, and have linted the code to help make it easier to work with.

Takeaway message: Ensure that tests are in place before you change anything.

Next steps

We are now in a good place to finally convert jQuery code to vanilla JavaScript, which we’ll start doing in my next post.

6 Likes

Part 4 of converting jQuery to vanilla JavaScript is where we fix an animation issue, and use a linting tool to help guide our conversions.

While writing up the next part on a different computer, I found there was a problem between the fadein and the tests, so I’ll convert the fadein animation first before moving on to the rest of the code.

Converting fadein

$(panel).fadeIn(350);

You’d think that this is tough to convert, but it’s easy. We just won’t use javascript for it. :slight_smile:

Instead, CSS does the job very well with animations.

.fade-in {
    opacity: 1;
    animation-name: fadeInOpacity;
    animation-timing-function: ease-in;
    animation-duration: 350ms;
}
@keyframes fadeInOpacity {
    0% {
        opacity: 0;
    }
    100% {
        opacity: 1;
    }
}

We can now replace the jQuery fadein with adding a “fade-in” class to the element.

        // $(panel).fadeIn(350);
        $(panel).show();
        $(panel)[0].classList.add("fade-in");

And the fadein now works fully using CSS to achieve it instead.

The tests that we had now return back to working too. It’s amazing how changing machines causes different problems to occur, but the issue that revealed itself has now been dealt with.

Cleaning up comments

While converting the fade in, I noticed that a comment occurs in the code marking the end of the click event function.

    $(".tabs a").click(function tabClickHandler(evt) {
        var $tab = $(evt.target);
        ...
        return false;
    }); //end click

When seeing comments, I try to figure out if I can improve the code so that the comments aren’t needed.

Striving to remove all comments isn’t the goal, for some are useful. With this one though the code benefits from a slight modification.

There is a common way of structuring code where all of the event handlers are assigned at the end of the code. Thanks to the linting we have already given the function a meaningful name, so we can just move the function out, leaving the event handler below it.

    function tabClickHandler(evt) {
        var $tab = $(evt.target);
        ...
        return false;
    }
    $(".tabs a").click(tabClickHandler); //end click

That end click comment no longer needs to be there, because the function already states that it’s a click function, and the click handler below the function tells us that the click handler is being used too. It’s a good time now to remove that comment.

    // $(".tabs a").click(tabClickHandler); //end click
    $(".tabs a").click(tabClickHandler);

This is when I would normally separate the tabClickHandler code into separate functions. It’s normally easy to tell when a function is doing several different types of things.

In this case, the tabClickHandler function is doing these three things:

  • setting tabs active/inactive
  • changing the panel background color
  • fading in the panel

That could be a separate function for each of them, but I’ll hold off on doing that until the converted code demands that this occurs.

Converting domIsReady

The code using domIsReady is our first target. As the code is running from the end of the body, we don’t need to wait because the DOM is already ready, and can instead replace it with an IIFE (immediately invoked function expression) which helps to protect the rest of the page from our code’s variables and functions.

// jQuery(function domIsReady($) {
(function iife() {
    ...
// });
}());

The tests reassure us that the code still works and does everything that it needs to do.

I’m also going to try and simplify this for me, by using JSLint to tell me about the next thing that needs to be done.

Undeclared ‘$’. (target element)

        var $tab = $(evt.target);

After having removed the domIsReady part of code, the dollar symbol needs to be fixed. Good, that’s some jQuery and we’re wanting to remove all of it.

In this case, we can just assign evt.target without the jQuery wrapper. But, it’s just a normal element, not a jQuery element so the $tab isn’t suitable either, and should be renamed to just tab.

        // var $tab = $(evt.target);
        var tab = evt.target;

That causes the tests to fail. So wherever we have $tab in the code, we should replace it with $(tab) instead.

If we want to go really slow and careful about this, we can use the linter to tell us about each $tab that’s causing trouble.

        // $tab.addClass("active").blur();
        $(tab).addClass("active").blur();
        ...
        // var panelContainerColor = $tab.css("background-color");
        var panelContainerColor = $(tab).css("background-color");
        ...
        // var panel = $tab.attr("href");
        var panel = $(tab).attr("href");
        ...
        // var panel = $tab.attr("href");
        var panel = $(tab).attr("href");

And the code goes back to working.

Undeclared ‘$’. (hide panel)

        $(".panel").hide();

The usual way to hide an element is to use a classname that sets the display to none, but the fadein animation is likely to then have trouble.

We can use document.querySelector for the dollar symbol, and just set the style to display: none. We should though leave a note to come and revisit this later for a potentially better way after the conversion.

        // $(".panel").hide();
        // TODO: Don't touch style and try to use classname instead
        document.querySelector(".panel").style = "display: none";

The tabs still work, but the linter warns that document is undeclared. At the bottom of the JSLint linting page we can tell JSLint to assume that we’re using a browser, and the document variables no longer cause complaints.

Assume...
  [ ] in development
  [✓] a browser

The linter does though say that TODO should be taken care of. I’ll start the next post by fixing that instead of letting it wait. It’s better to fix that early than leave it to possibly go ignored, and we can keep things nice and clean as we go.

Next steps

We are partway through the conversion of jQuery to vanilla JavaScript. The tests have helped us to ensure that the code remains fully working throughout the conversion, and we are using JSLint to help direct us to the next thing that needs fixing.

After all, using tools like that to do some of the thinking for us helps to free up some cognitive load, letting us think more clearly about what we’re working on instead.

Next up we will:

  • use a classname to hide the panel
  • carry on linting to prompt us about the next thing to convert
4 Likes

In post 5 we check over the work from the previous post,

Fixing a display problem

I noticed that the test page fails to work properly because we were only hiding one of the panels. That’s going to lead us in to fixing some HTML trouble from the initial page, and fixing up the showing/hiding of the panels.

In the previous post I was hiding only one panel.

        document.querySelector(".panel").style = "display: none";

There isn’t only one panel though. There are several of them

        const panels = document.querySelectorAll(".panel");
        panels.forEach(function (panel) {
            panel.style = "display: none";
        });

And everything is now working again.

It’s now time to use classname to hide elements, instead of styles.

Hiding elements using class

We will use this CSS to help us hide elements.

.hide {
    display: none;
}

Let’s remove the code that hides and shows panels, so that we can more easily tell what needs to be done.

            // panel.style = "display: none";
        ...
        // $(panel).show();

That causes all of the panels to be shown, and strangely enough the test is still happy with that. That gives us good direction that we need to update the tests.

Fixing another bug from the original code

Even worse, I see that we have a duplicate “Details” panel. The HTML code incorrectly has two panel2 sections, so one of those must be removed.

      <div class="panel" id="panel2">
        <h1 class = "panelContent">Details</h1>
        <p class = "panelContent"><span>Lorem ipsum ...</span></p>
        <p class = "panelContent"><span>Lorem ipsum ...</span></p>
      </div>
      <!-- <div class="panel" id="panel2">
        <h1 class = "panelContent">Details</h1>
        <p class = "panelContent"><span>Lorem ipsum ...</span></p>
      </div> -->

Test for desired starting state

Now that we’re starting with the right number of panels, we can add tests for what we want to occur.

We want the test to complain when inappropriate panels remain visible, and can use getComputedStyle which works regardless of whether styles or classes control things.

First I’ll do a confirmation test that the first panel is visible when the page first loads:

    describe("Starting state", function () {
        ...
        it("has a visible first panel", function () {
            const panel = document.querySelector(".panel");
            const styles = window.getComputedStyle(panel);
            const display = styles.display;
            expect(display).to.equal("block");
        });
        ...
    });

And there’s a similar set of code to check that the second and third panels are not visible.

    describe("Starting state", function () {
        ...
        it("doesn't have a visible second panel", function () {
            const panel = document.querySelector(".panel");
            const styles = window.getComputedStyle(panel);
            const display = styles.display;
            expect(display).to.equal("none");
        });
        ...
    });

That causes the test to break. Because a test currently fails, we want to get it passing in the simplest way possible. There are better ways to achieve what we’re doing, but that must wait until we first have all the tests working.

In this case we just add a hide class to the second panel element.

        panels.forEach(function (panel) {
            // panel.style = "display: none";
        });
        panels[1].classList.add("hide");

That’s good and working.

Hiding all but the first panel

We now want all of the panels to be hidden, except for the first one.

Instead of duplicating even more test code for the third panel, it’s better to move the code to a separate function,and call that instead.

    function isVisible(panelId) {
        const panel = document.querySelector("#" + panelId);
        const styles = window.getComputedStyle(panel);
        return styles.display === "block";
    }
    ...
        it("has a visible first panel", function () {
            expect(isVisible("panel1")).to.be.true;
        });
        it("has a hidden second panel", function () {
            expect(isVisible("panel2")).to.be.false;
        });
        it("has a hidden third panel", function () {
            expect(isVisible("panel3")).to.be.false;
        });

The tests still work for the first and second panel, and we have a new failing test for the third and last panel.

Let’s do a simple thing to make that pass, by adding a last line:

        panels.forEach(function (panel) {
            // panel.style = "display: none";
        });
        panels[1].classList.add("hide");
        panels[2].classList.add("hide");

And we now have enough information to justify putting that into a loop.

        panels.forEach(function (panel) {
            panel.classList.add("hide");
        });

But the first panel is also hidden, so we can temporarily remove it from the first panel.

        panels.forEach(function (panel) {
            panel.classList.add("hide");
        });
        panels[0].classList.remove("hide");

And a better place for that remove code is down with the panel fadein code.

        panels.forEach(function (panel) {
            panel.classList.add("hide");
        });
        // panels[0].classList.remove("hide");
        ...
        var panel = $(tab).attr("href");
        panels[0].classList.remove("hide");
        $(panel)[0].classList.add("fade-in");

And we can even change panels[0] to be more consistent with the existing $(panel)[0] notation:

        var panel = $(tab).attr("href");
        // panels[0].classList.remove("hide");
        $(panel)[0].classList.remove("hide");
        $(panel)[0].classList.add("fade-in");

That’s a bit cleaner, and it causes the panels to all properly work too.

The current state of the code

That’s some good progress on things today.

The updated HTML code is:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="css/mocha.min.css">
  <link rel="stylesheet" href="css/style.css">
</head>
<body>
  <div id="mocha"></div>
  <div class="tabbedPanels">
    <ul class="tabs">
      <li><a href="#panel1" class = "tabOne">About</a></li>
      <li><a href="#panel2" class = "tabTwo inactive">Details</a></li>
      <li><a href="#panel3" class = "tabThree inactive">Contact Us</a></li>
    </ul>
    <div class="panelContainer">
      <div class="panel" id="panel1">
        <h1 class = "panelContent">About</h1>
        <p class = "panelContent"><span>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ea illum magni error enim labore facere dolore obcaecati voluptate inventore nemo. Dolorum ipsam fuga nesciunt eos incidunt eum beatae quisquam enim.</span><span>Veniam velit quibusdam pariatur et autem veritatis nesciunt minima! Voluptas impedit voluptates amet dolores debitis labore asperiores quis libero est magnam voluptatum alias praesentium magni deserunt beatae optio quam itaque!</span></p>
        
      </div>
      <div class="panel" id="panel2">
        <h1 class = "panelContent">Details</h1>
        <p class = "panelContent"><span>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ea illum magni error enim labore facere dolore obcaecati voluptate inventore nemo. Dolorum ipsam fuga nesciunt eos incidunt eum beatae quisquam enim.</span><span>Veniam velit quibusdam pariatur et autem veritatis nesciunt minima! Voluptas impedit voluptates amet dolores debitis labore asperiores quis libero est magnam voluptatum alias praesentium magni deserunt beatae optio quam itaque!</span></p>
        <p class = "panelContent"><span>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ea illum magni error enim labore facere dolore obcaecati voluptate inventore nemo. Dolorum ipsam fuga nesciunt eos incidunt eum beatae quisquam enim.</span><span>Veniam velit quibusdam pariatur et autem veritatis nesciunt minima! Voluptas impedit voluptates amet dolores debitis labore asperiores quis libero est magnam voluptatum alias praesentium magni deserunt beatae optio quam itaque!</span></p>
      </div>
      <div class="panel" id="panel3">
        <h1 class = "panelContent">Contact Us</h1>
        <p class = "panelContent"><span>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ea illum magni error enim labore facere dolore obcaecati voluptate inventore nemo. Dolorum ipsam fuga nesciunt eos incidunt eum beatae quisquam enim.</span><span>Veniam velit quibusdam pariatur et autem veritatis nesciunt minima! Voluptas impedit voluptates amet dolores debitis labore asperiores quis libero est magnam voluptatum alias praesentium magni deserunt beatae optio quam itaque!</span></p>
      </div>
    </div>
  </div>
  <script src="js/jquery.min.js"></script>
  <script src="js/script.js"></script>
  <script src="js/mocha.min.js"></script>
  <script src="js/chai.min.js"></script>
  <script src="js/tests.js"></script>
</body>
</html>

The updated css code is:

.tabbedPanels {
	width: 75%;
	margin: 10px auto;
}

@media only screen and (max-width: 700px) {
	.tabbedPanels {
		width: 90%;
	}
}

.tabs {
	margin: 0;
	padding: 0;
}

.tabs li {
	list-style-type: none;
	float: left;
	text-align: center;
}

.inactive {
	position: relative;
	top: 0;
}

.tabs a {
	display: block;
	text-decoration: none;
	padding: 10px 15px;
	box-sixing: border-box;
	width: 8rem;
	color: black;
	border-radius: 10px 10px 0 0;
	font-family: 'Raleway';
	font-weight: 700;
	font-size: 1.2rem;
	color: white;
	letter-spacing: 2px;
}

@media only screen and (max-width: 700px) {
	.tabs a {
		width: 8rem;
		padding: 10px 12px;
	}
}

@media only screen and (max-width: 700px) and (max-width: 500px) {
	.tabs a {
		letter-spacing: 0;
		width: 7rem;
	}
}

.tabs a.active {
	border-radius: 10px 10px 0 0;
	position: relative;
	top: 1px;
	z-index: 100;
}

.tabOne {
	background-color: #3D85B8; /*Curious Blue */
}

.tabTwo {
	background-color: #2B5B82; /* Bahamas Blue */
}

.tabThree {
	background-color: #214559; /* Astronaut Blue */
}

.panel {
	width: 85%;
	margin: 1rem auto;
	background-color: white;
	border-radius: 20px;
	padding: 20px;
}

.panelContainer {
	clear: left;
	padding: 20px;
	background-color: #3D85B8; /*Curious Blue */
	border-radius: 0 20px 20px 20px;
}

.panelContent {
	line-height: 1.5;
	font-family: Raleway;
	padding: 0 1rem;
	font-size: 1.2rem;
}

h1.panelContent {
	font-size: 2.2rem;
}

@media only screen and (max-width: 700px) {
	html {
		font-size: 14px;
	}
}

@media only screen and (max-width: 700px) and (max-width: 450px) {
	html {
		font-size: 12px;
	}
}

.fade-in {
	opacity: 1;
	animation-name: fadeInOpacity;
	animation-timing-function: ease-in;
	animation-duration: 350ms;
}

@keyframes fadeInOpacity {
	0% {
		opacity: 0;
	}
	100% {
		opacity: 1;
	}
}

.hide {
	display: none;
}

The updated tests are:

mocha.setup("bdd");
const expect = chai.expect;

describe("Tab tests", function () {
    function isVisible(panelId) {
        const panel = document.querySelector("#" + panelId);
        const styles = window.getComputedStyle(panel);
        return styles.display === "block";
    }
    function getBackgroundColor(el) {
        const styles = window.getComputedStyle(el);
        return styles.backgroundColor;
    }

    const tabs = document.querySelectorAll(".tabs a");
    let tab1;
    let tab2;
    let tab3;

    beforeEach(function () {
        tab1 = tabs[0];
        tab2 = tabs[1];
        tab3 = tabs[2];
    });
    describe("Starting state", function () {
        it("has an active first tab", function () {
            expect(tab1.classList.contains("active")).to.equal(true);
        });
        it("has an inactive second tab", function () {
            expect(tab2.classList.contains("active")).to.equal(false);
        });
        it("has inactive second and third tabs", function () {
            expect(tab2.classList.contains("active")).to.equal(false);
            expect(tab3.classList.contains("active")).to.equal(false);
        });
        it("has a visible first panel", function () {
            expect(isVisible("panel1")).to.be.true;
        });
        it("has a hidden second panel", function () {
            expect(isVisible("panel2")).to.be.false;
        });
        it("has a hidden third panel", function () {
            expect(isVisible("panel3")).to.be.false;
        });
        it("has a different background color for second tab", function () {document.querySelector(".panel");
            const backgroundColor1 = getBackgroundColor(tab1);
            const backgroundColor2 = getBackgroundColor(tab2);
            expect(backgroundColor1).to.not.equal(backgroundColor2);
        });
        it("has panel matching first tab background color", function () {
            const panel = document.querySelector(".panelContainer");
            const tabBackground = getBackgroundColor(tab1);
            const panelBackground = getBackgroundColor(panel);
            expect(panelBackground).to.equal(tabBackground);
        });
    });
    describe("Going from first tab to second tab", function () {
        beforeEach(function () {
            tab1.click();
        });
        it("has an active second tab", function () {
            expect(tab1.classList.contains("active")).to.be.true;
            expect(tab2.classList.contains("active")).to.be.false;
            tab2.click();
            expect(tab2.classList.contains("active")).to.be.true;
        });
        it("Fix: Inactive tab shouldn't remain active", function () {
            tab2.click();
            expect(tab1.classList.contains("active")).to.be.false;
        });    
        it("changes panel background color when changing tabs", function () {
            const tabBackground1 = getBackgroundColor(tab1);
            const tabBackground2 = getBackgroundColor(tab2);
            const panel = document.querySelector(".panelContainer");
            const panelBackgroundBefore = getBackgroundColor(panel);

            expect(panelBackgroundBefore).to.equal(tabBackground1);
            tab2.click();

            const panelBackgroundAfter = getBackgroundColor(panel);
            expect(panelBackgroundBefore).to.not.equal(panelBackgroundAfter);
            expect(panelBackgroundAfter).to.equal(tabBackground2);
        });
        after(function () {
            // reset after tests so that first tab is selected
            tab1.click();
        });
    });
});

setTimeout(function () {
    mocha.run();
}, 100);

And the updated scripting code is:

(function iife() {
    function tabClickHandler(evt) {
        var tab = evt.target;
        
        const panels = document.querySelectorAll(".panel");
        panels.forEach(function (panel) {
            panel.classList.add("hide");
        });
        $(".tabs a").removeClass("active").addClass("inactive");
        $(tab).addClass("active").blur();
        
        var panelContainerColor = $(tab).css("background-color");
        $(".panelContainer").css({
            backgroundColor: panelContainerColor
        });
        
        var panel = $(tab).attr("href");
        $(panel)[0].classList.remove("hide");
        $(panel)[0].classList.add("fade-in");
        
        return false;
    }
    $(".tabs a").click(tabClickHandler);
    
    $(".tabs li:first a").click();
}());

Next steps

We still have a fair way to go, but now that elements aren’t being messed with as much by fading and show/hide code, it’s going to be an easier path from here to finish the task of converting jQuery to vanilla JavaScript.

I would have been lost without the tests. They’ve helped to assure me that everything works properly, and JSLint gives me good direction about the next code to work on.

Speaking of which, in the next post we will next be working on the active/inactive sections of code.

4 Likes

In part 6 of converting jQuery to vanilla JavaScript, we investigate and remove the need for the inactive class, convert the tab class code to vanilla JavaScript, and deal with the blur code.

The next piece of jQuery code that JSLint warns us about is here:

        $(".tabs a").removeClass("active").addClass("inactive");

Investigating the inactive style

Based on prior knowledge of tabbed interfaces, having both active and inactive is too much. The style of the tabs should default to being inactive, and only change when they are made active.

Is it possible to combine the inactive styles with existing elements, and for everything to still work?

Here are the styles that are involved:

.inactive {
	position: relative;
	top: 0;
}
.tabs a {
	display: block;
	text-decoration: none;
	padding: 10px 15px;
	box-sixing: border-box;
	width: 8rem;
	color: black;
	border-radius: 10px 10px 0 0;
	font-family: 'Raleway';
	font-weight: 700;
	font-size: 1.2rem;
	color: white;
	letter-spacing: 2px;
}
.tabs a.active {
	border-radius: 10px 10px 0 0;
	position: relative;
	top: 1px;
	z-index: 100;
}

And oh look! There’s a “box-sixing” spelling mistake in the original style declaration too.

Fixing that spelling to “box-sizing” causes the last tab to be twice the size of the other tabs. It’s a mystery to me why that occurs and might be something for a css-guru to explain, so I’ll just remove the “box-sizing” line completely. It wasn’t doing anything before, so out it goes.

Removing the inactive style

Back to the inactive style, both of the position and top declarations are being updated by the active declarations, so it’s a good bet that we can combine inactive into the tabs anchor style.

/* .inactive {
	position: relative;
	top: 0;
} */
.tabs a {
	position: relative;
	top: 0;
	display: block;
	text-decoration: none;
	...

Now that inactive isn’t being used, we can remove that from where it’s used in the code.

Starting with the HTML code, we find inactive is applied to some of the tabs:

    <ul class="tabs">
      <li><a href="#panel1" class = "tabOne">About</a></li>
      <li><a href="#panel2" class = "tabTwo inactive">Details</a></li>
      <li><a href="#panel3" class = "tabThree inactive">Contact Us</a></li>
    </ul>

For the sake of consistency, we could add active to just the first tab. Without it there might be just a tiny unstyled moment before the script runs, where the first tab is the same height as all the other ones, and when the script runs the tab then suddenly becomes one pixel shorter as the active tab.

Oh my - swoon - somebody please pass the smelling salts.

So to be sure (to be sure), I’ll start things off with the first tab as being active.

    <ul class="tabs">
      <li><a href="#panel1" class = "tabOne active">About</a></li>
      <li><a href="#panel2" class = "tabTwo">Details</a></li>
      <li><a href="#panel3" class = "tabThree">Contact Us</a></li>
    </ul>

The CSS has been dealt with in regard to inactive, which just leaves the scripting.

The inactive class is only added in one place:

        $(".tabs a").removeClass("active").addClass("inactive");

And it’s never removed. That’s yet another bug that’s been found. I feel I must state that I didn’t deliberately add all these problems into the code. I just google-searched for some jquery tabs and took the first code I found.

Removing that inactive class from the scripting code is the best thing for it.

        $(".tabs a").removeClass("active");

Undeclared ‘$’. (active and inactive tabs)

Converting this part to vanilla JavaScript is quite easy. The only slight complication is that jQuery is removing the class from multiple elements, while vanilla Javascript only does that from a single element, meaning that we’ll have to do it while looping through a collection of elements.

This is a conversion that doesn’t have to be done rapidly either. When I do these things quickly, sometimes it doesn’t work and I end up tweaking things or stopping to debug to figure out what went wrong.

To avoid all of that, we can take it slower, one step at a time, checking that our test page keeps on working at every step as we go.

First, we assign the collection of tabs to a local variable.

        // $(".tabs a").removeClass("active");
        const tabs = $(".tabs a");
        tabs.removeClass("active");

Then we use document.querySelectorAll to get those tabs, using $(tabs) on the second line to keep the removeClass working for the moment.

        // var tabs = $(".tabs a");
        const tabs = document.querySelectorAll(".tabs a");
        // tabs.removeClass("active");
        $(tabs).removeClass("active");

We can then loop through each of the tabs, and remove the class from a single tab at a time:

        const tabs = document.querySelectorAll(".tabs a");
        // $(tabs).removeClass("active");
        tabs.forEach(function removeClass(tab) {
            $(tab).removeClass("active");
        });

Note: My first time through this I used querySelector instead of querySelectorAll, and the loop failed to work. Oops :slight_smile: Taking it slow has its benefits, as there are less things to check when things go wrong.

And lastly, we can use classList to remove the class.

        const tabs = document.querySelectorAll(".tabs a");
        tabs.forEach(function removeClass(tab) {
            tab.classList.remove("active");
        });

The tests continue to pass, and the tabs keep on working at every stage through this conversion. How does JSLint feel about the code changes that I’ve done?

It complains about something else. typical

Undeclared ‘$’. (add active and blur)

Here’s the code in question:

        $(tab).addClass("active").blur();

When jQuery code is chained together like that, it’s often easier to convert when we split it apart.

        // $(tab).addClass("active").blur();
        $(tab).addClass("active");
        $(tab).blur();

As tab is just a normal element:

        const tab = evt.target;
        ...

We can use classList to add the active class to it.

        // $(tab).addClass("active");
        tab.classList.add("active");
        $(tab).blur();

And the blur, that prevents the clicked link from having focus, which in some browsers shows a dotted box around the link. Even with blur disabled I don’t seem to see any difference on my browser, but it might be something specific to a few other browsers. There’s no harm having it, and leaving it in might help in a “bootstraps and braces” kind of way.

        tab.classList.add("active");
        // $(tab).blur();
        tab.blur();

And that’s the tabs section of the code all converted.

Next steps

Looking through the code that we’ve been working with in the tabClickHandler function, I see that we have three main sections. One that hides the panels, one that works with the tabs, and one that shows a panel.

Next time we’ll move these out to separate functions, and make progress on converting the rest of the panel jQuery code before we finish up with the event handlers at the end.

4 Likes

In part 7 of converting jQuery to vanilla JavaScript, we separate the code into well-named functions, improve the preventDefault behaviour, check the new functions for if further improvements are needed, and convert the jQuery container color code to vanilla JavaScript.

Separating the code

I am seeing in the code though that tabClickHandler code that we hide the panels, work with the tabs, then work with panels again to show one of them.

Those are three very clear and different things that the function is doing, and should be divided up to represent that.

My code editor has a nice refactor feature, where you can select some code and tell it to extract to a function.

Doing that, I’ve extracted out three functions, one to update tabs, one to hide the panels, and one to show a panel.

    function updateTabs(tab) {
        var tabs = document.querySelectorAll(".tabs a");
        tabs.forEach(function removeClass(tab) {
            tab.classList.remove("active");
        });
        tab.classList.add("active");
        tab.blur();
    }
    function hidePanels() {
        const panels = document.querySelectorAll(".panel");
        panels.forEach(function (panel) {
            panel.classList.add("hide");
        });
    }
    function showPanel(tab) {
        var panelContainerColor = $(tab).css("background-color");
        $(".panelContainer").css({
            backgroundColor: panelContainerColor
        });
        var panel = $(tab).attr("href");
        $(panel)[0].classList.remove("hide");
        $(panel)[0].classList.add("fade-in");
    }

The main benefit of doing that is that the tabClickHandler function becomes so much easier to understand now.

    function tabClickHandler(evt) {
        const tab = evt.target;

        hidePanels();
        updateTabs(tab);
        showPanel(tab);

        return false;
    }

Prevent default

An example of what’s easier to understand, is that return false is now easily seen to prevent the default behaviour from when clicking on a link. We don’t care about what the function is returning, so we shouldn’t have return there.

Instead, we should make that desired behaviour much more explicit, by using preventDefault instead.

It’s also better to do that near the start of the function so that our use of the evt object all happens in the one place.

We now have a simple update that can be made.

    function tabClickHandler(evt) {
        const tab = evt.target;
        evt.preventDefault();

        hidePanels();
        updateTabs(tab);
        showPanel(tab);

        // return false;
    }

The tabClickHandler now tells us that we prepare things beforehand, hide panels, update tabs, and show a panel.

Update the updateTabs function

The updateTabs function is on the edge of being complex.

    function updateTabs(tab) {
        var tabs = document.querySelectorAll(".tabs a");
        tabs.forEach(function removeClass(tab) {
            tab.classList.remove("active");
        });
        tab.classList.add("active");
        tab.blur();
    }

However, the tab used in the removeClass function is different from the tab used in the updateTabs function, so for clarity it makes good sense to move that code out to a separate removeAllActive function.

    function removeAllActive() {
        var tabs = document.querySelectorAll(".tabs a");
        tabs.forEach(function removeClass(tab) {
            tab.classList.remove("active");
        });
    }
    function updateTabs(tab) {
        removeAllActive();
        tab.classList.add("active");
        tab.blur();
    }

The hidePanels function is all good and doesn’t need updating, and JSLint now tells us that the next part to work on is in the showPanels function.

Undeclared ‘$’. (showPanels)

Here is the showPanels function:

    function showPanel(tab) {
        var panelContainerColor = $(tab).css("background-color");
        $(".panelContainer").css({
            backgroundColor: panelContainerColor
        });
        var panel = $(tab).attr("href");
        $(panel)[0].classList.remove("hide");
        $(panel)[0].classList.add("fade-in");
    }

This function is already getting large, and does two main different things. One being to set the background color of the panel container, and the other is to show one of the panels.

It might be premature to extract this code to two other functions right now, but we’ll revisit this idea as we update the code.

Panel container color

The color of the panel container comes directly from the color of the currently active tab. There are two possible ways to deal with this.

Currently the code investigates the CSS styles on the tab and directly copy the background color to the panel container. It’s slow to investigate the CSS styles, and another way is to add/remove class names.

I’ll stay with investigating the CSS styles, but the other way will be floating in the background of my mind as I convert the code.

Getting the background color is the first part:

        var panelContainerColor = $(tab).css("background-color");

We can use getComputedStyle() to achieve that with vanilla JavaScript.

        // var panelContainerColor = $(tab).css("background-color");
        const tabStyle = window.getComputedStyle(tab);
        const panelContainerColor = tabStyle["background-color"];

Now even though the tab color is going to used to set the panel container color, it’s more appropriate for the variable name to be tabColor, because that’s the source from where it comes from.

Let’s rename panelContainerColor to tabColor instead.

        const tabStyle = window.getComputedStyle(tab);
        // const panelContainerColor = tabStyle["background-color"];
        const tabColor = tabStyle["background-color"];
        $(".panelContainer").css({
            // backgroundColor: panelContainerColor
            backgroundColor: tabColor
        });

Setting the panel container color

As there’s only one panelContainer element, should a classname be used for that? A unique identifier is better when there’s only one element. Classnames are better when there’s multiple elements.

Just to be safe, I’ll use vanilla JavaScript to get all panel containers, so that it keeps on working if there do end up being multiple panel containers involved.

        const tabStyle = window.getComputedStyle(tab);
        const tabColor = tabStyle["background-color"];
        const panelContainers = document.querySelectorAll(".panelContainer");
        // $(".panelContainer").css({
        $(panelContainers).css({
            backgroundColor: tabColor
        });

We can now use the forEach method, and move the jQuery inside of there to work on one panel container at a time from the loop.

        const tabStyle = window.getComputedStyle(tab);
        const tabColor = tabStyle["background-color"];
        const panelContainers = document.querySelectorAll(".panelContainer");
        panelContainers.forEach(function setBackgroundColor(panelContainer) {
        // $(panelContainers).css({
        //     backgroundColor: tabColor
        // });
            $(panelContainer).css({
                backgroundColor: tabColor
            });
        });

And lastly, we can use setProperty to set the background color of the element. Do you see how jQuery is setting backgroundColor for a property that’s called background-color? That’s because background-color: tabColor doesn’t work, and they didn’t want to use a quoted property "background-color": tabColor which does work.

I think that it’s important to remain consistant with the same style name. Instead of flip-flopping back and forth from background-color to backgroundColor, the code is clearer by remaining consistantly with one usage of it.

Just to be sure though, I’ll try renaming backgroundColor first.

        const tabStyle = window.getComputedStyle(tab);
        const tabColor = tabStyle["background-color"];
        const panelContainers = document.querySelectorAll(".panelContainer");
        panelContainers.forEach(function setBackgroundColor(panelContainer) {
            $(panelContainer).css({
                // backgroundColor: tabColor
                "background-color": tabColor
            });
        });

And now we can change that to vanilla JavaScript in one simple step.

        const tabStyle = window.getComputedStyle(tab);
        const tabColor = tabStyle["background-color"];
        const panelContainers = document.querySelectorAll(".panelContainer");
        panelContainers.forEach(function setBackgroundColor(panelContainer) {
            // $(panelContainer).css({
            //     "background-color": tabColor
            // });
            panelContainer.style.setProperty("background-color", tabColor);
        });

Improving the code structure

That code is just doing two things, but it takes a lot of lines.

I would prefer it if the code instead was:

const tabColor = getBackgroundColor(tab);
setBackgroundColor(".panelContainer", tabColor);

So let’s do that.

The tabStyle and tabColor lines can be extracted out to a getBackgroundColor function. Here’s the getTabColor function:

    function getBackgroundColor(tab) {
        const tabStyle = window.getComputedStyle(tab);
        return tabStyle["background-color"];
    }
    function showPanel(tab) {
        const tabColor = getBackgroundColor(tab);
        ...
    }

But for the setBackgroundColor function, I’ll use JSLint to ensure that nothing gets missed out.

Selecting the lines and extracting to global scope, results in:

    function showPanel(tab) {
        const tabColor = getBackgroundColor(tab);
        setBackgroundColor(tabColor);
        var panel = $(tab).attr("href");
        $(panel)[0].classList.remove("hide");
        $(panel)[0].classList.add("fade-in");
    }
...
function setBackgroundColor(tabColor) {
    const panelContainers = document.querySelectorAll(".panelContainer");
    panelContainers.forEach(function setBackgroundColor(panelContainer) {
        panelContainer.style.setProperty("background-color", tabColor);
    });
}

That’s not appropriate, as I want “.panelContainer” to be a separate variable. I’ll undo that and set it as a panelSelector variable first.

        const containerSelector = ".panelContainer";
        const tabColor = getBackgroundColor(tab);
        const panelContainers = document.querySelectorAll(containerSelector);

And now the refactor to extract to setBackgroundColor properly works.

    function setBackgroundColor(containerSelector, tabColor) {
        const panelContainers = document.querySelectorAll(containerSelector);
        panelContainers.forEach(function setBackgroundColor(panelContainer) {
            panelContainer.style.setProperty("background-color", tabColor);
        });
    }
    function showPanel(tab) {
        const containerSelector = ".panelContainer";
        const tabColor = getBackgroundColor(tab);
        // const panelContainers = document.querySelectorAll(containerSelector);
        // panelContainers.forEach(function setBackgroundColor(panelContainer) {
        //     panelContainer.style.setProperty("background-color", tabColor);
        // });
        setBackgroundColor(containerSelector, tabColor);
        ...
    }

I can now inline the containerSelector variable, down to the setBackgroundColor call, and rename the function property to cssColor instead.

    // function setBackgroundColor(containerSelector, tabColor) {
    function setBackgroundColor(containerSelector, cssColor) {
        const panelContainers = document.querySelectorAll(containerSelector);
        panelContainers.forEach(function setBackgroundColor(panelContainer) {
            // panelContainer.style.setProperty("background-color", tabColor);
            panelContainer.style.setProperty("background-color", cssColor);
        });
    }
    function showPanel(tab) {
        // const containerSelector = ".panelContainer";
        const tabColor = getBackgroundColor(tab);
        setBackgroundColor(".panelContainer", tabColor);
        ...
    }

Also updating the setBackgroundColor, as we are already within the context of the container, we don’t need to be as explicit with the names either.

    function setBackgroundColor(containerSelector, cssColor) {
        // const panelContainers = document.querySelectorAll(containerSelector);
        const containers = document.querySelectorAll(containerSelector);
        // panelContainers.forEach(function setBackgroundColor(container) {
        containers.forEach(function setBackgroundColor(container) {
            container.style.setProperty("background-color", cssColor);
        });
    }

And that code is much more appropriate.

The updated code

The updated script that we have at this stage is:

(function iife() {
    function removeAllActive() {
        var 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);
        var panel = $(tab).attr("href");
        $(panel)[0].classList.remove("hide");
        $(panel)[0].classList.add("fade-in");
    }
    function tabClickHandler(evt) {
        var tab = evt.target;
        evt.preventDefault();

        hidePanels();
        updateTabs(tab);
        showPanel(tab);
    }
    $(".tabs a").click(tabClickHandler);

    $(".tabs li:first a").click();
}());

Next steps

Next time we finish off converting the showPanel function by converting the remaining showPanel code from jQuery to vanilla JavaScript, then we finish off with the events at the end of the code.

4 Likes

In part 8 of converting jQuery to vanilla JavaScript, we convert the last parts of the showPanel function, and the code to add a click listener to the tabs, from jQuery to vanilla JavaScript.

JSLint says that the next part of the code to work on is the following:

Undeclared ‘$’. (tab href)

        var panel = $(tab).attr("href");

That code isn’t getting the panel itself. We can confirm this by logging the panel variable to the console.

        var panel = $(tab).attr("href");
        console.log(panel);
#panel1
#panel2
#panel3
#panel1

So what’s more correct is that it’s a panelId instead.

        const panelId = $(tab).attr("href");
        const panel = $(panelId);

How do we get that href string in vanilla JavaScript? We can’t just use tab.href.

        const panelId = tab.href;
        console.log(panelId);
        const panel = $(panelId);

Instead of id references, we get a fully qualified url instead.

http://127.0.0.1:8080/#panel1
http://127.0.0.1:8080/#panel2
http://127.0.0.1:8080/#panel3
http://127.0.0.1:8080/#panel1

There are a few ways to deal with that. One is to directly access the HTML href attribute of the element, and another is to use the hash method that’s a part of the location interface. I’ll use that second method this time.

        const panelId = tab.hash;
        const panel = $(panelId);

And we can now easily combine those two lines:

        // const panelId = tab.hash;
        // const panel = $(panelId);
        const panel = $(tab.hash);

And use document.querySelector to get the element:

        // const panel = $(tab.hash);
        const panel = document.querySelector(tab.hash);

Undeclared ‘$’. (remove hide)

The next thing to convert is removing the hide class.

        const panel = document.querySelector(tab.hash);
        $(panel)[0].classList.remove("hide");
        ...

We can just replace $(panel)[0] with panel and this is all good to go.

        const panel = document.querySelector(tab.hash);
        // $(panel)[0].classList.remove("hide");
        panel.classList.remove("hide");
        ...

Undeclared ‘$’. (add fade-in)

The last thing in this showPanel function to convert is adding a fade-in class.

        const panel = document.querySelector(tab.hash);
        panel.classList.remove("hide");
        $(panel)[0].classList.add("fade-in");
    }

We can do a similar replacement as with the remove hide code here too.

        const panel = document.querySelector(tab.hash);
        panel.classList.remove("hide");
        // $(panel)[0].classList.add("fade-in");
        panel.classList.add("fade-in");
    }

Undeclared ‘$’. (tab click)

    $(".tabs a").click(tabClickHandler);

Here we loop through each of the tab links, and attach the tabClickHandler.

A reliable way to convert this is a piece at a time. First by getting all of the tab links, then by looping through them, then finally converting what’s inside of the loop.

Here are the tab links:

    // $(".tabs a").click(tabClickHandler);
    const tabLinks = document.querySelector(".tabs a");
    $(tabLinks).click(tabClickHandler);

And the tests break. Gah! I used querySelector instead of querySelectorAll again. Still, doing things this way helps us to easily find and fix the problem.

    // const tabLinks = document.querySelector(".tabs a");
    const tabLinks = document.querySelectorAll(".tabs a");
    $(tabLinks).click(tabClickHandler);

Now we loop through the tabLinks:

    const tabLinks = document.querySelectorAll(".tabs a");
    // $(tabLinks).click(tabClickHandler);
    tabLinks.forEach(function addClickHandler(tabLink) {
        $(tabLink).click(tabClickHandler);
    });

And lastly we use addEventListener to attach the handler to each tab.

    const tabLinks = document.querySelectorAll(".tabs a");
    tabLinks.forEach(function addClickHandler(tabLink) {
        // $(tabLink).click(tabClickHandler);
        tab.addEventListener("click", tabClickHandler);
    });

But, that causes the tests to break, because jQuery is trying to use its own way to trigger a click on the first tab, and doesn’t know about addEventListener events.

    $(".tabs li:first a").click();

So, we temporarily step back to the previous code:

    const tabLinks = document.querySelectorAll(".tabs a");
    tabLinks.forEach(function addClickHandler(tabLink) {
        // tab.addEventListener("click", tabClickHandler);
        $(tabLink).click(tabClickHandler);
    });

and fix the first tab click problem instead.

First tab click

Here’s the code that clicks on the first tab.

    $(".tabs li:first a").click();

It’s much easier now to get the first tab, for it’s just the first index in the tabLinks collection.

    // $(".tabs li:first a").click();
    tabLinks[0].click();

And we can now go back to finishing off the tab click listener.

Tab click listener

The last of the jQuery is being used to attach a tab click handler.

    tabLinks.forEach(function addClickHandler(tabLink) {
        $(tabLink).click(tabClickHandler);
    });

Let’s use the vanilla JavaScript addEventListener to attach the handler.

    const tabLinks = document.querySelectorAll(".tabs a");
    tabLinks.forEach(function addClickHandler(tabLink) {
        // $(tabLink).click(tabClickHandler);
        tab.addEventListener("click", tabClickHandler);
    });

And that’s the last of the jQuery code all converted to vanilla JavaScript.

Remove the jQuery library

We can now remove the jQuery library from the HTML code.

  <!-- <script src="js/jquery.min.js"></script> -->
  <script src="js/script.js"></script>

And it should be no surprise by now, the tests for the code continue to work perfectly well, and the tabs happily change panels with no trouble.

We have successfully removed all of the jQuery from this code, all while keeping the page fully working all throughout the process.

Next steps?

Is there anything else to be done? JSLint is happy with the code so there’s nothing urgent to be fixed, but it does show that there’s a lot of functions.

I can see that the functions are mainly related to either tabs or panels, so the next and possibly last part will attempt to bring together some of these related functions.

4 Likes

Great series of posts Paul :slight_smile:

The problem is that the tabs have been set to a width of 8rem so when you corrected the typo in box-sizing:border-box that would reduce the available space for the 'Contact Us’ tab forcing the ‘Us’ text to wrap to a new line and thus making that tab twice as tall.

A better approach would have been to use min-width of 8rem rather than width and that would allow the text to grow without wrapping should some systems display fonts at wider widths or indeed when a user resizes text. (These days it would also have been better to use flex so that all tabs maintain the same height irrespective of content and avoid being mismatched.)

3 Likes

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

Celebrations! :tada: :pizza: :zzz:

6 Likes

Not so fast, Kemosabe. While the steps are done, there’s more to be learned.

Lessons learned

It was interesting working on the above 9 part series about converting jQuery to JavaScript, but what’s even better is when something can be learned from them. Here’s some useful lessons that I found while reflecting over the above posts.

Lessons from Part 1

The first post was quite small, primarily because I didn’t know how large a task this seemingly-simple conversion was going to be. This was because it’s hard to estimate programming time, so these articles on How to estimate programming time and How to Get Better at Estimating Software Development Time can be quite useful to keep in mind.

Lesson: Estimating time is really hard

Also, because I didn’t want to take people through the toolkit needed to convert SCSS to CSS and wanted to use my VSCode for coding instead of codePen, as that would let me work on the code offline while travelling. As a result of that I converted the SCSS to CSS, which kept things as just a simple HTML+CSS+JS.

In hindsight I should have kept the code on codePen, as I didn’t end up doing any offline work. Keeping the code on codePen would have let me retain the SCSS as it was, and codePen makes it easier to manage working with different variations of your code.

Lesson: Think carefully before changing code deveopment environments

What should I do based on that lesson? I’ll use codePen while working through these lessons, so that a range of links can be provided for the end of each part. The initial forked code is at https://codepen.io/pmw57/details/jOPamOw

Lessons from Part 2

Bugs were found while attempting to convert the code, so tests were used before fixing the bug. Tests helps you to find and focus on what you want to fix, and provide a reliable and repeatable way to ensure that the code keeps doing what it’s supposed to be doing.

Lesson: Tests are reliable and repeatable

It’s also easier to motivate yourself to write tests before doing things, instead of afterwards. I used that fact to let me write tests before fixing the bug, and before converting any of the code.

Lesson: Write tests before working with code, not after

Lessons from Part 3

Using tests to focus in on a bug that you’re fixing, makes it really easy to find an fix the bug. Especially when you have coverage tests that ensure everything else keeps working too.

Lesson: Tests make bugs really easy to fix

Also, linters are a really easy way to reliably improve your code. When you’re about to make lots of changes to code, you don’t want to be puzzling over things that linters can easily fix. You want as much of your concentration to be directed at what you’re working on, instead of on other irrelevant details that surround the project.

Lesson: Linters give consistent and reliable code

Lessons from Part 4

When faced with the fadein part, I was surprised to find that there are other potentially better ways that don’t involve much JavaScript at all.

Lesson: Don’t be afraid to look for solutions outside of JavaScript

Tests help to remove the fear of making changes to the code. The initial conversions were easily achieved because the tests helped to give immediate feedback that everything still works properly after changes were made.

Lesson: Good tests remove the fear of changing code

Lessons from Part 5

Tests are excellent at giving immediate feedback about problems. The fast feedback means that there’s not much code to check.

Lesson: Tests give fast feedback loops

This is also the first time that we used the simplest bad code to get a test working, so that with a working test we were then able to easily improve the code while keeping the test working too.

Lesson: Test-driven development makes it easy to improve code

Lessons from Part 6

When changing the active/inactive classes so that it was more consistent with common practice, more problems were found with the code, giving us a good opportunity to fix them up.

Lesson: Common practices make it easier to reason about the code

More bugs were found in the code while converting it. All of these bugs resulted in unexpected behaviour occurring, that would have been caught and fixed if some simple tests has been put in place while developing the code.

Lesson: Bugs can thrive in untested code

Lessons from Part 7

Frequently the code makes it quite clear that different things are being done in the same place. Functions tend to be easiest to work with, and to update, when they only have one main thing that they take deal with.

Lesson: Extract code into well-named functions for simple and easily maintained code

When there were a lot of possible things that could next be done to convert the jQuery code, it’s really helpful when those decisions don’t need to be made. The linter really helped to free up some cognitive load, making it easier to focus on what was to be converted instead.

Lesson: Use systems that help to reduce cognitive-load

Lessons from Part 8

When trying to convert the tab href part of the code, I found that I was getting confused by the panel variable name. We didn’t even need to keep that renamed variable, but fixing it helped to make it much easier to think about the problem, and paved the way to an easier situation.

Lesson: Good variable names are a challenge, but worth while

Later on we faced some trouble when converting jQuery event code. Instead of pushing on through we undid what was done and with passing tests, fixed the problem so that we could then do it again with no troubles.

Lesson: Ensure tests remain green when refactoring

Lessons from Part 9

When the code was separated into different functions, it became easy to see that there were two clear types of functions. Dividing the code in to two distinct separate sets of code became the clear solution.

The current code is better than it was, but in hindsight I would have used a module pattern so that functions in the same place can call each other in an easier way. I’ve updated the code to achieve this, and added a Lessons update to the collection of code.

Lesson: Always seek to improve the code every time you work with it

Fast changes were possible with the code thanks to the support of JSLint and code tests. They helped to provide direction about what next to fix, and gave assurance when everything works.

Lesson: Linters and tests help you to help you to go fast with confidence

List of lessons from each part

  • Part 1
    • Estimating time is really hard
    • Think carefully before changing code deveopment environments
  • Part 2
    • Tests are reliable and repeatable
    • Write tests before working with code, not after
  • Part 3
    • Tests make bugs really easy to fix
    • Linters give consistent and reliable code
  • Part 4
    • Don’t be afraid to look for solutions outside of JavaScript
    • Good tests remove the fear of changing code
  • Part 5
    • Tests give fast feedback loops
    • Test-driven development makes it easy to improve code
  • Part 6
    • Common practices make it easier to reason about the code
    • Bugs can thrive in untested code
  • Part 7
    • Extract code into well-named functions for simple and easily maintained code
    • Use systems that help to reduce cognitive-load
  • Part 8
    • Good variable names can be tricky, but are well worth the effort
    • Ensure tests remain green when refactoring
  • Part 9
    • Always seek to improve the code every time you work with it
    • Linters and tests help you to help you to go fast with confidence

Code for each part

The code at the end of each part of the above posts, and for the update made in this post, has been kept track of in the following codePen collection.
https://codepen.io/collection/XerjyL?grid_type=list&sort_col=created_at&sort_order=asc

4 Likes

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.