Another approach to adding icons to dynamically generated links

Another approach to adding icons to dynamically generated links

In my previous post, I outlined the code necessary to add icons to navigation links. I have since updated my implementation to use a function to add the link-to-icon mapping.

I have refactored my theme code to use the new approach.

The GitHub repository is ghost-cerkit-theme.

Instead of using JSON to define the link-to-icon mapping (as outlined in the previous post), we use an object on the window object:

window.linkIconMap = {};

Then, we define a function that we can use to add a new mapping:

window.addLinkIcon = function (target, icon, size) {
    // check to see if we have maps defined already
    if (!window.linkIconMap.maps) {
        // if not, define it
        window.linkIconMap.maps = [];
    }

    // if we have a size passed in, use it, otherwise use the default icon size on our icon map. If that's missing, use nothing (Font Awesome default size)
    var iconSize = size ? size : 'defaultIconSize' in window.linkIconMap ? window.linkIconMap.defaultIconSize : '';
    window.linkIconMap.maps.push({ "target": target, "icon": icon, "size": iconSize });
};

First, we see if our maps array exists. If not, we create it. This prevents us from doing anything if the theme user never added any mappings.

After we check on the maps, we figure out what size to use. Basically, the same rules apply as before: if the map does not define a size, then we use the default size stored in defaultIconSize on our window.linkIconMap (but only if it's defined).

// sample definition for icon size
window.linkIconMap.defaultIconSize = 'fa-lg';

As a last resort, we fall back on an empty string, which has the effect of using the default size from the Font Awesome font.

In order for the theme user to create the map, they simply call the window.addLinkIcon() function:

// Navbar Icon Map
window.addLinkIcon('nav-home', 'fa-home');
window.addLinkIcon('nav-about', 'fa-user');
window.addLinkIcon('nav-my-public-key', 'fa-key');
window.addLinkIcon('nav-test', 'fa-cogs', 'fa-2x' /* optional */);

Note: you may notice that it's possible to bind an icon to any class on your page, not just navigation links. For example, I just set up my navbar expansion button (displayed when the screen is too narrow to show all the buttons) so that it does not have anything in it. I then add a call to window.addLinkIcon('navbar-toggle', 'fa-sitemap', 'fa-2x'); for it. This will then apply to all navbar toggles.

Here is where the toggle button is defined.

When the page loads, it calls the following code:

function bindLinkIcons() {
    if (window.linkIconMap.maps) {
        var curIconMap;
        var curSize;

        for (var i = 0; i < window.linkIconMap.maps.length; i++) {
            // get a handle on the current icon map
            curIconMap = linkIconMap.maps[i];

            // set the icon on the navbar item
            createIcon(curIconMap.target, curIconMap.icon, curIconMap.size);
        }
    } else {
        console.warn('cerkit-bootstrap theme supports navbar link icons. Add the following to your footer in code injection: \<script\>window.addLinkIcon(/* target = */ "nav-home", /* icon = */ "fa-home", /* (optional) size = */ "fa-3x");\</script\>');
    }
}

$(bindLinkIcons);

We're only going to bind the icons if we have a map array to work with. If we do, then we simply loop through each entry and call the createIcon() function, passing in the relevant information.

Here is the createIcon() function definition:

function createIcon(target, icon, size) {
    var iconElement = $(document.createElement('i')).attr('class', 'link-icon fa fa-fw ' + icon + ' ' + size).append('&nbsp;');
    var targetNavbarItem = $('.' + target);
    var targetItemFirstChild = $(targetNavbarItem).children()[0];

    // figure out if the nav item has any links in it. If so, use that as the icon parent.
    // Otherwise, use the navbarIconItem.
    var iconParentElement = targetItemFirstChild == null ? targetNavbarItem : targetItemFirstChild;

    // insert the icon element at the beginning of the parent
    $(iconParentElement).prepend(iconElement);
}

The first thing we do is create a new i tag to use as the icon container element. We then add the appropriate class attributes based on the icon and size values passed into the function. ~~After the icon is defined, we append a non breaking space so that the icon isn't too close to the link. ~~ We then add the link-icon class to each of the icons that gets bound to a link. Then, in the site's css file, we simply add the following code:

.link-icon {
    margin-right: 3px;
}

The next thing that happens is the code gets a handle on the target by selecting it based on the class name passed into the function (as the target argument).

Then, the first child element is selected (it is assumed that this element will either be a a tag or plain text. Whatever it is, we'll prepend our new i element representing our icon.

I believe that this approach is superior to the original approach as it is friendlier and more expressive to call a function to add a mapping rather than some arbitrary (and potentially confusing) JSON code.

I have created a Gist file that contains the core code to make this work.