Click here to Skip to main content
15,903,175 members
Articles / Web Development / HTML

Let's Write Unobtrusive JavaScript

Rate me:
Please Sign up or sign in to vote.
4.77/5 (6 votes)
4 Mar 2015CPOL9 min read 26.8K   78   8  
An article about different ways to write unobtrusive JavaScript

Introduction

Unobtrusive JavaScript is the way of writing JavaScript language in which we properly separate Document Content and Script Content thus allowing us to make a clear distinction between them. This approach is useful in so many ways as it makes our code less error prone, easy to update and to debug.

Target Audience

This article is intended for those who have intermediate to advanced level of understanding in writing JavaScript code. There are some APIs and features that might be new for some developers and I have tried to describe them separately from the code.

The Concept

The basic concept of unobtrusive programming is that JavaScript should be used as an enhancement to our web page instead of an absolute requirement. If you do not need JavaScript, then don't use it; your static content will display just fine with plain old HTML and CSS. So many developers make the mistake of importing code libraries before even figuring out if they will be even required. jQuery for example has been so much mis-used when all our basic needs can be easily fulfilled by CSS and pure JavaScript code.

If your application is dynamic and needs to call server code asynchronously, update HTML content and do all sorts of event binding, then by all means, do use JavaScript, and libraries like jQuery, etc. but try to code them in a separate layer systematically so that the HTML content stays clean and semantically separated.

Following are the areas in web application development which have a clear positive impact when we write JavaScript unobtrusively:

Code Separation

JavaScript code is not embedded into HTML, thus making it easy to update or upgrade the code.

Stability

Unobtrusive JavaScript generates less error messages. The web page will always display all the content from the static markup and CSS even if the script fails to run.

Graceful Degradation and Progressive Enhancement

If any feature is not supported by the browser, then the code should silently turn off that feature instead of throwing an error message. Similarly, if more advanced versions of a feature are available, the code should use the upgraded features to give the user an experience as per the browser advancement.

Cleaner Global Scope

The global scope or the window scope will stay clean of unnecessary or unwanted objects, properties, functions and variables. The unobtrusive way is to have a single point of entry into the application's data using namespaces.

Using the Code

This article is focused less on the theoretical side of unobtrusive JavaScript programming and paying more attention to the practical side; areas of code where it applies the most. In the next section, there are code snippets which will show the Obtrusive Way and the Unobtrusive Way of writing code. The core idea is to separate JavaScript from the HTML and later to extend this approach when altering the DOM from the JavaScript.

Before we start comparing the Good code and the Bad code, I would like to make this very clear that this approach should not be pushed too far in use so as to make the life of other developers very difficult in doing changes to the codebase. In the end, we all agree that errors and bugs arise and anyone should be able to fix them in the least possible time; our unobtrusive code should not come in the way of doing that.

Let's start looking at the code snippets which are used commonly where we can apply the unobtrusive coding practice.

HTML Anchors

It is a very common practice to wire-up the anchor's click event in its HTML markup declaration. This can pose many problems; like difficulty in debugging the code and moreover we will need to modify the anchor tag if we need to change the JavaScript code for any reason.

HTML
<body>
    Search Engines
    <br />
    <a href="http://www.google.com" onclick="window.open(this.href);return false;">Google</a> &nbsp;
    <a href="http://www.search.yahoo.com" onclick="window.open(this.href);return false;">Yahoo Search
    </a> &nbsp;
    <a href="http://www.bing.com" onclick="window.open(this.href);return false;">Bing</a> &nbsp;
</body>

The solution is to go unobtrusive here by separating HTML and Script code.

Unobtrusive Way
HTML
<body>
    Search Engines
    <br />
    <a href="http://www.google.com">Google</a> &nbsp;
    <a href="http://www.search.yahoo.com">Yahoo Search</a> &nbsp;
    <a href="http://www.bing.com">Bing</a> &nbsp;
    <script>
        document.addEventListener('DOMContentLoaded', function () {
            var anchors = document.getElementsByTagName('a');
            WireupAnchors.bind(null, anchors)();
        });

        function WireupAnchors(anchors) {
            anchors = anchors || []; //If anchors is null or undefined then use empty array

            for (var i = 0; i < anchors.length; i++) {
                iterator.call(anchors[i]);
            }

            function iterator() {
                this.onclick = function () {
                    window.open(this.href);
                    return false;
                }
            }
        }        
    </script>
