In the previous blog article, I explained the concepts you need to know before building a pure javascript reusable component. It is now time to practice our newly acquired knowledge.

In this tutorial, we will go through everything you need to know to kickstart your widget. We will cover a wide range of topics including:

  • The tools we will install
  • Setting up a project
  • Writing a user interface
  • Sharing the library

To facilitate writing user interfaces, I decided to use a few tools. These tools do not add any dependencies to the deliverables.

  • Webpack: a tool that we will use to bundle our source code in a single .js file.
  • SASS: CSS with superpowers
  • PostCSS, autoprefixer and cssnano: tools to add vendor prefixes and minify the css code
  • Uglify-js: a javascript minifier

Because the web does not have enough todo-list implementations, our sample project will be a todo-list widget.

Note: you can find the source code of this widget here.

Setting up the project

First, make sure your installed node.js and npm on your computer. Then, create a folder to hold your project and run the following command inside it:

npm init

It will ask you a few questions. For information, you can find my answers on the image below.

npm init

Installing Webpack

Webpack will bundle all our files into a single JS file that we will later share. Along with all the dependencies I presented earlier, we will also install webpack-dev-server, which is a development server that provides live reloading. It will help us develop our widget.

npm install --save-dev webpack webpack-dev-server webpack-cli sass-loader node-sass css-loader style-loader postcss-loader uglifyjs-webpack-plugin html-webpack-plugin cssnano autoprefixer

Installing babel

Babel will transpile our ES6 code into ES5 to ensure it works with older browsers.

npm install babel-loader @babel/core @babel/preset-env --save-dev

Setting up Webpack and PostCSS

Now that we installed all the dependencies, let's add our webpack and postcss configuration.

In the root directory of your project, create a webpack.config.js file and append the following configuration to it


const path = require('path');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const uglifyJsPlugin = require('uglifyjs-webpack-plugin');
const webpack = require('webpack');

const libraryName = 'todo-widget'; // TODO: Change me
const outputFile = `${libraryName}.min.js`;


module.exports = {
  entry: './src/index.js',
  output: {
    library: libraryName,
    libraryTarget: 'umd',
    libraryExport: 'default',
    path: path.resolve(__dirname, 'dist'),
    filename: outputFile,
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/preset-env'],
            },
          },
        ],
      },
      {
        test: /\.scss$/,
        use: [
          'css-loader',
          'postcss-loader',
          'sass-loader',
        ],
      },
      {
        test: /\.(png|jp(e*)g|svg)$/,
        use: [{
          loader: 'url-loader',
          options: {
            limit: 20000, // Convert images < 8kb to base64 strings
            name: 'img/[hash]-[name].[ext]',
          },
        }],
      },
    ],
  },
  plugins: [
    new uglifyJsPlugin(),
    new HTMLWebpackPlugin({
      template: path.resolve(__dirname, 'index.html'),
    }),
    new webpack.HotModuleReplacementPlugin(),
  ],
};
webpack.config.js

This file basically tells webpack to package any css/javascript or image into a single file. You need to change the 7th line with the name of your library.

Finally, to setup PostCSS, create a file called postcss.config.js and append the following content to it:

module.exports = {
  plugins: [
      require('autoprefixer'),
      require('cssnano'),
  ]
};
postcss.config.js

It tells postcss to load autoprefixer and cssnano.

Setting up a development environment

Finally, before we start coding, we will setup a development environment. To start our development server, we will need to create a new npm script. Open your package.json file, and locate the "scripts" key.

Add the following values to the list of available scripts:

"build": "webpack",
"start": "webpack-dev-server --open"

It should now look like this

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "start": "webpack-dev-server --open"
  },

Then, create an index.html file. This is where we will display our plugin while we're developing it.

<!doctype html>
<html>
  <head>
    <title>Todo Widget</title>
  </head>
  <body>
      Hello, world!
  </body>
