Book Image

HTML5 Data and Services Cookbook

Book Image

HTML5 Data and Services Cookbook

Overview of this book

HTML5 is everywhere. From PCs to tablets to smartphones and even TVs, the web is the most ubiquitous application platform and information medium bar. Its becoming a first class citizen in established operating systems such as Microsoft Windows 8 as well as the primary platform of new operating systems such as Google Chrome OS. "HTML5 Data and Services Cookbook" contains over 100 recipes explaining how to utilize modern features and techniques when building websites or web applications. This book will help you to explore the full power of HTML5 - from number rounding to advanced graphics to real-time data binding. "HTML5 Data and Services Cookbook" starts with the display of text and related data. Then you will be guided through graphs and animated visualizations followed by input and input controls. Data serialization, validation and communication with the server as well as modern frameworks with advanced features like automatic data binding and server communication will also be covered in detail.This book covers a fast track into new libraries and features that are part of HTML5!
Table of Contents (21 chapters)
HTML5 Data and Services Cookbook
Credits
About the Authors
About the Reviewers
www.PacktPub.com
Preface
Index

Displaying metric and imperial measurements


Websites that deal with calculations and measurements often need to solve the problem of using both metric and imperial units of measurement. This recipe will demonstrate a data-driven approach to dealing with unit conversions. As this is an HTML5 book, the solution will be implemented on the client side rather than on the server side.

We're going to implement a client-side, "ideal weight" calculator that supports metric and imperial measurements. This time, we're going to create a more general and elegant data-driven solution that utilizes modern HTML5 capabilities, such as data attributes. The goal is to abstract away the messy and error-prone conversion as much as possible.

Getting ready

The formula for calculating the body mass index (BMI) is as follows:

BMI = (Weight in kilograms / (height in meters x height in meters))

We're going to use BMI = 22 to calculate the "ideal weight".

