Generate HTML Markup in JavaScript With Pure Functional Programming

This post is a follow up to my previous post, An Introduction to Functional Programming in JavaScript. The aim of this post is to use the functional patterns introduced in my previous post to generate markup for a list of HTML select options and to highlight how a functional approach can lead to both cleaner and more reusable code. As a lot of the concepts used this post are explained in detail in the previous post, I will not be repeating them here so if anything doesn’t make sense, I would recommend referring to that post.

Generating Markup for HTML Select Options

HTML Dropdown Menu

HTML month-year select menu options.

For a website I am currently working on, I need to take a list of year-month strings in the format ‘yyyy-mm’ and generate a list of HTML select menu options. The options list needs to be generated dynamically as it changes depending on what data is being displayed. The general form of a HTML select menu (dropdown menu) is:

  Option 1
  Option 2
  ...
  Option n

In this example I will be showing how to generate the HTML select options markup for the individual year-month pairs using pure functional programming. The aim is to take an array of year-month strings in the form:

var rawMonths = ['2010-10', '2011-01', '2011-12', '2012-05'];

and generate the following HTML string of options with the value parameter equal to the raw year-month string and the display text for the option to be in the form ‘September 2014’:

<option value="2010-10">October 2010</option>
<option value="2011-01">January 2011</option>
<option value="2011-12">December 2011</option>
<option value="2012-05">May 2012</option>'

This string can then be appended to the select element that needs populating with options.

In order to follow this post the following functions from the previous post (Intro to Functional Programming in JavaScript) will be required (the code for these functions is included at the end of this post but I will not be repeating how they work here):

  • curry
  • compose
  • map
  • reduce
  • add

These functions can be included anywhere in your .js file or more practically you can keep them in a separate file and load them first. I recommend the latter option as we will be adding to these general functions as we go along. All the functions that follow are designed along the principles outlined in the previous post, all are pure functions in that they rely only on their arguments and have no side effects and all functions are curryable. We will complete our task by defining a series of general small and reusable functions and combining them using combinations of currying and composition to achieve more specific outcomes. (if you are not familiar with these concepts I recommend that you read the previous post first).

Taking a single year-month string as an example, we will go through the steps required to build up the corresponding HTML option markup and then apply this to an array of year-month strings to generate the markup for all the options.

For the purpose of illustrating this example we will use the year-month string ‘2010-10’. First up we need a function to convert the string into the prettier format, ‘October 2010’. We then need to wrap this string in HTML option tags and finally we need to add a value parameter to the HTML tag and set the value equal to the original date string so we end up with:

October 2010

We will do this by defining and combining a series of small, specific and pure functions.

Step 1: Reformat Date String

We need to take a string of the format ‘yyyy-mm’ and convert it to a string in the format ‘Month yyyy’. To do this we need to extract the month and year parts of the original string, we do this using the JavaScript’s built in string split method. The built in split method takes a split pattern (as a string or a regular expression) and splits the string about this pattern into an array of substrings. In our example, we need to use the split pattern '-' if we want to feed it as string or /-/ using regular expressions, both are equivalent. For example, '2010-10'.split(/-/) returns ['2010', '10']. Following the pattern used in the previous post, we will redefine the split method as a curryable function that takes the split pattern and the string it is to act on as arguments:

var split = curry(function(splitPoint, str) {
    return str.split(splitPoint);
});

Defined like this split will return an array of substrings, however, we want to pick individual substrings so we will define a new function, splitN which will return the nth substring:

var splitN = curry(function(splitPoint, n, str) {
        return str.split(splitPoint)[n];
});

As splitN is curryable, we can apply the first two arguments to create new functions which when given the remaining argument (the raw year-month string) will return the month or the year:

var month = splitN(/-/, 1);
var year = splitN(/-/, 0);

For a detailed explanation of partial application and currying see the previous post, but briefly, var month = splitN(/-/, 1) is exactly the same as:

var month = function(str) {
    return str.split(/-/)[1];
};

Now we have extracted the month component from the string we need to convert it to a month in word format. We could do this using the following function:

var monthWord = function(i) {
    var monthWords = ["January", "February", "March", "April",
                    "May", "June", "July", "August",
                    "September", "October", "November", "December"];
    return months[parseInt(i, 10)-1];
};

The above implementation of wordMonth will work perfectly fine, it will take as a argument the month in numerical form (either as a string (e.g. ’10’) or a number (e.g. 10)) and return the name of the month but it is inefficient and we can easily improve on it.

In its current form the array containing the month names, monthWords has to be generated every time the function is called. We can avoid this by taking advantage of closure. In JavaScript a function has access to any variables or functions defined in the environment in which it was created. To take advantage of this fact we will define monthWord as follows:

var monthWord = (function(){
  var monthWords = ["January", "February", "March", "April",
                    "May", "June", "July", "August",
                    "September", "October", "November", "December"];

    return function(i) {
        return monthWords[parseInt(i, 10) - 1];
    };
}());

