Proper websites, done properly

Part 3: Our MobileNav class

7 minute read time / 899 words

We know how to import, initialise and get classes running, so let's write some real functionality now.

Outlining our class

Before we dive in, let's take a moment to figure out what our MobileNav class is actually going to need.

Besides our constructor and initialisation functions, we're going to probably need at least the following functionality:

  • Open menu function
  • Close menu function
  • Button click event to trigger opening or closing the menu
  • Destroy menu functionality if we switch between mobile and desktop size screens

Some of the functionality we'll write will only be accessed internally to the class, and other parts will be called from the entry file. Some may be called from both. There are ways to keep properties and functions either public or private, but I tend to just denote functions I expect to only use internally by starting the function name with an underscore (_).

For our MobileNav class, the init() and destroy() functionality will be needed outside of the class, and the open() and close() functions will be used internally only.

Create our base functions

Create a new file at /public/js/MobileNav.js and get the skeleton structure in place:

export default class MobileNav {
    constructor (container, toggle) {
        if (!container || !toggle) {
            console.log('MobileNav. Exiting constructor. container:', container, 'toggle:', toggle);

            return;
        }

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

    init () {
        console.log('MobileNav: init.');
    }
}

We know we're going to need at least a 'container' element where the navigation will be held on the page, and we know we're going to need a button 'toggle' to trigger opening and closing the menu, so it makes sense we should have arguments and checks for both those things from the off.

Notice that I've left out this.init() from the constructor() function. This is because we want our entry file to initialise our mobile menu for us only when it's needed, and remove the functionality when it isn't.

Let's get our base functions in place now as well. Add the following inside the class and after our init() function.

destroy () {
    console.log('MobileNav: destroy.');
}

_open () {
    console.log('MobileNav: _open.');
}

_close () {
    console.log('MobileNav: _close.');
}

We also need to add the click events listener too, so add a function for that too:

_addToggleListener () {
    console.log('MobileNav: _addToggleListener.');
}

We've added console.log lines into each function to begin with so we know when they're triggering even before we've done any real functionality. This is an obvious and very simple way to avoid daft early headaches!

So now we've got some functions in place, but how does any of it work? Well, we need to make sure that when our init() function is called it can go away and attach the event listeners, and do any jobs it needs to do without any other help.

init () {
    console.log('MobileNav: init.');

    this._addToggleListener();
}

Import and initialise our new class

That's it. Sort of. If you go and check your page, there'll still be nothing happening of note. We need to import the class into our entry point and get it initialised!

import MobileNav from './MobileNav.js';

const mobileNav = new MobileNav(false, false);

Do the above in our entry file, and remove any other code. If you now check your browser console, you should see that we get the familiar 'Constructor exiting' message again. This is because we've just passed in false for both the container and the toggle arguments. Let's fix that now.

We already added a nav block into our Eleventy site previously, so we can use this for our container. To make things easier, if we're targeting an element in JavaScript, it's generally better to find it via an ID - because those have to be unique per page so finding them should be efficient, and if other developers work on your code too, they should know better than to change those, but should have no fear of altering element types or classnames for future updates.

Open up our /src/site/includes/nav.njk include and give our <nav> element a sensible ID.

{% from 'macros/navItem.njk' import navItem %}

<nav id="site-nav">
    <ul>
        {% set pages = collections.all | eleventyNavigation %}

        {%- for entry in pages %}
            {{ navItem(entry, page) }}
        {% endfor %}
    </ul>
</nav>

We're also missing a toggle element too, so let's add a new <button> to this file too, just before the nav element. Don't forget to give it an ID too!

<button type="button" id="site-nav-toggle">Menu</button>

Now we should be able to initialise our MobileNav class without the constructor exiting, we just need to pass these new elements into our class from our entry file.

We need to set up two variables, pass them to the instance we're creating, and then run our MobileNav init() function.

import MobileNav from './MobileNav.js';

const navContainer = document.getElementById('site-nav');
const navToggle = document.getElementById('site-nav-toggle');

const mobileNav = new MobileNav(navContainer, navToggle);

mobileNav.init();

Check your console again, and now you should see that you've got a message saying that init() is running, and that _addToggleListener has also run.

Now we have a skeleton MobileNav class, that have imported and initialised successfully. Next we need to make the button trigger the corrrect functionality when clicked.