</html>
index.html

In the webpack configuration, we specified that the entry point of our library is src/index.js. Create the src folder and the index.js file. Then, add an alert so we can test everything is working correctly.

alert('hello, world from the widget!');
src/index.js

To start the dev server, run npm start. It will automatically build our plugin and insert it in the index.html file we created earlier. Open your browser and navigate to https://localhost:8080. The alert should display.

Coding the widget

Our widget will define two custom elements: todo-list and list-item. The first one holds the list and the second one is an element of the list.

Each list-item can have a "done" attribute that will strike the text if set.

Add the following code in the <body> your index.html:

<todo-list>
  <list-item done>Clean the bedroom</list-item>
  <list-item>Watch a movie</list-item>
</todo-list>

Once we finish our plugin, this code will render a nice todo-list that look like this

The final result

Here's a list of all the files we will create for our widget.

.
├── src
│   ├── css
│   │   ├── item.scss
│   │   └── list.scss
│   ├── lib
│   │   ├── list-item.js
│   │   └── todo-list.js
│   └── index.js
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
└── webpack.config.js

The src/lib folder will contain the source code of our widget. As you can see, it has two files in it, one for each custom element. The src/index.js file will register our custom elements so we can use them in the HTML page. As we saw earlier, it is the entrypoint of our library.

In the css folder, we will add all our styles. In this tutorial, I decided to create one for each custom-element.

The todo-list element

Let's start with todo-list.js. At the top of the file, I like to define the initial structure of our web component. To do this, I create a <template> element.

import styles from '../css/list.scss'

const template = document.createElement('template');
template.innerHTML = `
  <style>${styles.toString()}</style>
  <ul>
    <slot>
      <p>You have nothing to do, yay!</p>
    </slot>
  </ul>
  <input id="new-item"/>
  <button>Add</button>
`;

As you can see, I imported the styles from the lib/css/list.scss file. Then, I used the toString() method inside a <script> element to append them into the DOM. Why did I import the styles as a module? Because we will later create a Shadow DOM and we have to define them inside it. If we directly imported them using import '../css/list.scss', they would be defined outside of our shadow DOM and may also clash with the ones already defined in the page.

Our custom-element is simple. It has an unordered list, an input and a button. Inside the list, you will notice a <slot> element. In a shadow DOM, the content of a slot will automatically be replaced with the children from the Light DOM.

<todo-list>
  #shadow-root
    <slot></slot> <!-- the slot will contain the two list-items -->
    
  <list-item done>Clean the bedroom</list-item>
  <list-item>Watch a movie</list-item>
</todo-list>

It's time to implement the element. Our class is simple. In the constructor, it initializes the structure of the element by rendering the template we created earlier. The connectedCallback sets the "onclick" event on the button and the addItem method adds the new item once we click the Add button.

class TodoList extends HTMLElement {
  constructor() {
    super();

    // Add a shadow DOM
    const shadowDOM = this.attachShadow({ mode: 'open' });   

    // Render the template in the shadow dom
    shadowDOM.appendChild(template.content.cloneNode(true));

    // Method binding
    this.addItem = this.addItem.bind(this);
  }

  // Called when the element is added to the DOM
  connectedCallback() {
    const button = this.shadowRoot.querySelector('button');
    button.onclick = this.addItem;
  }

  addItem() {
    // Get the input
    const input = this.shadowRoot.querySelector('#new-item');
    
    // Create a new list item
    const item = document.createElement('list-item');
    item.innerHTML = input.value;

    // Add it to the light DOM
    this.appendChild(item);
  }
}

export default TodoList;

To showcase how styling works, you can append the following content to src/css/list.scss

ul {
  background-color: #ffc7c7;
}

The list-item element

Again, I first defined the styles and template for this element, then implemented it. As it is similar to the one we just defined, I'll just give you the code.

import styles from '../css/item.scss';