How to do it...

  1. Create the following HTML page:

    <!DOCTYPE HTML>
    <html>
        <head>
            <title>BMI Units</title>
        </head>
        <body>
            <label>Unit system</label>
            <select id="unit">
                <option selected value="height=m,cm 0;weight=kg 1;distance=km 1">Metric</option>
                <option value="height=ft,inch 0;weight=lbs 0;distance=mi 1">Imperial</option>
            </select><br>
    
            <label>Height</label>
            <span data-measurement="height" id="height">
                <input data-value-display type="text" id="height" class="calc">
                <span data-unit-display></span>
                <input data-value-display type="text" id="height" class="calc">
                <span data-unit-display></span>
            </span>
            <br>
            <label>Ideal Weight</label>
            <span data-measurement="weight" id="weight">
                <span data-value-display type="text">0</span>
                <span data-unit-display></span>
            </span> <br>
            
            <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
            <script type="text/javascript" src="unitval.js"></script>
            <script type="text/javascript" src="example.js"></script>
            </script>
        </body>
    </html>

    This page looks very much like the regular page we would make for a BMI-based ideal weight calculator. The main differences are as follows:

    • We have an imperial/metric selection input

    • We also have additional custom data attributes to give special meanings to the HTML fields

    • We use data-measurement to denote the kind of measurement that the element will display (for example, either the weight or the height)

    • We use data-display-unit and data-display-value to denote fields that display unit strings and values of the measurement respectively

  2. Create a file named example.js with the following code:

    (function() {
        // Setup unitval
        $.unitval({
            weight: {
                "lbs": 0.453592, // kg
                "kg" : 1 // kg
            },
            height: {
                "ft"  : 0.3048, // m
                "inch": 0.0254, // m
                "m"   : 1, // m
                "cm"  : 0.01, // m
            }
        });
        $("#unit").change(function() {
            var measurementUnits = $(this).val().split(';').map(function(u) {
                var type_unitround = u.split('='),
                    unitround = type_unitround[1].split(' ');
                return {
                    type: type_unitround[0],
                    units: unitround[0].split(','),
                    round: unitround[1]
                };
            });
            // Setup units for measurements.
            $('body').unitval(measurementUnits);
        });
    
        $("#unit").trigger("change");
    
        $('#height').on('keyup change',function() {
            var height = $('#height').unitval(), bmi = 22;
            var idealWeight = bmi * height * height;
            $("#weight").unitval(idealWeight);
        });
    
    }

    The first part of the code configures a jQuery plugin called unitval, with the conversion factors for the measurements and units that we are going to use (weight and height).

    The second part sets the measurement units for the document by reading the specification from the select field. It specifies an array of measurements, each having the following:

    • A type string, for example "height"

    • A list of units, for example ["ft", "inch"]

    • The number of decimals to use for the last unit

    The third part is a regular calculator that is written almost exactly as it would be written if there were no unit conversions. The only exception is that values are taken from the elements that have the data-measurement attribute using the jQuery plugin named $.unitval.

  3. We're going to write a generic unit converter. It will need two functions: one that will convert user-displayed (input) data to standard international (SI) measurement units, and another to convert it back from SI units to user-friendly display units. Our converter will support using multiple units at the same time. When converting from input, the first argument is the measurement type (for example, distance), the second is an array of value-unit pairs (for example, [[5, 'km'], [300,'m']]), a single pair (for example [5,'km']), or simply the value (for example 5).

  4. If the second parameter is a simple value, we're going to accept a third one containing the unit (for example 'km'). The output is always a simple SI value.

    When converting a value to the desired output units, we specify the units as an array, for example, as either ['km', 'm'] or as a single unit. We also specify rounding decimals for the last unit. Our output is an array of converted values.

    Conversion is done using the values in the Factors object. This object contains a property for every measurement name that we're going to use. Each such property is an object with the available units for that measurement as properties, and their SI factors as values. Look in the following in example.js for an example.

  5. The source code of the jQuery plugin, unitval.js, is as follows:

    (function() {
        var Factors = {};
        var Convert = window.Convert = {
            fromInput: function(measurement, valunits, unit) {
                valunits = unit ? [[valunits, unit]] // 3 arguments
                    : valunits instanceof Array && valunits[0] instanceof Array ? valunits  
                    : [valunits]; // [val, unit] array
                
                var sivalues = valunits.map(function(valunit) { // convert each to SI
                    return valunit[0] * Factors[measurement][valunit[1]];
                });
                // sivalues.sum():
                return sivalues.reduce(function(a, e) { return a + e; });
            },
            toOutput: function(measurement, val, units, round) {
                units = units instanceof Array ? units : [units];
                var reduced = val;
                return units.map(function(unit, index) {
                    var isLast = index == units.length - 1,
                        factor = Factors[measurement][unit];
                    var showValue = reduced / factor;
                    if (isLast && (typeof(round) != 'undefined'))
                        showValue = showValue.toFixed(round) - 0;
                    else if (!isLast) showValue = Math.floor(showValue);
                    reduced -= showValue * factor;
                    return showValue;
                });
            }
        };
        $.unitval = function(fac) {
            Factors = fac;
        }
        // Uses .val() in input/textarea and .text() in other fields.
        var uval = function() {
            return ['input','textarea'].indexOf(this[0].tagName.toLowerCase()) < 0 ?
                    this.text.apply(this, arguments) : this.val.apply(this, arguments);
        }
  6. Our generic convertor is useful, but not very convenient or user friendly; we still have to do all the conversions manually. To avoid this, we're going to put data attributes on our elements, denoting the measurements that they display. Inside them, we're going to put separate elements for displaying the value(s) and unit(s). When we set the measurement units, the function setMeasurementUnits will set them on every element that has this data attribute. Furthermore, it will also adjust the inner value and unit elements accordingly:

    // Sets the measurement units within a specific element.
    // @param measurements An array in the format [{type:"measurement", units: ["unit", ...], round:N}]
    // for example [{type:"height", units:["ft","inch"], round:0}]
        var setMeasurementUnits = function(measurements) {
            var $this = this;
            measurements.forEach(function(measurement) {
                var holders = $this.find('[data-measurement="'+measurement.type+'"]');
                var unconverted = holders.map(function() { return $(this).unitval(); })
                holders.attr('data-round', measurement.round);
                holders.find('[data-value-display]').each(function(index) {
                    if (index < measurement.units.length)    
                        $(this).show().attr('data-unit', measurement.units[index]);
                    else $(this).hide();
                });
                holders.find('[data-unit-display]').each(function(index) {
                    if (index < measurement.units.length)    
                        $(this).show().html(measurement.units[index]);
                    else $(this).hide();
                });
    
                holders.each(function(index) { $(this).unitval(unconverted[index]); });
            });
        };
  7. As every element knows its measurement and units, we can now simply put SI values inside them and have them display converted values. To do this, we'll write unitval. It allows us to set and get "united" values, or set unit options on elements that have the data-measurement property:

        $.fn.unitval = function(value) {
            if (value instanceof Array) {
                setMeasurementUnits.apply(this, arguments);
            }
            else if (typeof(value) == 'undefined') {
                // Read value from element
                var first       = this.eq(0),
                    measurement = first.attr('data-measurement'),
                    displays    = first.find('[data-value-display]:visible'),
                    // Get units of visible holders.
                    valunits = displays.toArray().map(function(el) {
                        return [uval.call($(el)), $(el).attr('data-unit')] });
                // Convert them from input
                return Convert.fromInput(measurement, valunits);
            }
            else if (!isNaN(value)) {
                // Write value to elements
                this.each(function() {
                    var measurement   = $(this).attr('data-measurement'),
                        round         = $(this).attr('data-round'),
                        displays      = $(this).find('[data-value-display]:visible'),
                        units         = displays.map(function() {
                            return $(this).attr('data-unit'); }).toArray();
      var values = Convert.toOutput(measurement, value, units, round);
                    displays.each(function(index) { uval.call($(this), values[index]); });
                });
            }
        }
    }());

This plugin will be explained in the next section.

How it works...

HTML elements have no notion of measurement units. To support unit conversion, we added our own data attributes. These allow us to give a special meaning to certain elements—the specifics of which are then decided by our own code.

Our convention is that an element with a data-measurement attribute will be used to display values and units for the specified measurement. For example, a field with the data-measurement="weight" attribute will be used to display weight.

This element contains two types of subelements. The first type has a data-display-value attribute, and displays the value of the measurement (always a number). The second type has a data-display-unit attribute, and displays the unit of the measurement (for example, "kg"). For measurements expressed in multiple units (for example, height can be expressed in the form of "5 ft 3 inch"), we can use multiple fields of both types.

When we change our unit system, setMeasurementUnits adds additional data attributes to the following elements:

  • data-round attributes are attached to data-measurement elements

  • data-unit attributes containing the appropriate unit is added to the data-display-value elements

  • data-display-unit elements are filled with the appropriate units

As a result, $.unitval() knows both the values and units displayed on every measurement element on our page. The function reads and converts the measurement to SI before returning it. We do all our calculations using the SI units. Finally, when calling $.unitval(si_value), our value is automatically converted to the appropriate units before display.

This system minimizes the amount of error-prone unit conversion code by recognizing that conversions are only really needed when reading user input and displaying output. Additionally, the data-driven approach allows us to omit conversions entirely from our code and focus on our application logic.