Proper websites, done properly

Part 4: Triggering menu events

6 minute read time / 751 words

Our class is running, but it's not really doing much at the moment. Time to make our toggle actually toggle!

Make the toggle do something

We need to update our _addToggleListener() function to actually do what it says on the tin.

_addToggleListener () {
    if (this.toggle.dataset.listenerAdded !== undefined) {
        return;
    }

    console.log('MobileNav: _addToggleListener. listener already added:', this.toggle.dataset.listenerAdded);

    this.toggle.addEventListener('click', e => {
        console.log('MobileNav: toggle click');
    });

    this.toggle.dataset.listenerAdded = true;
}

The first thing this function does is return early if it doesn't need to do anything. This is general good practice and I try to return early as often as I can. In this case, we don't want to keep trying to add event listeners to elements, so we check if we've previously set a data- attribute on the toggle or not.

If we have, we exit the function. If we haven't we go ahead and output some debugging information, and then add a click event listener to the toggle element.

For now, all this click event will do is trigger a message in the console saying that it's been clicked. Go ahead and try it.

After we add the click event listener, we add a data attribute onto the toggle element and set it to true so that if we try to run this function again before page refresh, it will return early as promised.

Now we want to be able to have the toggle decide whether it needs to run the open or the close function when you click it. The easiest way to do this is either via a data attribute or a class. Since I generally use classnames for active states I usually opt for this approach. Let's update our _addToggleListener() function:

_addToggleListener () {
    if (this.toggle.dataset.listenerAdded !== undefined) {
        return;
    }

    console.log('MobileNav: _addToggleListener. listener already added:', this.toggle.dataset.listenerAdded);

    this.toggle.addEventListener('click', e => {
        console.log('MobileNav: toggle click');

        if (this.toggle.classList.contains('is_active')) {
            this._close();
        } else {
            this._open();
        }
    });

    this.toggle.dataset.listenerAdded = true;
}

This won't do much for now, but once we set up our open and close functions, we will be either adding or removing our 'is_active' class which will let our listener know what to do when the toggle is clicked. If you try to run this now in your browser, you'll see in the console a message saying 'MobileNav: toggle click.', followed by 'MobileNav: _open.'

Open or close?

Now we need to add some basics into our open and close functions so that our click event will do a bit more. In our open() function, add the following:

this.toggle.classList.add('is_active');

And in our close() function:

this.toggle.classList.remove('is_active');

Try out the toggle in your browser, and you'll now see that it will alternate between running the close and open functions. If you look in the elements panel, you'll be able to see the 'is_active' class being added and removed from the toggle too.

Sensible improvements

Let's make this a bit less repetitive and open to user error during maintenance or updates. Our active class would be better as a variable for starters, and it would be good if the dev initialising it could change what it is when they initialise the class. What would be even better is if we could provide a default value automatically if a value isn't provided during initialisation.

We can do all of those things, and very easily too! We can provide default values for function arguments which will allow us a great deal of flexibility. First we need to update our class to allow this. Update the constructor to match this:

constructor (
    container,
    toggle,
    activeClass = 'is_active'
) {
    if (!container || !toggle) {
        console.log('MobileNav. Exiting constructor. container:', container, 'toggle:', toggle);

        return;
    }

    this.container = container;
    this.toggle = toggle;
    this.activeClass = activeClass;
}

Now, instead of writing 'is_active' throughout this file, we can just use this.activeClass instead. If we're happy with this, that's all we need to do, because in our constructor we've provided activeClass a default value of 'is_active'.

Let's say we want to change this to something else though. Open up our entry file, and update the initialisation call to the following:

const mobileNav = new MobileNav(navContainer, navToggle, 'sexy-times');

That's it. Now when you toggle the menu using the button, the 'sexy-times' class will be added and removed instead.