Use Unobtrusive JavaScript

Technology oriented Frontend developer

Unobtrusive JavaScript is about catering for people whose browser lack JavaScript support, or if JavaScript fails. This means not making JavaScript a requirement for a functional application. It is also about avoiding unnecessary movements on a web page, unintuitive widget functionality and unfamiliar controls. This is an advantage to everyone, but especially benefits keyboard and screen-reader users. This section also describes some general good coding practises with other benefits besides accessibility.

WARNING: Note that Unobtrusive JavaScript is not necessarily accessible JavaScript and is no guarantee for keyboard and screen reader access.

The seven rules of unobtrusive JavaScript:

  • Do not make any assumptions
  • Find your hooks and relationships
  • Use CSS to traverse the DOM
  • Understand browsers and users
  • Understand events
  • Play well with others
  • Work for the next developer

Do not make any assumptions

Do not expect JavaScript to be available, and do not expect the intended mark-up to be there. Four things to keep in mind are:

  • Do not expect browsers to support certain methods and have the correct properties, but test for them before accessing them
  • Do not expect the correct HTML to be at your disposal, but check for it and do nothing when it is not available
  • Keep functionality independent of input device
  • Expect other scripts to try to interfere with the functionality and keep the scope of the scripts as secure as possible

Find your hooks and relationships

Before starting to plan a script:

  • Look at the HTML the script will be enhancing
  • Consider what is the best way of letting the script interact with it
  • Consider the hooks and relationships in the HTML

HTML hooks are:

  • Unique IDs (in valid HTML). Access them with the DOM method getElementById.
  • HTML elements which can be retrieved with getElementsByTagName and CSS classes.

Regarding HTML relationships ask the following questions:

  • How can I reach this element the easiest way and with the least steps traversing the DOM?
  • What elements do I have to alter to update all the child elements which should be changed?
  • What attributes does one element have that I can use to link to another element?

Use CSS to traverse the DOM

Using CSS to traverse the DOM is more effective, takes fewer resources and will not create any unnecessary dependency on  JavaScript.

In the example below, we perform a similar styling of images first with CSS and then with JavaScript.

1
2
<img class="css-border random-color-border" src="http://lorempixel.com/200/200/nature/1"/>
<img class="css-border" src="http://lorempixel.com/200/200/nature/2"/>
<img class="css-border random-color-border" src="http://lorempixel.com/200/200/nature/1"/>
<img class="css-border" src="http://lorempixel.com/200/200/nature/2"/>

1
2
3
4
5
6
7
.css-border{
    width: 200px;
    height: 200px;
    border:5px solid;
    border-radius:100px;
    border-bottom-left-radius:0;
}
.css-border{
    width: 200px;
    height: 200px;
    border:5px solid;
    border-radius:100px;
    border-bottom-left-radius:0;
}

1
2
3
4
5
6
7
8
(function(){
    var images = document.querySelectorAll('img');
    for (var i = 0; i < images.length; i++){
      images[i].style.border = '5px solid';
      images[i].style.borderBottomLeftRadius = 0;
      images[i].className='css-border';
    }
})();
4-5

Don't do this. Leave precise style details to the stylesheet

6

It is best practice to dynamically manipulate classes via the className property

(function(){
    var images = document.querySelectorAll('img');
    for (var i = 0; i < images.length; i++){
      images[i].style.border = '5px solid';
      images[i].style.borderBottomLeftRadius = 0;
      images[i].className='css-border';
    }
})();

In many cases, and where possible, it really is best practice to dynamically manipulate classes via the className property since the ultimate appearance of all of the styling hooks can be controlled in a single stylesheet. JavaScript code then also becomes cleaner since instead of being dedicated to styling details, it can focus on the overall semantics of each section it is creating or manipulating, leaving the precise style details to the stylesheet.

Read more about Using dynamic styling information.

Use JavaScript to enhance CSS

JavaScript can interact with stylesheets, allowing you to write programs that change a document's style dynamically.

There are three ways to do this:

  • By working with the document's list of stylesheets—for example: adding, removing or modifying a stylesheet.
  • By working with the rules in a stylesheet—for example: adding, removing or modifying a rule.
  • By working with an individual element in the DOM—modifying its style independently of the document's stylesheets

1
2
3
4
5
6
7
(function(){
    var images = document.querySelectorAll('.random-color-border');
    for (var i = 0; i < images.length; i++){
      var randomColor = ' #'+(~~(Math.random()*(1<<24))).toString(16);
      images[i].style.borderColor = randomColor;
    }
})();
4

Dynamism can be implemented with the support of scripting. This is a progressive enhancement of the styling, not affecting functionality.

(function(){
    var images = document.querySelectorAll('.random-color-border');
    for (var i = 0; i < images.length; i++){
      var randomColor = ' #'+(~~(Math.random()*(1<<24))).toString(16);
      images[i].style.borderColor = randomColor;
    }
})();

