Frameworks vs. Web Components
What are Web Components anyways?
Web components are a set of web platform APIs that allow you to create new custom, reusable, encapsulated HTML tags to use in web pages and web apps. Custom components and widgets built on the Web Component standards, will work across modern browsers, and can be used with any JavaScript library or framework that works with HTML.
In fact, Web Components are just like any other DOM element. All you need to use them is a simple HTML file with some additional lines of Javascript (of course, you can also import your Javascript from an external file) and a modern browser (e.g. recent versions of Google Chrome, Mozilla Firefox, Safari or Microsoft Edge, based on Chromium). No additional dependencies. You don’t even need NPM or YARN if you don’t want to. Or Webpack or any other bundler or sophisticated tool (well, RollupJS comes in pretty handy sometimes as I’ll show you later). But, generally speaking, you can really KISS („keep it simple, stupid“).
The big advantage (if you ever maintained e.g. an old AngularJS app you surely know what I mean): just like with HTML, you basically rely on web standards that will likely be around for at least a couple of years, so you should be safe from regular, painful framework updates, breaking changes, broken dependencies etc.
Of course, where there is light there is also darkness, so there may be some disadvantages, or at least shortcomings for you, depending on your use-case or project scope. But to me, the advantages weigh a lot more. More on some potential Webcomponent shortcomings later, though.
There are also some JS frameworks which can generate Web Components, but I am pretty sure that in the not too distant future, there will be less and less Javascript frameworks – and standard Web Components will be used all over the place.
Web Components advantages
Some of the main advantages for me are the following:
- Web Components are vendor-independent
- they are based on official web standards
- all major browser vendors are supporting them
- there are already loads of components available, commercial and open source
- stable and predictable lifetime – no upgrade traps, no breaking changes
- you can combine components from different vendors
- no preset styling – you can choose any CSS framework or roll your own styles
- very easy to learn and use
- polyfills available if you really need to support older browsers
Web Components „Hello, World“
You didn’t see that coming, did you? The ever famous „Hello, world“ with a very basic, one-file Web Component:
<html>
<head>
<title>Hello, world</title>
<script language="JavaScript">
class HelloWorld extends HTMLElement
{
connectedCallback() {
this.innerHTML = `Hello, world`;
}
}
customElements.define('hello-world', HelloWorld);
</script>
</head>
<body>
<hello-world></hello-world>
</body>
</html>
HTMLElement is a globally available class, as is the customElements object, which offers some convenience methods for, well, custom elements :).
To make that first component it a bit more dynamic and „complete“, let’s add an attribute to change the text from „outside“ the component. Let’s also check if an element with that name already exists and utilize the mysterious Shadow DOM:
<html>
<head>
<title>Hello, world</title>
<script language="JavaScript">
class HelloWorld extends HTMLElement
{
constructor() {
super();
this.root = this.attachShadow({mode: "open"});
}
connectedCallback() {
let who = 'world';
if (this.getAttribute('who') !== '') {
who = this.getAttribute('who');
}
this.root.innerHTML = `Hello, ${who}`;
}
}
if (!customElements.get('hello-world')) {
customElements.define('hello-world', HelloWorld);
}
</script>
</head>
<body>
<hello-world who="Web Components"></hello-world>
</body>
</html>
This will simply output „Hello, Web Components“ on the HTML page. Please note that a dash is obligatory in the identifier of your components to distinguish them from „official“ DOM elements!
attachShadow creates the so called „Shadow DOM“ and assigns it to the root variable you can use later on. The Shadow DOM basically isolates your Web Components from the surrounding DOM to prevent any side effects from other Javascript or CSS on the page.
Importing your Web Components
As mentioned above, you can also import your components from external JS files of course. You can use ES2015 style modules and import statements (with ‚type=“module“‚) to do so, e.g.
<script src="js/hello-world.js" type="module"></script>
and your script would look exactly the same like in the above example:
// you could import some more components etc. here with e.g.
import './another-component-or-script.js';
class HelloWorld extends HTMLElement
{
connectedCallback() {
this.innerHTML = `Hello, world`;
}
}
customElements.define('hello-world', HelloWorld);
Styling your Web Components
You can style your components „internally“, but you can also open them up for styling from outside, wether you are using Shadow DOM or not. An easy way to make parts of your „shadowed“ component UI customizable is to use CSS variables. So, inside your component, you could define an element with a background-color like this, optionally with a default value:
#line{
background-color:var(--wizard-line-color, #dfdfdf);
}
Now, when using the component, you can overwrite that variable with a custom color, e.g.
<script src="js/coolwizard.js" type="module"></script>
<style type="text/css">
#my-wizard{
--wizard-line-color: rgb(172, 0, 122);
}
</style>
</head>
<body>
<cool-wizard id="my-wizard"></cool-wizard>
</body>
Without Shadow DOM, one could still overwrite styles of the component by using ids or classes for the Webcomponent base DOM element or just by more globally defined modifiers on the page („style creep“). But if you use Shadow DOM (or Shadow CSS for that matter), no external, colliding styles will be applied to your components inner DOM and it should always look as expected.
If you want to style the root element itself, e.g. in the example above, you could use the :host modifier inside the components CSS:
this.root.innerHTML = `
<style>
:host {
border: 1px solid red;
padding: 5px;
}
span {
color: green;
}
</style>
<span>Hello, ${who}</span>
`;
You can also add external CSS files inside your component, e.g. to integrate a standard CSS framework (I’ve used Bulma) into it:
this.root.innerHTML = `
<link rel="stylesheet" type="text/css" href="./assets/css/bulma.min.css">
<link rel="stylesheet" type="text/css" href="./assets/css/styles.min.css">
<div class="card">
<header class="card-header">
<p class="card-header-title"> ...
`;
Just make sure the paths are correct.
Integrating 3rd party Web Components
There are various Open-Source Web Components available, a lot of them by really „big players“ like Google, SAP or Vaadin which you can integrate into your own Web Components project. Also checkout the Web Components.org website for a registry of various components.
Use the traditional way with NPM or YARN
While it is pretty straightforward to include 3rd party components via NPM or YARN (just check the README instructions for the components), I always had a bad feeling to have a 50 to 100MB node_modules folder sitting in my project directory with maybe thousands of external dependencies.
Use RollupJS to define your own bundles to include
So once I heard Adam Bien talking about using RollupJS to bundle external components into single JS files to integrate into a project, I was completely sold to that idea and used it in my Web Components project, too.
The idea is to have a really simplistic, plain HTML/JS/CSS project which you can run with e.g. BrowserSync (or any standard webserver like Apache or Nginx) without the need for a complex NodeJS / NPM / Webpack setup. Of course, if you don’t have any problems using NodeJS, NPM etc. with all those node_modules dependencies etc., you can skip the rest of this chapter and just go the „traditional“ way :) So, e.g. in a separate „build“ or „3rd-party“ folder or even a different (maybe shared?) project you can indeed install external libs via NPM (or, even just download the external release packages if you want to completely avoid using NPM) and then write a rollup.config.js file how to read the 3rd party lib files and put everything into a single output file which supports ES6 style imports to use that in your project. Then you can just copy that generated (and probably minified) file into your main project and use it directly. Of course, anytime you want to update the external component, you can (and have to) do so and re-bundle it with RollupJS. Here is an example for the Vaadin grid component, installed with NPM in a separate folder and processed with RollupJS:
import resolve from '@rollup/plugin-node-resolve';
import nodePolyfills from 'rollup-plugin-node-polyfills';
// use terser for minification of output files
import { terser } from "rollup-plugin-terser";
export default [{
input: 'node_modules/@vaadin/vaadin-grid/all-imports.js',
output: {
file: './dist/VaadinGrid.min.js',
format: 'esm',
name: 'vaadin-grid'
},
plugins: [
resolve({
jsnext: true
}),
nodePolyfills(),
terser()
]
}]
After that, you get a single, minified JS file with all dependencies „compiled“ into which you can copy into your project and import it directly, without even using NodeJS /NPM / YARN in your main project at all:
import './../lib/VaadinGrid.min.js';
...
this.root.innerHTML = `
<vaadin-grid theme="row-stripes" aria-label="Latest Results">
<vaadin-grid-column path="name"></vaadin-grid-column>
...
`;
If you use the RollupJS plugin-multi-entry plugin, you can even bundle more than one input file into one output file, e.g. to include more than one vendor component into your project. Here is an example for a „multi Vaadin bundle“ with the Grid and the DatePicker component plus LitHTML and MomentJS „bundle files“:
import resolve from '@rollup/plugin-node-resolve';
import nodePolyfills from 'rollup-plugin-node-polyfills';
import multi from '@rollup/plugin-multi-entry';
// use terser for minification of output files
import { terser } from "rollup-plugin-terser";
export default [{
input:
['node_modules/@vaadin/vaadin-grid/all-imports.js',
'node_modules/@vaadin/vaadin-date-picker/vaadin-date-picker.js'],
output: {
file: './dist/VaadinLibs.min.js',
format: 'esm',
name: 'vaadin-libs'
},
plugins: [
resolve({
jsnext: true
}),
nodePolyfills(),
terser()
]
}, {
input: 'node_modules/lit-html/lit-html.js',
output: {
file: './dist/lit-html.min.js',
format: 'esm',
name: 'lit'
},
plugins: [
resolve({
jsnext: true
}),
commonjs(),
nodePolyfills(),
terser()
]
}, {
input: 'node_modules/moment/moment.js',
output: {
file: './dist/moment.min.js',
format: 'esm',
name: 'moment'
},
plugins: [
resolve({
jsnext: true
}),
commonjs(),
nodePolyfills(),
terser()
]
}]
You can also use RollupJS to re-bundle external libs which are not yet ES6 ready and don’t provide exports you can use in your code. I used that approach to import MomentJS or CodeMirror into my project.
import moment from './lib/moment.min.js'
...
let monthYear = moment(item.execstarttime).format('MM/YYYY');
Tips and tricks for your components
Helpful libraries
LitHTML
LitHTML is a very small and lightweight Javascript template library you can use inside your components. It is one of the projects that was „outsourced“ from the Polymer project by Google. It is very efficient and supports partial DOM caching, variables, expressions and control structures and works in all major browsers.
Probably the two most-used funtions of LitHTML are „html“ and „render“:
import { html, render } from './lib/lit-html.min.js';
import './lib/VaadinGrid.min.js';
class MyGrid extends HTMLElement {
constructor() {
super();
this.headline = 'Hello, Lit!';
// create Shadow DOM to encapsulate all CSS etc!
this.root = this.attachShadow({mode: "open"});
}
// default component callback
connectedCallback() {
let me = this;
// use lit-html's html() and render()
const template = html`
<style>
.details {
display: flex;
}
</style>
<h1>${me.headline}</h1>
<vaadin-grid aria-label="A Grid">
<vaadin-grid-column path="name" header="Name"></vaadin-grid-column>
<vaadin-grid-column path="type" header="Type"></vaadin-grid-column>
</vaadin-grid>
<button @click=${_ => me.clicked()}>Click me!</button>
`;
// render to shadow dom!
render(template, me.root);
}
// LitHTML click handler
clicked() {
alert('You clicked me!');
}
}
customElements.define('my-grid', MyGrid);
LitHTML has a lot more features, though! An alternative is HyperHtml.
RollupJS
RollupJS helps not only to bundle your own project for production once it is finished, it also helps to „rollup“ or re-bundle external 3rd party Javascript scripts, libs or Web Components you want to integrate into your project.
We’ve already seen how to use RollupJS for re-bundling 3rd party libs, for packaging the finished project I’ve used a rollup.config.js like this:
import minifyHTML from 'rollup-plugin-minify-html-literals';
import visualizer from 'rollup-plugin-visualizer';
import minify from 'rollup-plugin-babel-minify';
import babel from 'rollup-plugin-babel';
import pkg from "./package.json";
import * as path from 'path';
const vaadinGrid = path.resolve('lib/VaadinGrid.min.js');
export default [
{
preserveModules: false,
// exclude Vaadin file from rollup
external: [vaadinGrid],
input: "js/index.js",
output: {
name: 'xv-app',
format: 'esm',
sourcemap: false,
dir: 'dist',
paths: {
vaadinGrid: './VaadinGrid.min.js'
},
},
plugins: [
minifyHTML(),
babel({
exclude: 'node_modules/**'
}),
minify({
banner: `/*!\n * ${pkg.name} ${pkg.version} \n * (c) 2020-present ${
pkg.author
} \n * Released under the ${pkg.license} License.\n */`,
bannerNewLine: true,
comments: false
}),
visualizer({
open: true
})
]
}
];
As you can see, I had to exclude the Vaadin Grid source from my bundle because I was getting errors when it was added to my dist JS file – that is one of the „gotchas“ I’ve encountered when not using NPM/YARN and instead bundle the 3rd party components myself with RollupJS.
What’s really cool though is the RollupJS Visualizer plugin, which generates a HTML report of the package content and file sizes etc.
Gotchas and possible problems
As I wrote earlier, if your project scope is to not only create single components, but to realize a bigger app or project with multiple Web Components, especially if you also choose to integrate 3rd party components, there may be some gotchas or shortcomings, or at least things to be aware of. I will show you some that I have encountered in my first Web Components projects.
Do not mix up too many 3rd party components
When using Web Components, you have all the freedom to choose and use any 3rd party Webcomponent you want, right? You can use e.g. the Vaadin Grid Component, the SAP UI5 Datepicker Component, the Prime Elements MegaMenu Component, etc. Well yes, you could do that, but you should keep in mind, although those components are indeed standalone components which can be combined quite easily, most of the time they will probably include other „base“ components or quite some bootstrap code for the general functionality as well as maybe lots of CSS definitions for the layout etc.
So my advice is to choose wisely and maybe stick with one or two Webcomponent „vendors“, e.g. if you need a grid view and a date picker, use both from the same vendor if possible. This will save you quite some kilobytes, because they probably share some base functionality and CSS classes.
Missing Features
Of course, there are some features you need to implement yourself or find some library that does the job for you. E.g. there is no two-way data binding as in Vue or Angular, there is no default router, etc. But most of the time, you will find a library that does just what you need. For basic data binding e.g., check out this article. I have also successfully used Form2JS to handle nested forms fields. For a simple router, check out this.
When not using a NodeJS / NPM / YARN setup
This point has really nothing to do with Web Components, but I really like the idea to have a „plain“, really straight-forward Javascript project, and Web Components make it easy to go that way.
But as enjoyable as it is to have a „KISS“ setup, with every component directly included as (mostly one) „plain“ JS and CSS file, without having maybe hundreds or even thousands of subfolders inside your famous „node_modules“ folder and without the need to handle advanced Webpack configurations or the like, of course there can be some gotchas or disadvantages for you.
Update routine
If you choose not to use NodeJS / NPM / YARN in your main project and instead „rollup“ the external components to import them manually, you may have to implement something to update those libraries, e.g. to get security fixes. You could e.g. set up a separate „libs“ project with NPM, update that, auto-run your rollup process and copy/deploy the generated files to your „plain“ project. Or you can just manually look for updates if you are only using a handful of external dependencies. Whatever you do, you should think about it before you choose to go that path.
RollupJS bundling hickups
As described in my rollup setup above, I had a problem with at first bundling the Vaadin Grid to use it in my project as an import, and then „re-bundle“ it for a minified release of the whole project. So I ended up adding it as a separate file to my project bundle in the end.
Older browsers
If you really need to support older browsers, e.g. the old Edge or even IE11, you have to do some extra setup using polyfills to get Web Components working and especially if you want to use Shadow DOM / Shadow CSS.
But overall, I still prefer to not have NodeJS / NPM / YARN / Webpack etc. in my project, but that’s just a personal preference :)
Conclusion
As you can see, it is really easy to get started with Web Components, you can literally use one HTML file and play with it in your browser. The API is simple and concise and there are plenty of resources and 3rd party components available to get started. So why learn yet another framework when you can go with the new standards instead?
Resources and references
Ben Farrell blog and book https://benfarrell.com/2019/09/22/why-web-components-now/ „Web Components in action“
SAP UI5 web components https://www.npmjs.com/package/@ui5/webcomponents
Vaadin Components https://vaadin.com/components
LitHTML reference https://lit-html.polymer-project.org/guide/template-reference
RollupJS https://rollupjs.org/guide/en/
RollupJS Node plugin https://github.com/rollup/plugins/tree/master/packages/node-resolve
RollupJS Visualizer https://www.npmjs.com/package/rollup-plugin-visualizer
Adam Bien W-Jax Workshop (German) https://youtu.be/4At08st9wlQ
Adam Bien’s blog http://www.adam-bien.com/roller/abien/
Styling WebComponents https://css-tricks.com/making-web-components-for-different-contexts/ and https://codepen.io/equinusocio/pen/vMOqBO
1-way Data Binding https://medium.com/swlh/https-medium-com-drmoerkerke-data-binding-for-web-components-in-just-a-few-lines-of-code-33f0a46943b3
A simple router https://github.com/filipbech/element-router
Refresh Fetch https://github.com/vlki/refresh-fetch
BrowserSync https://www.browsersync.io/
CSS Grid example https://vaadin.com/blog/responsive-design-made-easy-with-css-grid-and-web-components
Webstandards Reference https://developer.mozilla.org/de/
The Shadow DOM https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM
Web Components MDN https://developer.mozilla.org/de/docs/Web/Web_Components