</body>

In the above code, DOMContentLoaded event is fired when the document content is parsed and all the HTML has been loaded. This does not include stylesheets, external CSS and image files.

When the DOM is ready, then WireupAnchors will get called. The anchors are being passed as an array to WireupAnchors and for each item iterator will be called to bind the click event of the anchor with an anonymous function to open a new window. The href attributes's value is used as the URL.

Event Binding

The following code is more or less same as the above, the click event binding is done inline of the button declaration.

HTML
<body>    
    <input type="button" value="Button1" onclick="button1_Click(); return false;" />
    <input type="button" value="Button2" onclick="button2_Click(); return false;"/>

    <script>
        function button1_Click() {
            alert('Button1 was Clicked!');
        }

        function button2_Click() {
            alert('Button2 was Clicked!');
        }
    </script>
</body>
Unobtrusive Way

Instead of using inline code, there are specific IDs assigned to the buttons and event binding is done through separate JavaScript code which is not mixed up with the HTML.

HTML
<body>    
    <input id="button1" type="button" value="Button1" />
    <input id="button2" type="button" value="Button2"/>
    <script>
        document.addEventListener('DOMContentLoaded', WireUpEvents);

        function WireUpEvents() {
            var button1 = document.getElementById('button1'),
                button2 = document.getElementById('button2');

            button1.addEventListener('click', button1_Click);
            button2.addEventListener('click', button2_Click);
        }

        function button1_Click() {
            alert('Button1 was Clicked!');
            return false;
        }

        function button2_Click() {
            alert('Button2 was Clicked!');
        }
    </script>
</body>

In the above code, addEventListener is simply being used to bind the click event of the buttons

Setting CSS Properties

Whenever we need to set the HTML styling for any control using JavaScript, then our first thought of action is to set specific CSS property values. While there is no direct threat to the well being of our web application by doing this, the problem arises when we have to update the code. We may need to find all the occurences of the lines of code that need to be changed and that is a painfully time consuming process.

HTML
<body>    
    <div id="myDiv"></div>
    <script>
        
        document.addEventListener('DOMContentLoaded', onLoad);

        function onLoad() {
            var myDiv = document.getElementById('myDiv');

            myDiv.style.width = '100px';
            myDiv.style.height = '100px';
            myDiv.style.borderRadius = '5px';
            myDiv.style.borderColor = 'black';
            myDiv.style.borderWidth = '2px';
            myDiv.style.borderStyle = 'solid';
            myDiv.style.backgroundColor = 'green';
            myDiv.style.display = 'block';
        }
    </script>
</body>
Unobtrusive Way

The solution to the above problem is to create specific CSS classes and then update the element's CSS class value directly instead of setting specific CSS properties. This approach will make code updates much easier and the code-base will be less prone to bugs.

HTML
<head>
    <title></title>
    <style type="text/css">
        .GreenBox {
            width: 100px;
            height: 100px;
            border-radius: 5px;
            border: 2px solid black;
            display: block;
            background-color: green;        
        }
    </style>
</head>
<body>    
    <div id="myDiv"></div>
    <script>
        document.addEventListener('DOMContentLoaded', onLoad);

        function onLoad() {
            var myDiv = document.getElementById('myDiv');
            myDiv.className = 'GreenBox';
        }
    </script>
</body>

GreenBox is a CSS class in the <style> tag which contains all the property values which were individually being set through the JavaScript code in the previous code snippet. In this code, we are simply setting the class name of the element.

Use of HTML Attributes

The following code is for setting a red border around the text-box if its empty, to show the user that the field is required. In the following code, there is a function RequiredFieldValidation which is used commonly for all the fields. But the problem is that the change event is being wired-up for all the individual fields which makes our code unnecessarily long.

