JavaScript components low basics

28 June 2013

This article features common problems that developers often face, or maybe used to face before the current open source era, when writing JavaScript for the client side; showcase of possible solutions to these problems and explanations of my choices.

Client-side magic

I am going to write about client-side JavaScript code only which runs under a browser and empowers an interface with interactivity.

A piece of interface is represented with some HTML (and obviously styled with CSS). Also it can be equiped with JavaScript usually describing how to react to user's actions.

So, when developing a piece of interface, we write:

  1. HTML/CSS
  2. JavaScript
  3. Some code to link 2 to 1.
    (This code says which part of HTML on a page corresponds to a JavaScript component.)

A few ways to matchmake

Thus, the so-called 'linking' joins designated piece of JavaScript to its corresponding piece of HTML. There are different ways around this.

Linking with event attributes. The old school way.

Since, in most cases running JavaScript is a reaction to some events firing, there is an obvious way to declare in HTML what the reaction should be. These are the intrinsic event attributes.

For example, if you need a hidden <div> with an authorization form to be shown when a user clicks on a 'log in' link you can write a JavaScript function for that. Then, call it in with an onclick attribute.

<a href="/login-page" onclick="return showLogin()">log in</a>
<div id="login-form">
  <form name="login" ...>...</form>
</div>
dom = document.getElementById ? true : false;

function showLogin() {
  if (dom && document.forms["login"]) {
    document.getElementById("login-form").style.display = "block";
    return false;
  } else return true;
}

That works fine, except it uses global variables and demonstrates constraint with choosing a script loading strategy, which will be detailed below. Also in this case you cannot code any predefined action; everything happens after a user does something.
I personally don't like this method because it leaves JavaScript implants in HTML code, whose main purpose is to describe the document structure, not page behavior.

Moving JavaScript into JavaScript

Therefore, a better way would be to stack everything related to JavaScript into the page .js file.

From here I will write jQuery code to save time and space. If you prefer another library, just get the gist.

$.fn.myPlugin = function() {
  this.fadeIn(
    'normal',
    function() { ... }
  );
}

...

$('#element').myPlugin();

With plugins you can describe behavioral pattern and then apply it to the elements needed. In this case you don't need to inject JavaScript into HTML any more, which makes maintenance easier. But for every single element you still have to write a line of code to apply the plugin.

Declaring behavior with CSS classes

When having many controls per page it's not useful to initialize each of them separately. A better way would be to mark HTML block with a matching CSS class.

<input class="js-myPlugin" type="text" name="login" />
$.fn.myPlugin = function() {
  this.fadeIn(
    'normal',
    function() { ... }
  );
}

$('.js-myPlugin').myPlugin();

With that you need your 'linking' code only once for a plugin; it can be written in the plugin file to maintain consistency.

One CSS class to rule them all

However, when the variety of components grows, the 'linking' solution should be even more declarative. If there is JavaScript to run for a component, it will be visible from the block HTML structure. You can mark all the 'alive' parts of your page with a special CSS class; for now let’s stick with js class.

<input class="inpt js" data-component="myPlugin" name="login" value="" />

The step up of this method is the need of information which plugin is to apply to a particular control and a little more code to initialize. This leads onto a question about how to parametrize the component. This example suggests data attributes although there is another idea (spoiler!).

With minor change there is the code initializing a proper JavaScript component for all the blocks on a page:

$(".js").each(function () {
  var $this = $(this);
  $this[$this.data("component")]();
});

Parametrizing

Not only a component name has to be provided when initializing with js CSS class. Different instances of the same component also may need to pass some information. For example, an input plugin can validate the field value. But depending on the input different kinds of data can be expected.

$.fn.myInput = function () {
  // ...
};
<input name="login" value="" /> <input name="zipcode" value="" />

It is easy to provide per-instance parameters when initializing every instance separately.

$('input[name="login"]').myInput({
  validator: "login",
});
$('input[name="zipcode"]').myInput({
  validator: "zipcode",
});

data attributes

With more declarative manner you need to include such information into a component HTML structure. Already mentioned data attributes can help with that.

<input
  class="inpt js"
  data-component="myInput"
  data-validator="login"
  name="login"
  value=""
/>

<input
  class="inpt js"
  data-component="myInput"
  data-validator="zipcode"
  name="login"
  value=""
/>
$.fn.myInput = function() {
    if (this.data('validator') === 'login') // Supported by many frameworks
}

Data attributes is a brainchild of HTML5, so carry a doctype when using them.

As a widespread solution with many libraries providing helpers for them, data attributes make parametrizing not difficult. Although the limitation of being able to use key-value pairs only can affect the plugin architecture.

onclick

An elegant way is to provide data for a JavaScript component using natural JavaScript format, such as hashes. Here the event attributes can serve us again.

<input
  class="myInput"
  onclick="return {
        validator: 'login'
    }"
  name="login"
  value=""
/>

When a user normally clicks, the hash is booleanized into true, so the default browser click reaction is exactly as expected. From your plugin you can call the event-named method and get the data in its native JavaScript format without any need to parse and convert.

$.fn.myInput = function () {
  this[0].onclick().validator === "login";
};

As JavaScript, this data can be of any type. Not only strings and numbers, but large treeish hashes, arrays and even functions are possible. Such flexibility is extra salutary for organizing data when writing complex components and for customization.

<input
  class="inpt js"
  onclick="return {
        component: 'myInput',
        validator: function() { ... }
    }"
  name="login"
  value=""
/>

With nested hashes an HTML block can be operated by 2 or more different JavaScript components, each with its own set of parameters.

<input
  class="inpt js"
  onclick="return {
        'myInput' : {
            validator: function() { ... }
        },
        'anotherPlugin' : {}
    }"
  name="login"
  value=""
/>

Start fast

Initializing components

Applying plugins to elements is only possible when the page is loaded and your library can look for these elements in the document.

First, you can link the script file after its related piece of HTML code.

<input
  class="myInput"
  onclick="return {
        validator: 'login'
    }"
  name="login"
  value=""
/>
<script src="js/plugins/myInput.js"></script>

Whilst it can work sometimes, the method is just unreliable. It's not certain that the DOM will be ready by the time of the script loaded. Loading every plugin as a separate file definitely slows down the page. In addition, this is just ugly.

The page needs to be loaded when running scripts. So, logically an onload event on the window object can help.

$(window).load(function () {
  $(".js").each(function () {
    var $this = $(this);
    $this[$this.data("component")]();
  });
});

The drawback is that window.onload occurs when all the content is loaded. Including images, which mostly are not necessary to run scripts.

The time to safely initialize your components is right after the document has been loaded and parsed into DOM, which is accompanied by a document.ready event.

$(function () {
  $(".js").each(function () {
    var $this = $(this);
    $this[$this.data("component")]();
  });
});

With that all the plugins used can be bundled into one JavaScript file and linked to a page within the <head> element as usual. Purposely loaded in advance, the script will be run when the page is ready.

Loading scripts

Although, putting <script> tag into <head> also delays page loading. A better strategy is to draw a page as fast as possible and then introduce JavaScript magic. Reach that by placing <script> before the closing </body> tag.

On the other hand, when a user sees a page, it is expected to work. By now, it's hardly possible to respond user actions with plain HTML; people are used to rich interfaces. If that's crucial for your page functioning, divide the script file into 2. The first one linked in <head> provides major functionality. And the second, placed before </body> contains all the magic tricks.

Common best practises

Cover up

In this article, I switched from calling showLogin function to jQuery plugins and some improvements were made in linking components to HTML code. But more than this, giving JavaScript responsibility for the page behavior also enables to step forward with avoiding global variables in the code.

This is true not only for JavaScript. The ids, form names and input names (all the things that can be named in HTML) are also a kind of global object.

Common advice is to avoid their use.

By reducing your global footprint to a single name, you significantly reduce the chance of bad interactions with other applications, widgets, or libraries. Douglas Crockford

With the module pattern wrapping your component into self-invoking anonymous function all the global variables turn into local ones.

Use CSS classes instead of id and name attributes to identify elements and access them from JavaScript. This makes code more declarative, introduces flexibility and is easier to maintain in the long run.

Doublecheck you don't double-do

Initializing components can take time and resources. So, when first initialized, the result can be saved and reused later. jQuery provides a nice data method suitable for that.

$.fn.myPlugin = function() { return this.each(function() {
  var $this = $(this),
      data = $this.data('myPlugin');
  if (!data) {
    // init...
    $this.data('myPlugin', { /* ... */ });
  }
}};

Life beyond the element

If the corresponding block has been removed from a page, it's the component's responsibility to clean up. Event listeners, constant background calculations and even layout changes, that could have happend before. When being deactivated, a component has to place things back with a destroy method.

$.fn.myPlugin = function() { return this.each(function() {
  var $this = $(this),
      data = $this.data('myPlugin');
  if (!data) {
    // init...
    $this.data('myPlugin', {
      destroy: function() { /* ... */ }
    });
  }
}};

Initializing by request

With dynamically changed pages it's not enough to initialize components just the ones after domReady. There has to be some code to run every time you append new block onto a page, which turns HTML fragment into a functioning component.

$.fn.myInit = function() {
    this.find('.js').each(function() {
        var $this = $(this);
        $this[$this.data('component')]();
    });
    return this;
})

A special method to destroy the components being removed has similar sense.

$.fn.myDestroy = function() {
    this.find('.js').each(function() {
        var $this = $(this),
            name = $this.data('component');
        $this.data(name).destroy();
    });
    return this.remove();
})

Lazy initialization

One more trick is a lazy initialization of components. The idea is similar to lazy loading and merely means do as less as possible beforehand.

Instead of initializing all the components when domReady fires, you can do this separately for every component at the time the particular component needs that. Usually that's after a user started to interact with it and the respective events triggered. So, all the necessary predefined actions won't be run in advance and won't slow down page rendering and the work of other components. Objects corresponding to never-used components won't be created, which saves memory.

Component core

Many code practises above would be repeated again and again when implementing a lot of components. As you can guess, all the common things can be wrapped with a framework representing the core code for the components.

One of the examples of this is jQuery UI. As many other frameworks it provides API to create your own JavaScript components.

Predefined component structure

With a component framework a developer uses API and describes a predefined component structure. All the common things are in the core.

$.widget('my.component', {
    _create: function() { /* ... */ },
    destroy: function() { /* ... */ }
    myMethod: function() { /* ... */ },
});

High-level patterns

Besides predefined structure, frameworks also provide architectural solutions to different problems.
For example, with the releaving modules a core can provide an API to extend the main component functionality.

$.widget("ui.dialog", $.ui.dialog, {
  close: function () {
    if (confirm("Close???")) {
      this._super("close");
    }
  },
});

Component frameworks define basics for reusing code and implementing reusable JavaScript modules.

Another example of such a high-level component framework is JavaScript part of BEM. A developer can operate components with OOP common patterns; create classes, their dynamic and static methods, as well as inherit component and extend their functionality and reuse basics with super calls.

BEM.DOM.decl(
  { block: "my-dialog", baseBlock: "dialog" },
  {
    myMethod: function () {
      this.__base();
      this.__self.myStaticMethod();
    },
  },
  {
    myStaticMethod: function () {
      /*...*/
    },
  }
);

From now on

A number of JavaScript frameworks and libraries were created last years. Generally they contain the solutions to the problems listed and also cover others, like:

  • loading by request
  • building page .js files
  • dependency system
  • complex component relations and data sharing
  • data binding

With comparing articles like Rich JavaScript Applications – the Seven Frameworks by Steven Sanderson you can choose the one you like and have a quick look under the hood.

Credits

Before I wrap up, let me thank Sergey Berezhnoy also known as @veged for his presentation about creating JavaScript components. Russian speaking readers can also enjoy the video! Сергей Бережной, Яндекс Разные способы создания клиентских js-компонентов

You can hire me and the whole Bridge-the-Gap team to set up, manage, develop, and champion your design system. I can align the design and development processes in your organisation for a larger business impact.

© Varya Stepanova 2024