Here we define monthWord as an immediately invoked function expression. The empty parenthesis following the outer function definition cause it to be evaluated immediately. A brilliant feature of JavaScript’s closures is that a function will always have access to variables declared in the environment (closure) it was declared in itself even if the enclosing function has completed its evaluation and returned a value. Using this, monthWords is created only once when the wrapping function is invoked and the function it returns and assigns to monthWord will still have access to this array. Now, if we combine month and monthWord and apply to our raw year-month string, we get the month returned as a word:

console.log(monthWord(month('2010-10'))); // >> 'October'

Now we have the month in word format it is straight forward to create a function that returns the date in the format we want:

var pretyDate = function(uglyDate) {
    return monthWord(month(uglyDate)) + " " + year(uglyDate);
};

If we call pretyDate('2010-10') we get the string ‘October 2010’. It is easy to see how the functions we have built, split, splitN and monthWord are very general and as such can be reused over and again to build a variety of other functions. This is one of the major benefits of functional programming, that creating small, single purpose, general functions makes for highly reusable code, especially when these functions can dynamically create new functions through partial application as we did to create the month and year functions.

Step 2. HTML Tagger

Now that we have our date in the format that we desire we need a way to add some option tags around it. Sticking to our theme of writing modular, reusable code we will first define a general function for wrapping a string in HTML tags and then use currying to define a specific option tag wrapping function. Using the usual pattern we define our function htmlTagger as:

var htmlTagger = curry(function(tag, str) {
    return '' + str + '';
});

This function is pretty self explanatory, it takes a string and wraps it in HTML tags. For example, to wrap the string ‘Hello’ in ‘h1’ tags we can call the following:

htmlTagger('h1', 'Hello'));

As we defined htmlTagger to be a curryable function we can create a new function, optionTagger by calling htmlTagger with only the first argument (in this case ‘option’), this returns a function that accepts the remaining argument (the string we want to be wrapped) and wraps it in option tags:

var optionTagger = htmlTagger('option');

Step 3. Add ‘value’ Attribute

We almost have our complete option markup now, we only need to add a value attribute and set it equal to the original ‘yyyy-mm’ year-month string. Let us first define a general curryable function, htmlAddAttribute, for adding attribute-value pairs to HTML tags:

var htmlAddAttribute = curry(function(attr, val, str) {
    var regex = />/;
    if (str.match(regex)) {
        return str.replace(regex, ' ' + attr + '="' + val + '">');
    }
    return str;
});

Here, htmlAddAttribute takes three arguments, the attribute we want to add (attr), the value we want to set this attribute to (val) and the HTML string we want to add it to (str). It does some very, very simple string checking, if there is no ‘>’ it assumes there are no HTML tags and just returns the original string, otherwise it finds the first closing angle bracket and appends the attribute-value pair before it (and after anything else within the tag). To add ‘value=”2010″‘ to our HTML string we call htmlAddAttribute('value', '2010-10', optionTagger('October 2010')). We will be frequently using this function so let us define a more specific addValue function by calling htmlAddAttribute('value'):

var addValue = htmlAddAttribute('value');

Step 4. Full Option With Value

Now we have all the bits and pieces we need to create a full HTML option string with a value and the display date in the correct format. We could write it as follows:

var uglyDate = '2010-10';
var fullOption = function (uglyDate) {
    return addValue(uglyDate, optionTagger(pretyDate(uglyDate));
};

Or we could use function composition (see previous post) and remove a bit of the nesting and improve the readability:

var fullOption = function(uglyDate) {
    return compose(addValue(uglyDate), optionTagger, pretyDate)(uglyDate);
};

Here we can see that fullOption takes the raw year-month string and first converts the format with pretyDate, the result of this is then wrapped in option tags by passing it to optionTagger, we then pass the result to the function created by invoking addValue with only the first of its two arguments, the value we want to set the value attribute equal to which in this case is the original uglyDate month-date string.

Step 5. Create Options for Each Individual Month

With fullOption we now have a function for generating the complete markup for an individual year-month string. We want to apply this to each element in an array of year-month strings, rawMonths = ['2010-10', '2011-01', '2011-12', '2012-05'].

We do this by using the map function introduced in the previous post. map simply takes a function and an array as arguments and returns a new array of elements by applying the supplied function to each element in the original array.

If we call map with fullOption and rawMonths as arguments an array of individual HTML option strings for each month will be returned. However, we do not want an array of individual option strings, we want a single string of all the option strings in the array concatenated, we do this by defining the function array2str.

To create array2str we need to make use of the reduce function from the previous post. reduce takes a function, an initial accumulator value and an array as arguments and goes through the array value by value applying the supplied function to the current array element and the accumulator, the return value of this function then becomes the accumulator value used when the function is called with the next array element, when the final element of the original array has been acted on, the final value is the final accumulator value.

To concatenate an array of strings we can define array2str as follows:

var add = function(a, b) {
    return a + b;
};

var array2str = function(array) {
    return reduce(add, "", array);
};

In JavaScript + concatenates two strings so using the simple addition function and setting the initial accumulator value to the empty string, "", array2str will successively concatenate each string in array to the empty string.

Finally, to return our complete single string of HTML options we define the function dateOptions as:

var dateOptions = compose(array2str, map(fullOption));
console.log(dateOptions(rawDates));
// >> '<option value="2010-10">October 2010</option><option value="2011-01">January 2011</option><option value="2011-12">December 2011</option><option value="2012-05">May 2012</option>'

And if we call dateOptions(rawMonths) we get a single string of HTML options with each having a value attribute set to the original year-month string and the display text in a nice readable format. We can now go on and append this string to the relevant HTML select element.

Full Code

We can split the code used in this example into two parts, the application specific code which is used to generate the HTML options string for an array of year-month strings and a library of general functions (curry, map, htmlTagger, etc) which can be used in any other project.

Here, our application specific code comes down to the following set of functions which we build up using functions from our general functions library:

Application Specific Code

// raw data
var rawMonths = ['2010-10', '2011-01', '2011-12', '2012-05'];

// option tagger
var optionTagger = htmlTagger('option');

// year months
var month = splitN('-', 1);
var year = splitN('-', 0);

// need to make a nice date
var pretyDate = function(uglyDate) {
    return monthWord(month(uglyDate)) + " " + year(uglyDate);
};

// add value tag
var addValue = htmlAddAttribute('value');

// generate a full option with value and correctly formatted display date
var fullOption = function(uglyDate) {
    return compose(addValue(uglyDate), optionTagger, pretyDate)(uglyDate);
};

// create html string of options for each year-month string in an array.
var dateOptions = compose(array2str, map(fullOption));
console.log(dateOptions(rawMonths));

Library of General Functions

// curry: take any function and make it curryable
var curry = function (fn, fnLength) {
    fnLength = fnLength || fn.length;
    return function () {
        var suppliedArgs = Array.prototype.slice.call(arguments);
        if (suppliedArgs.length >= fn.length) {
            return fn.apply(this, suppliedArgs);
        } else if (!suppliedArgs.length) {
            return fn;
        } else {
            return curry(fn.bind.apply(fn, [this].concat(suppliedArgs)), fnLength - suppliedArgs.length);
        }
    };
};

// compose
var compose = function() {
    var funcs = arguments;
    return function() {
        var args = arguments;
        for (var i = funcs.length; i --> 0;) {
            args = [funcs[i].apply(this, args)];
        }
        return args[0];
    };
};

// reduce
var reduce = curry(function(func, init, xs) {
    return xs.reduce(func, init);
});

// map
var map = curry(function(func, xs){
    return xs.map(func);
});

// add
//+ add :: a -> b -> a | b
var add = curry(function(a, b) {
    return a + b;
});

// splitN
// return the nth substring of split about expr
//
//+ splitN :: expr -> int -> str -> str
var splitN = curry(function(expr, n, str) {
    return str.split(expr)[n];
});

// monthWord
// give a number, return the month as a word
//
//+ monthWord :: int -> str
var monthWord = (function(){
    var monthWords = ["January", "February", "March", "April",
                    "May", "June", "July", "August",
                    "September", "October", "November", "December"];

    return function(i) {
        return monthWords[parseInt(i, 10) - 1];
    };
}());

// array2str
// collapse an array down to a string
//
//+ array2str : [a] -> str
var array2str = function(xs) {
    return reduce(add, "", xs);
};

// htmlTagger
// wrap a string in html tags
//
//+ htmlTagger :: tag (str) -> str -> str
var htmlTagger = curry(function(tag, str) {
    return '' + str + '';
});

// htmlAddAttribute
// add an attribute="value" string to html element
//
//+ htmlAddAttribute :: str -> str -> str -> str
var htmlAddAttribute = curry(function(attr, val, str) {
    var regex = />/;
    if (str.match(regex)) {
        return str.replace(regex, ' ' + attr + '="' + val + '">');
    }
    return str;});

Summary

In this example we see how we can use a series of small, general, pure functions and combine them in specific ways to create more specific complex functions. The benefit in adopting this approach is that each small function has a specific purpose and as such they are very easy to test. This approach leads to highly modular code which is great for reusability. We can see that at no point do we store or pass solid values, the only solid value is the array of initial year-month strings, the final output string is obtained by passing and combining a series a functions which each have a known return value for a given input. We managed all of this without having to define loops or constantly update the value of variables to keep a track of the current state of the program (see this as reducing the number of exposed moving parts). This higher level of abstraction, in my opinion, results in much more readable code as rather than describing exactly how we want to do something, the code reads more as describing what we want to do.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s