HTML
<head>
    <title></title>
    <style type ="text/css">
        .RedBox{
            padding:2px;
            border:2px solid red;
            border-radius:5px;
        }
        .BlackBox{
            padding:2px;
            border:2px solid black;
            border-radius:5px;
        }
    </style>    
</head>
<body>    
    <input id="txtFistName" type="text" placeholder="First Name" class="RedBox" /> &nbsp; 
    <input id="txtLastName" type="text" placeholder="Last Name" class="RedBox" />
    <br /><br />
    <input id="txtEmail" type="text" placeholder="Email Id" class="RedBox" />
    <br /><br />
    <input id="txtPhone" type="text" placeholder="Phone" class="RedBox" />
    <script>
        document.addEventListener('DOMContentLoaded', onLoad);

        function onLoad() {
            var txtFirstName = document.getElementById('txtFistName'),
                txtLastName = document.getElementById('txtLastName'),
                txtEmail = document.getElementById('txtEmail'),
                txtPhone = document.getElementById('txtPhone');

            txtFistName.addEventListener('change', RequiredFieldValidation.bind(txtFistName));
            txtLastName.addEventListener('change', RequiredFieldValidation.bind(txtLastName));
            txtEmail.addEventListener('change', RequiredFieldValidation.bind(txtEmail));
            txtPhone.addEventListener('change', RequiredFieldValidation.bind(txtPhone));
        }

        function RequiredFieldValidation() {
            this.className = (this.value.length === 0) ? 'RedBox' : 'BlackBox';
        }
    </script>
</body>
Unobtrusive Way

The above code can be improved by adding a common attribute to all the inputs which are needed to be validated by following same set of rules. Following code does that by having Required attribute for the text inputs. In the following code we are binding RequiredFieldValidation function with the change event of all the text-fields having the Required attribute.

HTML
<head>
    <title></title>

    <style type ="text/css">
        .RedBox{
            padding:2px;
            border:2px solid red;
            border-radius:5px;
        }
        .BlackBox{
            padding:2px;
            border:2px solid black;
            border-radius:5px;
        }
    </style>    
</head>
<body>    
    <input id="txtFistName" type="text" placeholder="First Name" class="RedBox" Required/> &nbsp; 
    <input id="txtLastName" type="text" placeholder="Last Name" class="RedBox" Required/>
    <br /><br />
    <input id="txtEmail" type="text" placeholder="Email Id" class="RedBox" Required/>
    <br /><br />
    <input id="txtPhone" type="text" placeholder="Phone" class="RedBox" Required/>
    <script>
        document.addEventListener('DOMContentLoaded', onLoad);

        function onLoad() {
            var required = document.querySelectorAll('[Required]');

            for (var i = 0; i < required.length; i++) {
                required[i].addEventListener('change', RequiredFieldValidation.bind(required[i]));
            }
        }

        function RequiredFieldValidation() {
            this.className = (this.value.length === 0) ? 'RedBox' : 'BlackBox';
        }
    </script>
</body>

The browsers which support Html5 provide an error message for the fields with required attribute if the field is blank when the form is submitted. For more information on this, you can refer to the following link:

In the above code, required attribute is being extended to visually highlight the field with a red border. The code is mostly self explanatory, querySelectorAll is being used to find all the elements with the required attribute. For each element found, the code is binding the change event with the RequiredFieldValidation function.

Namespaces

The next code is for a small calculator application. You can easily notice that every variable and function is just lying there in the Global Scope. This approach is okay if we do not have external libraries imported into the code, but we know this rarely is the case in any decent web application. We have all kinds of external APIs to help us make better user interface and to write better business logic.

So we cannot have any little variable, property, function or object polluting the global scope because it might be overwriting some existing code implementation.

HTML
<body>

    <input type="text" id="txtNumber1" /> &nbsp; <input type="text" id="txtNumber2" />
    <br /><br />

    <input type="button" value="Add" add/> &nbsp;
    <input type="button" value="Subtract" subtract/> &nbsp;
    <input type="button" value="Multiply" multiply/> &nbsp;
    <input type="button" value="Divide" divide/> &nbsp;

    <br /><br />
    <input type="text" id="txtResult" />
    
    <script>
        var txtNumber1, txtNumber2, txtResult;
        document.addEventListener('DOMContentLoaded', onLoad);

        function onLoad() {
            txtNumber1 = document.getElementById('txtNumber1');
            txtNumber2 = document.getElementById('txtNumber2');
            txtResult = document.getElementById('txtResult');

            document.querySelector('[add]').addEventListener('click', add_click);
            document.querySelector('[subtract]').addEventListener('click', subtract_click);
            document.querySelector('[multiply]').addEventListener('click', multiply_click);
            document.querySelector('[divide]').addEventListener('click', divide_click);
        }

        function calculate(number1, number2, operationType) {
            var result = 0;
            switch (operationType.toLowerCase()) {
                case 'add':
                    result = number1 + number2;
                    break;
                case 'subtract':
                    result = number1 - number2;
                    break;
                case 'multiply':
                    result = number1 * number2;
                    break;
                case 'divide':
                    result = number1 / number2;
                    break;
            }
            return result;
        }

        function add_click() {
            txtResult.value = calculate(parseFloat(txtNumber1.value)
                                    , parseFloat(txtNumber2.value), 'Add');
        }

        function subtract_click() {
            txtResult.value = calculate(parseFloat(txtNumber1.value)
                                    , parseFloat(txtNumber2.value), 'Subtract');
        }

        function multiply_click() {
            txtResult.value = calculate(parseFloat(txtNumber1.value)
                                    , parseFloat(txtNumber2.value), 'Multiply');
        }

        function divide_click() {
            txtResult.value = calculate(parseFloat(txtNumber1.value)
                                    , parseFloat(txtNumber2.value), 'Divide');
        }
    </script>
</body>
Unobtrusive Way

The above problem has a very simple solution and that is to use namepsaces. A namespace in JavaScript is implemented using objects, and they provide us a single point of entry into our application's different modules. This approach eliminates the possibility of global scope pollution and its resulting side effects.

In the following code, App object is being treated as the root namespace. Properties like Operations, Inputs, Events etc are sub-namespaces which contain information specific to them.

HTML
<body>
    <input type="text" id="txtNumber1" /> &nbsp;
    <input type="text" id="txtNumber2" /> <br /><br />
    <input type="button" value="Add" add /> &nbsp;
    <input type="button" value="Subtract" subtract /> &nbsp;
    <input type="button" value="Multiply" multiply /> &nbsp;
    <input type="button" value="Divide" divide /> &nbsp;
    <br /><br />
    <input type="text" id="txtResult" />
    <script>
        var App = { 'Operations': {}, 'Inputs': {}, 'Outputs': {}, 'Commands': {}, 'Events': {} };

        App.Events = {

            'Add': function () {
                App.Outputs.Result.value = App.Operations.Calculate(
                          parseFloat(App.Inputs.Number1.value)
                        , parseFloat(App.Inputs.Number2.value), 'Add');
            },

            'Subtract': function () {
                App.Outputs.Result.value = App.Operations.Calculate(
                          parseFloat(App.Inputs.Number1.value)
                        , parseFloat(App.Inputs.Number2.value), 'Subtract');
            },

            'Multiply': function () {
                App.Outputs.Result.value = App.Operations.Calculate(
                          parseFloat(App.Inputs.Number1.value)
                        , parseFloat(App.Inputs.Number2.value), 'Multiply');
            },

            'Divide': function () {
                App.Outputs.Result.value = App.Operations.Calculate(
                          parseFloat(App.Inputs.Number1.value)
                        , parseFloat(App.Inputs.Number2.value), 'Divide');
            },

            'Load': function () {

                App.Inputs = {
                    'Number1': document.getElementById('txtNumber1'),
                    'Number2': document.getElementById('txtNumber2'),
                };

                App.Outputs = {
                    'Result': document.getElementById('txtResult')
                };

                App.Commands = {
                    'Add': document.querySelector('[add]'),
                    'Subtract': document.querySelector('[subtract]'),
                    'Multiply': document.querySelector('[multiply]'),
                    'Divide': document.querySelector('[divide]')
                };
                
                App.Commands.Add.addEventListener('click', App.Events.Add);
                App.Commands.Subtract.addEventListener('click', App.Events.Subtract);
                App.Commands.Multiply.addEventListener('click', App.Events.Multiply);
                App.Commands.Divide.addEventListener('click', App.Events.Divide);
            }
        };
        App.Operations.Calculate = function (number1, number2, operationType) {
            var result = 0;
            switch (operationType.toLowerCase()) {
                case 'add':
                    result = number1 + number2;
                    break;
                case 'subtract':
                    result = number1 - number2;
                    break;
                case 'multiply':
                    result = number1 * number2;
                    break;
                case 'divide':
                    result = number1 / number2;
                    break;
            }
            return result;
        };
        document.addEventListener('DOMContentLoaded', App.Events.Load);
    </script>
</body>

The best thing about the above code snippet is that it is neatly organized into relevant sections of the namespace and each piece of information has its own namespace address.

Graceful Degradation

Graceful degradation simply means to silently fall back to the primitive version of a functionality if a particular feature is not supported or if the JavaScript is not available for use. The following code demonstrates the use of <noscript> tag. The contents of this tag will be added to the web page if JavaScript is disabled.

HTML
<body>

    <noscript>
        JavaScript seems to be disabled!
        <br />
    </noscript>

    <a href="www.codeproject.com">CodeProject</a>

</body>

Progressive Enhancement

Progressive enhancement is the art enhancing the user experience or code efficiency by utilizing most recent feature available. The best example can be seen in rounded corners which were previously unavailable to CSS and were hard to implement as a custom solution. Other examples can be seen in the new APIs that are available in the modern browsers.

In the following code example, click event is being wired-up by creating a delegate of button_click function. For this purpose, Function.prototype.bind is being used, but what happens if our code is running on an older browser which does not support bind(). To handle this scenario, we need to add our custom bind API to mimic the modern browsers and to make sure that our application will run, no matter which browser is used.

HTML
<body>
    <input id="button1" type="button" value="Hello" />
    <script>
        var button1,
            messages = {'Message1': 'Hello World'};

        document.addEventListener('DOMContentLoaded', onLoad);

        function onLoad(){
            button1 = document.getElementById('button1');
            button1.addEventListener('click', button_click.bind(messages));                
        }

        function button_click() {
            alert(this.Message1);
        }

        //If Function.prototype.bind is not available then create a custom bind function.
        //Polyfill: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects
          /Function/bind
        if (!Function.prototype.bind) {
            Function.prototype.bind = function (oThis) {
                if (typeof this !== 'function') {
                    // closest thing possible to the ECMAScript 5
                    // internal IsCallable function
                    throw new TypeError('Function.prototype.bind - what is trying to be bound is not 
                                          callable');
                }

                var aArgs = Array.prototype.slice.call(arguments, 1),
                    fToBind = this,
                    fNOP = function () { },
                    fBound = function () {
                        return fToBind.apply(this instanceof fNOP && oThis
                               ? this
                               : oThis,
                               aArgs.concat(Array.prototype.slice.call(arguments)));
                    };

                fNOP.prototype = this.prototype;
                fBound.prototype = new fNOP();

                return fBound;
            };
        }
    </script>
</body>

There can be so many other places where unobtrusive JavaScript programming can be applied. We should always be on the lookout for such areas where we can write code differently so as to improve the codebase and make it less prone to errors and bugs.

Final Words

The decision to follow unobtrusive coding practice depends on developer preference, but it never hurts to create our web application by following defensive measures. When our code runs on the internet, there can be a thousand ways for things to go wrong; such as an unidentified device, unsupported APIs, unwanted dynamic scripts etc. So it's always a good idea to follow certain standard coding practices, like the ones in this article to make our lives a little more easier to live.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
India India
Just a regular guy interesting in programming, gaming and a lot of other stuff Smile | :)

Please take a moment to visit my YouTube Channel and subscribe to it if you like its contents!
My YouTube Channel

Don't be a stranger! Say Hi!!

Cheers!

Comments and Discussions

 
-- There are no messages in this forum --