Publishing a Web Components Library

Once upon a time, in order to create web components, one had to resort to a front end framework like angular or react (yes yes, I’ve heard the “it’s only a lib” before from all the angularists in the crowd) .

Web component is one awesome piece of technology (or rather, a combination of 4 technologies). Now that browser support is rising, publishing a web component to github for global consumption seems like a good contribution to the community.

In this article, we will build upon the example shown in the former article in the web components series: the modal window web component. You can read the article, or go directly to the repository on github.

We will structure a project in a scalable way, and modularize the components so that consumers would be able to consume only one component without the need to import the whole library.

Clone the repository

  1. Make sure you have git installed;
  2. git clone https://github.com/YonatanKra/web-components-ui-elements.git
  3. git checkout before_componentization
  4. yarn or npm i

You now have the repository with the modal window from the previous article. For the full result, just checkout master.

Create the main file that imports everything

Like most modern libraries, we would like to create one file where people could import all of our components in one go (a.k.a. a barrel file). In addition, we’d like to add the ability to import specific components for performance sensitive applications (a.k.a. lazy load).

We can do that by splitting our code to separate modules both in folder structure as well as in the webpack code splitting configuration.

The components folder:

Inside our src folder create a components folder. Inside create a ce-modal-window folder and copy the ce-modal-window files into it. In addition, create an index.js file there.

In the index.js file we will import CEModalWindow and define the custom element. In CEModalWindow we will remove the custom element definition.

This is how the files should look in the ce-modal-window folder:

const templateString = `
<style>
.overlay {
opacity: 1;
visibility: visible;
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: rgba(0, 0, 0, 0.42);
-webkit-transition: opacity 0.5s;
transition: opacity 0.5s;
display: flex;
align-items: center;
justify-content: center;
}
.overlay-hidden {
opacity: 0;
visibility: hidden;
-webkit-transition: opacity 0.5s, visibility 0s 0.5s;
transition: opacity 0.5s, visibility 0s 0.5s;
}
</style>
<div class="overlay overlay-hidden">
<div class="overlay-content"></div>
</div>
`;
const template = document.createElement('template');
template.innerHTML = templateString;
export class CEModalWindow extends HTMLElement{
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(template.content.cloneNode(true));
this._overlay = shadowRoot.querySelector('.overlay');
this._content = shadowRoot.querySelector('.overlay-content');
}
open(config) {
const supportedStyles = ['width', 'height'];
if (!config) {
return;
}
this._overlay.classList.remove('overlay-hidden');
this._content.innerHTML = config.content;
supportedStyles.forEach((style) => {
CEModalWindow.setStyle(this._content, style, config[style]);
});
}
close() {
this._overlay.classList.add('overlay-hidden');
}
static setStyle(element, style, value) {
const pxStyles = ['width', 'height'];
if (value) {
if (pxStyles.indexOf(style) > -1) {
value += 'px';
}
element.style[style] = value;
}
}
}
import { CEModalWindow } from './ce-modal-window';
if (!customElements.get('ce-modal-window')) {
window.customElements.define('ce-modal-window', CEModalWindow);
}
view raw index.js hosted with ❤ by GitHub

Finally, let’s change our tests a bit to make them more robust by defining the custom element inside our spec file:

import { CEModalWindow } from './ce-modal-window';
window.customElements.define('ce-tested-modal-window', CEModalWindow);
describe('app integration tests', () => {
let element, shadowRoot;
beforeEach(() => {
element = document.createElement('ce-tested-modal-window');
shadowRoot = element.shadowRoot;
document.body.append(element);
});
// check that the exposed API works
describe('init', () => {
it('should add a div with the overlay and overlay-hidden classes under the shadow root', () => {
expect(shadowRoot.querySelector('.overlay.overlay-hidden')).toBeTruthy();
});
});
describe('open', () => {
it('should remove the hidden class from overlay', () => {
const overlay = shadowRoot.querySelector('.overlay.overlay-hidden');
element.open({});
expect(overlay.classList.contains('overlay-hidden')).toBeFalsy();
});
it('should add class transparent to the overlay if config.hideOverlay is true', () => {
const overlay = shadowRoot.querySelector('.overlay.overlay-hidden');
element.open({
hideOverlay: true
});
expect(overlay.classList.contains('transparent')).toBeFalsy();
});
it('should insert the content string as HTML to the content element', () => {
const randId = Math.random().toString(2);
const content = shadowRoot.querySelector('.overlay-content');
const htmlBefore = content.innerHTML;
const config = {
content: `<div id="${randId}">Hello CE!</div>`
};
element.open(config);
expect(htmlBefore).toEqual("");
expect(content.innerHTML).toEqual(config.content);
});
it('should set width and height according to config values', () => {
const content = shadowRoot.querySelector('.overlay-content');
const config = {
height: Math.round(Math.random() * 100 + 50),
width: Math.round(Math.random() * 100 + 50)
};
element.open(config);
const overlayBoundingBox = content.getBoundingClientRect();
expect(overlayBoundingBox.width).toEqual(config.width);
expect(overlayBoundingBox.height).toEqual(config.height);
});
});
describe('close', () => {
it('should add the overlay-hidden class', () => {
const overlay = shadowRoot.querySelector('.overlay');
element.open({}); // we already know it removes the class
element.close();
expect(overlay.classList.contains('overlay-hidden'));
});
});
afterEach(() => {
document.body.removeChild(element);
})
});
It’s the same test file, only now we are defining the custom element inside. This will enable us to do integration tests later on, for our main file, because you cannot define the same element tag on the same page and refreshing the page would cost us in test time.

All the tests pass. We can move on.

The main file

Let’s create src/main.spec.js:

 

import './main';
import { CEModalWindow } from "./components/ce-modal-window/ce-modal-window";
describe('ui-elements integration tests', () => {
describe('ce-modal-window', () => {
it(`should be defined`, () => {
const ceModalWindowClass = window.customElements.get('ce-modal-window');
expect(ceModalWindowClass).toBe(CEModalWindow);
});
});
});
view raw main.spec.js hosted with ❤ by GitHub
Testing that the custom element is registered as expected

And now implement src/main.js that makes this test pass:

import * from './components/ce-modal-window';
view raw main.js hosted with ❤ by GitHub

The main file would just import our components and make sure they are defined correctly. We can add more integration tests here but for now it is enough.

Anyone who requires our main.js file will get the custom elements we import to main.js.

The demo folder

For order’s sake, we will give the demo its own folder.

Move the src/index.js to a demo folder and import main.js instead of using the standalone components:

import '../main';
const modalWindow = document.createElement('ce-modal-window');
modalWindow.addEventListener('click', () => {
modalWindow.close();
});
document.body.appendChild(modalWindow);
const button = document.createElement('button');
button.innerText = 'Open modal';
button.addEventListener('click', () => {
modalWindow.open({
content: '<h1>Hello Modal</h1>',
height: 50,
width: 100
});
});
document.body.appendChild(button);
view raw index.js hosted with ❤ by GitHub

Here’s the commit for this step.

That’s so very cool. Our tests still pass, so our app is supposed to work as expected ( npm run serve or npm run build and then run the resulting dist\index.html).

Now we need to tell our build process to split our files so they can be consumed separately.

File splitting

File splitting in webpack is a breeze. I’ve written an article about it when webpack 3.0 was all the rage. Webpack 4 makes the whole splitting so much easier…

In our case, we’d like to do the following:

  1. Expose the main file so one could import the whole library in one go
  2. Expose each component in its own file
  3. Create the demo folder

Webpack has us covered here. We just go to the config/webpack.common.js file and change a few things:

  1. Our entry statement would now include our main module, the new demo folder and our components (currently we have only one — but we’re going to scale soon).
  2. The HTML webpack plugin now adds the HTML file inside the demo folder
  3. I’ve added a new plugin — the Clean webpack plugin, which conveniently removes the dist folder on every build

Here’s the webpack.common.js code:

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
'demo/index' : './src/demo/index.js',
index: './src/main.js',
'lib/ce-modal-window/index': './src/components/ce-modal-window/index.js'
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'TDD Challenge',
meta: {
viewport: 'width=device-width, initial-scale=1'
}
})
],
module: {
rules: [
// use the html loader
{
test: /\.html$/,
exclude: /node_modules/,
use: {loader: 'html-loader'}
},
// use the css loaders (first load the css, then inject the style)
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|gif|jpg|jpeg|svg|xml|json)$/,
use: [ 'url-loader' ]
}
]
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
}
}
}
};