const template = document.createElement('template');
template.innerHTML = `
  <style>${styles.toString()}</style>
  <li>
    <span>Empty</span>
    <button>Remove</button>
  </li>
`;

class ListItem extends HTMLElement {
  constructor() {
    super();

    // Add a shadow DOM
    const shadowDOM = this.attachShadow({ mode: 'open' });

    // Render the template
    shadowDOM.appendChild(template.content.cloneNode(true));

    // Bindings
    this.onRemove = this.onRemove.bind(this);
    this.renderChanges = this.renderChanges.bind(this);
    this.onDoneChange = this.onDoneChange.bind(this);
  }

  // any attribute specified in the following array will automatically
  // trigger attributeChangedCallback when you modify it.
  static get observedAttributes() {
    return ['done'];
  }
  
  renderChanges() {
    const text = this.shadowRoot.querySelector('li > span');

    // done attribute
    if (this.hasAttribute('done')) {
      text.style.textDecoration = 'line-through';
    } else {
      text.style.textDecoration = 'none';
    }
  }

  // Mark: - Component lifecycle
  attributeChangedCallback(attr, oldVal, newVal) {
    this.renderChanges();
  }

  connectedCallback() {
    const shadowDOM = this.shadowRoot;

    // init list item text and button
    const text = shadowDOM.querySelector('li > span');
    text.innerHTML = this.innerHTML;
    text.onclick = this.onDoneChange;

    // remove button action
    const button = shadowDOM.querySelector('li > button');
    button.onclick = this.onRemove;

    this.renderChanges();
  }

  // Mark: - Actions
  onRemove() {
    this.remove();
  }

  onDoneChange() {
    this.toggleAttribute('done');    
  }
}

export default ListItem;

Registering our custom elements

Finally, open src/index.js and append the following code to it:

import TodoList from './lib/todo-list';
import ListItem from './lib/list-item';

customElements.define('list-item', ListItem);
customElements.define('todo-list', TodoList);

It will register our two custom elements. Navigate to https://localhost:8080/ and you should see it working.

Building production-ready code

To build your code, all you have to do is running the following command:

npm run build

It will create a dist/ folder containing your final javascript file. 🎉

Distributing the library

There are three ways you can distribute your library:

  • as a file

Probably the easiest way to share your widget. In the dist/ folder, grab your .js file and share it.

  • as a NPM package

You can publish your package on NPM in 3 easy steps.

  1. Create an account on npmjs.com
  2. Login to npm in your terminal by running the following command: npm login
  3. Run npm publish

You should see something like this.

npm publish
  • As a link (hosted on a CDN)

To achieve this, we will use unpkg. Unpkg is a free CDN that hosts your npm packages. Once published, you can get a link to your package using their service.

All you have to do is adding the following lines to your package.json:

  "unpkg": "dist/plugin-name.min.js",
  "files": [
    "dist"
  ],

The first line tells unpkg the file to serve, and the last 3 lines tell npm to add the dist folder in the package. (if you ignored it in the .gitignore file)

The link to your file will look like this

https://unpkg.com/:package@:version/dist/:package.min.js

in our case, it would be https://unpkg.com/todo-widget@1.0.2/dist/todo-widget.min.js

Finally, the following HTML initiates our plugin and downloads the script from the CDN:

<!doctype html>
<html>

<head>
  <title>Todo Widget</title>
</head>

<body>
  <todo-list>
    <list-item done>Clean the bedroom</list-item>
    <list-item>Watch a movie</list-item>
  </todo-list>
  <script type="text/javascript" src="https://unpkg.com/todo-widget@1.0.2/dist/todo-widget.min.js"></script></body>
</html>

That's it! I hope you enjoyed this article. If you want to be notified when we post something new, click on the notification bell located at the bottom right end corner of your screen. By the way, the bell is a widget I built using the knowledge I shared in this tutorial 😉

PS: The source code for this tutorial is available here.