Play with this example of using JavaScript to enhance CSS in Codepen.

Understand users and browsers

You need to understand:

  • how browsers work
  • how browsers fail
  • what users expect to happen

Do not wander too far from the way browsers work and how users expect them to work. Consider the following:

  • Will the interface work independent of input device, and if not, what should be the fallback?
  • Is the interface following rules of the browser or the rules of the rich interface? Is it for example possible to navigate a multi level menu with cursors or is tabbing required? Are some keyboard shortcuts overlooked? Try to keep the conventional keyboard shortcuts.
  • What necessary functionality is dependent on JavaScript?

Understand Events

Event handling helps with separating the JavaScript from the HTML and CSS, and also goes a bit further.

  • The elements in the document are placed there to wait for handlers to listen to a change happening to them. When it happens the handlers retrieve an object (normally a parameter called e) that tells them what happened to what and what can be done with it.

Event handling does not only happen to the element you want to reach, but also to all the elements above it in the DOM hierarchy. (This does not apply to all events. Focus and blur do not do that.) This allows you to assign one single event handler to for example a navigation list and use the event handling's methods to reach the element in question. This technique is called event delegation and it has several benefits:

  • You only need to test if a single element exists, not each of them
  • It is possible to dynamically add or remove new child elements without having to remove or add new event handlers
  • It is possible to react to the same event on different elements

The event handling follows an event order when there are elements inside elements as such:

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html>
    <body>
        <h1>This example uses the addEventListener() to demonstrate event order</h1>
        <div id="myDiv">
            <button id="myBtn">Click me</button>
        </div>
    </body>
</html>
<!DOCTYPE html>
<html>
    <body>
        <h1>This example uses the addEventListener() to demonstrate event order</h1>
        <div id="myDiv">
            <button id="myBtn">Click me</button>
        </div>
    </body>
</html>

1
2
3
4
5
6
document.getElementById("myDiv").addEventListener("click", myFunction);
document.getElementById("myBtn").addEventListener("click", myFunction);

function myFunction() {
    document.getElementById("myDiv").insertAdjacentHTML('beforeend', '<p>You clicked ' + this + "</p>");
}
document.getElementById("myDiv").addEventListener("click", myFunction);
document.getElementById("myBtn").addEventListener("click", myFunction);

function myFunction() {
    document.getElementById("myDiv").insertAdjacentHTML('beforeend', '<p>You clicked ' + this + "</p>");
}

This will output:

You clicked [object HTMLButtonElement]
You clicked [object HTMLDivElement]

Play with this  example of event listeners in Codepen.

There are two different event order models:

Event capturing: the outer element event takes place first. This means it starts capturing events from the outer element.

Image illustrating Event capturing Figure 7: W3C event model - Event capturing

Event bubbling: the inner element event takes place first. It starts capturing events from the inner element.

Image illustrating Event bubbling

Figure 8: W3C event model - Event bubbling

Event capturing and event bubbling can be combined so that events are first captured until it reaches the target element and then bubbles up again.

Image illustrating  combination of event capturing and event bubbling

Figure 9: W3C event model - Combination of event capturing and event bubbling

Play well with others

There will hardly ever be only one script used in a document. Make sure your script does not interfere with others, and make your script difficult to interfere with.

  • The code should not have global function or variable names that others scripts can override
  • Instantiate every variable using the var keyword
    • Declared variables are constrained in the execution context in which they are declared. Undeclared variables are always global.
    • Declared variables are created before any code is executed. Undeclared variables do not exist until the code assigning to them is executed.
    • Declared variables are a non-configurable property of their execution context (function or global). Undeclared variables are configurable (e.g. can be deleted).

      Because of these three differences, failure to declare variables will very likely lead to unexpected results. Thus it is recommended to always declare variables, regardless of whether they are in a function or global scope.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var nav = document.querySelector('nav');

function init() {
  if (nav.classList.contains('nav-hide')) {
    showNavigation();
  }
}

function showNavigation() {
  if (nav.classList.contains('nav-hide')) {
    nav.classList.remove('nav-hide');
    nav.classList.add('nav-show');
  }
}

function hideNavigation() {
  if (nav.classList.contains("nav-show")) {
    nav.classList.remove("nav-show");
    nav.classList.add("nav-hide");
  }
}

init();
1

The script has a global variable called nav which can be accessed from all the functions init(), show() and reset(). The functions can access the global variable and each other by name.

4

HTML5’s classList functionality (IE10+) makes adding and removing classes easy. You can feature detect if the browser supports it by using  if ("classList" in document.documentElement)

var nav = document.querySelector('nav');

function init() {
  if (nav.classList.contains('nav-hide')) {
    showNavigation();
  }
}

function showNavigation() {
  if (nav.classList.contains('nav-hide')) {
    nav.classList.remove('nav-hide');
    nav.classList.add('nav-show');
  }
}

function hideNavigation() {
  if (nav.classList.contains("nav-show")) {
    nav.classList.remove("nav-show");
    nav.classList.add("nav-hide");
  }
}

init();

Play with this example of declaring variables in Codepen.

The object literal

  • Avoid all global code by wrapping the code in an object using the object literal. That way you turn the functions into methods and the variables into properties.
  • Define the methods and variables with a name followed by a colon and separate each of them from the others with a comma.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var myScript = {
    nav:document.querySelector('nav'),
    init:function(){
        myScript.showNavigation();
    },
    showNavigation:function(){
        if(myScript.nav.classList.contains('nav-hide')){
            myScript.nav.classList.remove('nav-hide');
            myScript.nav.classList.add('nav-show');
        }
    },
    hideNavigation:function(){
        if(myScript.nav.classList.contains('nav-show')){
            myScript.nav.classList.remove('nav-show');
            myScript.nav.classList.add('nav-hide');
        }
    }
}

myScript.init();
var myScript = {
    nav:document.querySelector('nav'),
    init:function(){
        myScript.showNavigation();
    },
    showNavigation:function(){
        if(myScript.nav.classList.contains('nav-hide')){
            myScript.nav.classList.remove('nav-hide');
            myScript.nav.classList.add('nav-show');
        }
    },
    hideNavigation:function(){
        if(myScript.nav.classList.contains('nav-show')){
            myScript.nav.classList.remove('nav-show');
            myScript.nav.classList.add('nav-hide');
        }
    }
}

myScript.init();

Play with this example of Object literal in Codepen.

These methods can be accessed from outside and inside the object by pre-pending the object name followed by a full stop. The drawback with this pattern is that the name of the object needs to be repeated every time it is accessed from another method. In addition, everything put inside the object is publicly accessible. 

Module pattern

If you want to only make parts of the script accessible to other script in the document it is possible use the module pattern:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var myScript = function() {
  // these are private
  var nav = document.querySelector('nav');
  var init = function() {
    myScript.showNavigation();
  }

  // these are public
  return {
    showNavigation: function() {
      if (nav.classList.contains('nav-hide')) {
        nav.classList.remove('nav-hide');
        nav.classList.add('nav-show');
      }
    },
    hideNavigation: function() {
      if (nav.classList.contains('nav-show')) {
        nav.classList.remove('nav-show');
        nav.classList.add('nav-hide');
      }
    },
    init: init
  }
}();

myScript.init();
5

Problem: to access one public method from another or from a private method you need to go through the verbose long name.

9

Public methods and properties wrapped in a return statement and using the object literal

var myScript = function() {
  // these are private
  var nav = document.querySelector('nav');
  var init = function() {
    myScript.showNavigation();
  }

  // these are public
  return {
    showNavigation: function() {
      if (nav.classList.contains('nav-hide')) {
        nav.classList.remove('nav-hide');
        nav.classList.add('nav-show');
      }
    },
    hideNavigation: function() {
      if (nav.classList.contains('nav-show')) {
        nav.classList.remove('nav-show');
        nav.classList.add('nav-hide');
      }
    },
    init: init
  }
}();

myScript.init();

Play with this example of Module patterns in Codepen.

You can access the public properties and methods that are returned the same way as in the object literal. The problem is that to access one public method from another or from a private method you need to go through the verbose long name again (the main object name can get rather long). 

Module pattern - return an object with synonyms

To avoid repeating long verbose names, define the methods as private and only return an object with synonyms.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var myScript = function() {
  // these are all private methods and properties
  var nav = document.querySelector('nav');

  var init = function() {
    showNavigation();
  }

  var showNavigation = function() {
    if (nav.classList.contains('nav-hide')) {
      nav.classList.remove('nav-hide');
      nav.classList.add('nav-show');
    }
  }

  var hideNavigation = function() {
    if (nav.classList.contains('nav-show')) {
      nav.classList.remove('nav-show');
      nav.classList.add('nav-hide');
    }
  }

  //return objects with synonyms
  return {
    showNavigation: showNavigation,
    hideNavigation: hideNavigation,
    init: init
  }
}();

myScript.init();
6

We can now access showNavigation with its short name.

24

return public pointers to the private methods and properties you want to reveal

var myScript = function() {
  // these are all private methods and properties
  var nav = document.querySelector('nav');

  var init = function() {
    showNavigation();
  }

  var showNavigation = function() {
    if (nav.classList.contains('nav-hide')) {
      nav.classList.remove('nav-hide');
      nav.classList.add('nav-show');
    }
  }

  var hideNavigation = function() {
    if (nav.classList.contains('nav-show')) {
      nav.classList.remove('nav-show');
      nav.classList.add('nav-hide');
    }
  }

  //return objects with synonyms
  return {
    showNavigation: showNavigation,
    hideNavigation: hideNavigation,
    init: init
  }
}();

myScript.init();

Play with this example of returning an object with synonyms in Codepen.

This allows for a consistency in coding style and gives the possibility to write shorter synonyms when they are revealed.

Anonymous function

To avoid revealing any methods or properties, it is possible to wrap the whole code block in an anonymous function and call it immediately after it was defined.

“The risk we are running by adding names to the global namespace, is that someone (or you) has already used that name elsewhere. This can result in a conflict. In the worst case you may break other code.” -lillylabs.no

In the example below we use an anonynous function to progressively enhance the page by creating a print button only if the browser supports it. Notice how defensive the script is, we don’t assume anything.

1
2
3
<p id="printThis">
  Thank you for your order. Please print this page for your records.
</p>
<p id="printThis">
  Thank you for your order. Please print this page for your records.
</p>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function() {
  if (document.getElementById) {
    var printThis = document.getElementById('printThis');
    if (printThis && typeof window.print === 'function') {
      var printButton = document.createElement('input');
      printButton.setAttribute('type', 'button');
      printButton.setAttribute('value', 'Print this now');
      printButton.onclick = function() {
        window.print();
      };
      printThis.appendChild(printButton);
    }
  }
})();
1,14

To avoid leaving any global variables behind, we wrap the whole functionality in an anonymous function and immediately execute it - this is done with (function(){})()

2-3

We test for DOM support and try to get the element we want to add the button to

4

We then test if the element exists and if the browser has a window object and a print method

5-10

we create a new click button and apply window.print() as the click event handler

11

The last step is to add the button to the paragraph.

(function() {
  if (document.getElementById) {
    var printThis = document.getElementById('printThis');
    if (printThis && typeof window.print === 'function') {
      var printButton = document.createElement('input');
      printButton.setAttribute('type', 'button');
      printButton.setAttribute('value', 'Print this now');
      printButton.onclick = function() {
        window.print();
      };
      printThis.appendChild(printButton);
    }
  }
})();

Play with this  example of Anonymous function in Codepen or read more about progressive enhancement vs graceful degradation on w3.org

Anonymous functions is great pattern for functionality that just needs to be executed once and has no dependency on other functions. This will make the code work well for the user and the machine it is running on as well as other developers.

Work for the next developer

Think about the developer who has to take over once this code is in production. Consider the following:

  • Are all the variable and function names logical and easy to understand?
  • Is the code logically structured? Is it possible to "read" it from top to bottom?
  • Are the dependencies obvious?
  • Are areas that might be confusing commented?

The HTML and CSS of a document is much more likely to change than the JavaScript as these make up visual output. It is therefore a good idea not to have any class and ID names or strings that will be shown to the end user buried somewhere in the code, but separate it out into a configuration object instead.

This way maintainers know exactly where to change these without having to alter the rest of your code.

1
2
.show{display:block}
.hide{display:none}
.show{display:block}
.hide{display:none}

1
2
3
4
5
<button onclick="myScript.showNavigation()">Show navigation</button>
<button onclick="myScript.hideNavigation()">Hide navigation</button>
<nav id="nav" class="show" >
    Page navigation
</nav>
<button onclick="myScript.showNavigation()">Show navigation</button>
<button onclick="myScript.hideNavigation()">Hide navigation</button>
<nav id="nav" class="show" >
    Page navigation
</nav>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var myScript = function() {
  var config = {
    navigationID: 'nav',
    visibleClass: 'show',
    invisibleClass: 'hide'
  };

  var nav = document.getElementById(config.navigationID);

  var showNavigation = function() {
    nav.classList.add(config.visibleClass);
    nav.classList.remove(config.invisibleClass);
  }
  var hideNavigation = function() {
    nav.classList.remove(config.visibleClass);
    nav.classList.add(config.invisibleClass);
  }
  return {
    showNavigation: showNavigation,
    hideNavigation: hideNavigation
  }
}();
2

We define a configuration object that contain all hardcoded references to the classes used in our html, instead of having references to css classes spread all around out javascript code. 

var myScript = function() {
  var config = {
    navigationID: 'nav',
    visibleClass: 'show',
    invisibleClass: 'hide'
  };

  var nav = document.getElementById(config.navigationID);

  var showNavigation = function() {
    nav.classList.add(config.visibleClass);
    nav.classList.remove(config.invisibleClass);
  }
  var hideNavigation = function() {
    nav.classList.remove(config.visibleClass);
    nav.classList.add(config.invisibleClass);
  }
  return {
    showNavigation: showNavigation,
    hideNavigation: hideNavigation
  }
}();

Play with this example of Configuration object in Codepen.