If you build ( npm run build ) a dist folder will be created and you will see it, inside our folder structure.

This way, anyone using our package will be able to do import 'web-components-ui-elements’ to get our main index.js file or import 'web-components-ui-elements/components/ce-modal-window' to just get the modal window component without loading the whole library.

You can see the full commit here.

NPM configurations (a.k.a. package.json)

The package.json should point to the main file. It’s already doing that (in the main attribute). We should add a description and change the name of our module.

There are lots and lots of configurations to a package.json. You can read more about them here and modify as you like. Here’s what I came up with:

{
"name": "web-components-ui-elements",
"version": "1.0.0",
"description": "UI elements library based on web components",
"main": "index.js",
"scripts": {
"test": "karma start config/karma.conf.js",
"build": "webpack --config config/webpack.prod.js",
"build:watch": "webpack --config config/webpack.prod.js --watch",
"serve": "webpack-dev-server --config config/webpack.dev.js"
},
"author": "Yonatan Kra",
"license": "MIT",
"devDependencies": {
"clean-webpack-plugin": "^0.1.19",
"copy-webpack-plugin": "^4.5.3",
"css-loader": "^1.0.0",
"file-loader": "^2.0.0",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"jasmine": "^3.2.0",
"karma": "^3.0.0",
"karma-webpack": "^3.0.5",
"style-loader": "^0.23.1",
"url-loader": "^1.1.2",
"webpack": "^4.20.2",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.9",
"webpack-karma-jasmine": "^3.0.4"
},
"dependencies": {
"webpack-merge": "^4.1.4"
},
"repository": {
"type": "git",
"url": "https://github.com/yonatankra/web-components-ui-elements.git"
}
}
view raw package.json hosted with ❤ by GitHub

The relevant commit is here.

Documentation

Our users can’t read our mind. Let’s add documentation:

  1. Edit the README.MD file (if it doesn’t already exist, create it).
  2. Check out the github markdown page to see how to format your documentation.
  3. Let’s document our work:

web-components-ui-elements

A web components UI library.

Installation

npm install web-components-ui-elements

Usage

You can require the whole library:

import * from web-components-ui-elements;

And use in the DOM like this:

<ce-modal-window id="modal-window"></ce-modal-window>

And then use the API:

const modal = document.querySelector('#modal-window');
modal.open({
    content: '<h1>Hello Modal!</h1>'
});

// close the modal when clicking on it
function closeModal() {
    modal.close();
    modal.removeEventListener('click', closeModal);
}
modal.addEventListener('click', closeModal);

If you want, you can just create the element on your own and add it to the DOM:

const modalWindow = document.createElement('ce-modal-window');
modalWindow.addEventListener('click', () => {
    modalWindow.close();
});
document.body.appendChild(modalWindow);

const button = document.createElement('button');
button.innerText = 'Open modal';

button.addEventListener('click', () => {
    modalWindow.open({
        content: '<h1>Hello Modal</h1>',
        height: 50,
        width: 100
    });
});
document.body.appendChild(button);

API

Modal Window

Tag Name

ce-modal-window

Methods

Open

Accepts a config object and opens the modal.

Close

Closes the modal

ICD

Config
{
    content: '', // <string> HTML snippet to show inside the modal
    hideOverlay: false, // <boolean> show or hide the opack overlay behind the modal
    height: 150, // <number> height of the modal
    width: 150, // <number> width of the modal
}

Contributing

  • Clone
  • npm i
  • npm run build to get the build
  • npm run test to test
  • npm run serve to run a development environment
view raw readme.md hosted with ❤ by GitHub

4. Let’s install something that auto-creates a table of contents (TOC) for us:
`npm i -D doctoc`

5. Add a precommit hook to build the TOC inside the package.json:
`”precommit”: “doctoc ./README.md”,`

Here’s the commit for these changes: The Commit.

NPM Publish

Before we continue, please add "private": true to the package.json. Remove it when your library is truly ready to publish.

Done? Cool. This one is easy:

npm login and follow along the instructions

npm publish

And……….. We are done!

Summary

Congratulations! You’ve just published a web component library to NPM.

You can maintain it and add more features to it.

Keep in mind that a CI/CD process should be setup, so we can manage an army of contributors to this new awesomeness.

Hope you had fun and learned something here 🙂

Sign up to my newsletter to enjoy more content:

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments