Build a Reusable JavaScript Library

Published Oct 17, 2016Last updated Jan 18, 2017

Why you need to create a custom library

One great quality of a top developer is the ability to do more with less—ranging from variable declarations to optimizing and refactoring a code to make a function block that takes specific values and returns the desired output.

When you work on multiple projects, you begin to see patterns in the code that you write, features that you use often, those you rarely use, and those you never use. Things like AJAX requests and manipulating DOM elements seem to rank high on that list. As time passes, you get tired of implementing the same functionality from scratch or using a library that significantly affects the load time of your web application. That is when you should consider creating a custom library for those specific functionalities.

Now that I have explained why you need a personal custom library as a developer, let’s get started on creating one.

Application Logic

We are going to build a JavaScript library that has similar functionalities as jQuery. It manipulates a selected DOM element when creating HTML elements or editing pre-existing ones.

What the end goal is to be able to select a DOM element and modify content using the format:

ghost('div').html('<p>I am a paragraph</p>');

Setting Up The Development Environment

For the purpose of this tutorial, we are going to use Webpack to transpile, bundle,  and minify our library. You can check out my previous post on Webpack.

You can fork an already prepared repository here or you can follow along below to create one.

We can start our project by creating a project directory called library, cd into it and do:

# instantiate an npm project
$ npm init

And fill in answers to the questions presented to you.

Create files and folders to match our project file and folder structure below:

library
    |- .babelrc
    |- package.json
    |- webpack.config.js
    |- src
        |- App.es6

Install the required dependencies, using Yarn or NPM:

npm i webpack webpack-dev-server babel-core babel-loader babel-preset-es2015 -D

Also, install our dotenv module so we can add our configurations from an external file:

$ npm i dotenv -D

Create a .env file at the root of our project folder. These configurations will be used in our Webpack configuration file using the process.env object

#Library Name
NAME=ghost
# production/development
NODE_ENV=production
# window/umd
TARGET=window

Next, we need to add configurations to our webpack.config.js file. I am going to add a simple configuration that just includes the entry, Babel loader for our ES6 code, and output filename and path. Also, we can specify our output as a library and the target for our library (in our case, it’s a browser).

Our Webpack configuration can look like:

const path = require('path');
const config = require('./package.json');

const webpack = require('webpack');
require('dotenv').config();

const PROD = process.env.NODE_ENV === 'production';

let plugins = [];

PROD ? [
    plugins.push(new webpack.optimize.UglifyJsPlugin({
      compress: { warnings: false }
    }))
  ] : '';

module.exports = {
  entry: path.resolve(__dirname, config.main),
  devtool: 'source-map',
  output: {
    library: process.env.NAME,
    libraryTarget: process.env.TARGET,
    path: __dirname,
    filename: (PROD) ? 'build/ghost.min.js' : 'build/ghost.js'
  },
  module: {
    loaders: [
      {test: /\.es6?$/, exclude: /node_modules/, loader: 'babel-loader'}
    ]
  },
  plugins: plugins
};

Add our presets configuration for babel in .babelrc file contains:

{
  "presets": ["es2015"]
}

NOTE : Make sure that your main key value in your package.json file contains the path to your entry file, in this case, it should contain src/App.es6.

Building Our Library

Our library is going to be a really simple one, built using an ES6 class and a function which has an instance of the class that can be exported.

First, we have our class which can set the selector. Select the DOM element using its constructor.

"use strict";

class ghost {
  constructor(selector) {
    this.selector = document.querySelector(selector);
  }
}

Next, we want to be able to get or edit the HTML content of a DOM element. We can add the html() method to our class:

html (content = null) {
  if (content !== null) {
    this.selector.innerHTML = content;
  }
  return this.selector.innerHTML;
}

In the method above, we are using the ES6 default argument value to set the content to null. We check if the content argument is not null and change the value of our DOM element based on that and return the value.

Fairly simple, you can customize and add more functionalities to this JavaScript library as you see fit.

Now, we can export an anonymous function that returns an instance of this class using es5 module.exports:

module.exports = (selector) => {
  return new ghost (selector);
};

Testing with Mocha, Chai, & JSdom

Just like every other code that you write, you should have an accompanying test that validates it. To test our library, we are going to use Mocha, Chai, and JSdom to turn our test environment into a mock headless browser.

First, we need to install our dependencies:

$ npm i mocha chai jsdom jsdom-global -D

Next, we can create our test folder in the project root directory and create test file.

# create test directory
$ mkdir test
$ cd test

# create test file and mocha opts file
$ touch index.test.js mocha.opts

Add JSdom global as a requirement for our Mocha test and also add babel-core as compiler for our JS files in the mocha.opts file:

--compilers js:babel-core/register
-r jsdom-global/register

Time to write our tests. Import necessary modules in index.test.js:

// import chai as our assertion library
import chai from 'chai';

//require jsdom-global and run
require('jsdom-global')()

// import our library
import ghost from '../src/App.es6';

// initialize chai should
chai.should();

Next, we need to create an HTML element that we can run our test with. We can achieve this using the document global that is made available to us using JSdom.

//....
// create mock html tag
document.body.innerHTML = "<div>Sample text in div</div>";

Now, we can describe our library:

describe('#Ghost Library Test', function () {
  //....
  // create other descriptions and run assertions in here
});

Since the selector in our library gets HTML elements from our DOM, it should return an object, we can write our test for that as:

describe('#Element Selector', function () {
  it('should be an object', function () {
    ghost('div').selector.should.be.an('object');
  });
});

We can test our HTML method in our library, too:

//....
// check if it returns a string
describe('#DivContent', function () {
  it('should be a string', function () {
    ghost('div').html().should.be.a('string');
  });

  // test if it returns the actual text within div in mock html
  it('should equal Sample text in div', function () {
    ghost('div').html().should.equal('Sample text in div');
  });

  // see if it changes the value
  it('should equal paragraph text', function () {
    ghost('div').html('<p>changed value</p>').should.equal('<p>changed value</p>');
  });

  // see if it clears the text content
  it('should equals empty text', function () {
    ghost('div').html('').should.equal('');
  });
});

Writing test for every line of code is advisable, that way you have code quality assurance in mind. You can use libraries such as Istanbul for code coverage, making sure that every line of code that you write ran at least once during your test. For ES6 codes, you can use isparta to check for coverage in the original ES6 code before it gets transpiled.

Continuous Integration

If you are using version control systems like git, you might want to set up systems that run test builds each time you commit and push to the remote repository. If you are working with a group of other developers, continuous integration is a good way to make sure that your tests pass and they contain good code quality that won’t break the build before you go through with any pull request. You can use tools like travis-ci for that.

Conclusion

You can build your library using:

$ webpack && NODE_ENV=development webpack

The code above creates both a production and development version of our code in ES6. Running this can be a bit tedious to do every time we want to create a build. You can add that as "build" script in your package.json file and then run this instead:

$ npm run build

For development server using webpack-dev-server, you can create an HTML page in the root of our project directory that has content:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    <div class="">Call Me</div>
    <script src="build/ghost.js" charset="utf-8"></script>
    <script type="text/javascript">
      console.log(ghost('div').html());
    </script>
  </body>
</html>

And run the webpack-dev-server with hot module replacement:

$ webpack-dev-server --inline --hot

You can also add that as a "dev" script in your package.json file and run development server as:

$ npm run dev

That’s it! Good To Go!

These are the basic building blocks of creating a JavaScript library and with these systems put in place, it is very easy to maintain if your library scales or grows to be something much more than you expected.

Discover and read more posts from Chimeremeze Ukah
get started