Query Selector Shadow Dom

query-selector-shadow-dom

querySelector that can pierce Shadow DOM roots without knowing the path through nested shadow roots. Useful for automated testing of Web Components e.g. with Selenium, Puppeteer.

ko-fi


// available as an ES6 module for importing in Browser environments

import { querySelectorAllDeep, querySelectorDeep } from 'query-selector-shadow-dom';

What is a nested shadow root?

Image of Shadow DOM elements in dev tools You can see that .dropdown-item:not([hidden]) (Open downloads folder) is several layers deep in shadow roots, most tools will make you do something like

document.querySelector("body > downloads-manager").shadowRoot.querySelector("#toolbar").shadowRoot.querySelector(".dropdown-item:not([hidden])")

EW!

with query-selector-shadow-dom:

import { querySelectorAllDeep, querySelectorDeep } from 'query-selector-shadow-dom';
querySelectorDeep(".dropdown-item:not([hidden])");

API

  • querySelectorAllDeep - mirrors querySelectorAll from the browser, will return an Array of elements matching the query
  • querySelectorDeep - mirrors querySelector from the browser, will return the first matching element of the query.
  • collectAllElementsDeep - collects all elements on the page, including shadow dom

Both of the methods above accept a 2nd parameter, see section Provide alternative node. This will change the starting element to search from i.e. it will find ancestors of that node that match the query.

Known limitations

  • Source ordering of results may not be preserved. Due to the nature of how this library works, by breaking down selectors into parts, when using multiple selectors (e.g. split by commas) the results will be based on the order of the query, not the order the result appear in the dom. This is different from the native querySelectorAll functionality. You can read more about this here: https://github.com/Georgegriff/query-selector-shadow-dom/issues/54

Plugins

WebdriverIO

This plugin implements a custom selector strategy: https://webdriver.io/docs/selectors.html#custom-selector-strategies


// make sure you have selenium standalone running
const { remote } = require('webdriverio');
const { locatorStrategy } = require('query-selector-shadow-dom/plugins/webdriverio');

(async () => {
    const browser = await remote({
        logLevel: 'error',
        path: '/wd/hub',
        capabilities: {
            browserName: 'chrome'
        }
    })

    // The magic - registry custom strategy
    browser.addLocatorStrategy('shadow', locatorStrategy);


    // now you have a `shadow` custom locator.

    // All elements on the page
    await browser.waitUntil(() => browser.custom$("shadow", ".btn-in-shadow-dom"));
    const elements = await browser.$$("*");

    const elementsShadow = await browser.custom$$("shadow", "*");

    console.log("All Elements on Page Excluding Shadow Dom", elements.length);
    console.log("All Elements on Page Including Shadow Dom", elementsShadow.length);


    await browser.url('http://127.0.0.1:5500/test/')
    // find input element in shadow dom
    const input = await browser.custom$('shadow', '#type-to-input');
    // type to input ! Does not work in firefox, see above.
    await input.setValue('Typed text to input');
    // Firefox workaround
    // await browser.execute((input, val) => input.value = val, input, 'Typed text to input')

    await browser.deleteSession()
})().catch((e) => console.error(e))

How is this different to shadow$

shadow$ only goes one level deep in a shadow root.

Take this example. Image of Shadow DOM elements in dev tools You can see that .dropdown-item:not([hidden]) (Open downloads folder) is several layers deep in shadow roots, but this library will find it, shadow$ would not. You would have to construct a path via css or javascript all the way through to find the right element.

const { remote } = require('webdriverio')
const { locatorStrategy } = require('query-selector-shadow-dom/plugins/webdriverio');

(async () => {
    const browser = await remote({capabilities: {browserName: 'chrome'}})

    browser.addLocatorStrategy('shadow', locatorStrategy);

    await browser.url('chrome://downloads')
    const moreActions = await browser.custom$('shadow', '#moreActions');
    await moreActions.click();
    const span = await browser.custom$('shadow', '.dropdown-item:not([hidden])');
    const text = await span.getText()
    // prints `Open downloads folder`
    console.log(text);

    await browser.deleteSession()
})().catch((e) => console.error(e))

Known issues

https://webdriver.io/blog/2019/02/22/shadow-dom-support.html#browser-support

From the above, firefox setValue does NOT currently work. . A workaround for now is to use a custom command (or method on your component object) that sets the input field's value via browser.execute(function).

Safari pretty much doesn't work, not really a surprise.

There are some webdriver examples available in the examples folder of this repository. WebdriverIO examples

Puppeteer

Update: As of 5.4.0 Puppeteer now has a built in shadow Dom selector, this module might not be required for Puppeteer anymore. They don't have any documentation: https://github.com/puppeteer/puppeteer/pull/6509

There are some puppeteer examples available in the examples folder of this repository.

Puppeteer examples

Playwright

Update: as of Playwright v0.14.0 their CSS and text selectors work with shadow Dom out of the box, you don't need this library anymore for Playwright.

Playwright works really nicely with this package.

This module exposes a playwright selectorEngine: https://github.com/microsoft/playwright/blob/main/docs/api.md#selectorsregisterenginefunction-args

const { selectorEngine } = require("query-selector-shadow-dom/plugins/playwright");
const playwright = require('playwright');

 await selectors.register('shadow', createTagNameEngine);
...
  await page.goto('chrome://downloads');
  // shadow= allows a css query selector that automatically pierces shadow roots.
  await page.waitForSelector('shadow=#no-downloads span', {timeout: 3000})

For a full example see: https://github.com/Georgegriff/query-selector-shadow-dom/blob/main/examples/playwright

Protractor

This project provides a Protractor plugin, which can be enabled in your protractor.conf.js file:

exports.config = {
    plugins: [{
        package: 'query-selector-shadow-dom/plugins/protractor'
    }],
    
    // ... other Protractor-specific config
};

The plugin registers a new locator - by.shadowDomCss(selector /* string */), which can be used in regular Protractor tests:

element(by.shadowDomCss('#item-in-shadow-dom'))

The locator also works with Serenity/JS tests that use Protractor under the hood:

import 'query-selector-shadow-dom/plugins/protractor';
import { Target } from '@serenity-js/protractor'
import { by } from 'protractor';

const ElementOfInterest = Target.the('element of interest')
    .located(by.shadowDomCss('#item-in-shadow-dom'))

See the end-to-end tests for more examples.

Examples

Provide alternative node

    // query from another node
    querySelectorShadowDom.querySelectorAllDeep('child', document.querySelector('#startNode'));
    // query an iframe
    querySelectorShadowDom.querySelectorAllDeep('child', iframe.contentDocument);

This library does not allow you to query across iframe boundaries, you will need to get a reference to the iframe you want to interact with. 
If your iframe is inside of a shadow root you could cuse querySelectorDeep to find the iframe, then pass the contentDocument into the 2nd argument of querySelectorDeep or querySelectorAllDeep.

Chrome downloads page

In the below examples the components being searched for are nested within web components shadowRoots.


// Download and Paste the lib code in dist into chrome://downloads console to try it out :)

console.log(querySelectorShadowDom.querySelectorAllDeep('downloads-item:nth-child(4) #remove'));
console.log(querySelectorShadowDom.querySelectorAllDeep('#downloads-list .is-active a[href^="https://"]'));
console.log(querySelectorShadowDom.querySelectorDeep('#downloads-list div#title-area + a'));

Shady DOM

If using the polyfills and shady DOM, this library will still work.

Importing

  • Shipped as an ES6 module to be included using a bundler of your choice (or not).
  • ES5 version bundled ontop the window as window.querySelectorShadowDom available for easy include into a test framework

Running the code locally

npm install

Running the tests

npm test

Running the tests in watch mode

npm run watch

Running the build

npm run build

Author: Georgegriff
Source Code: https://github.com/Georgegriff/query-selector-shadow-dom 
License: MIT license

#node #javascript #webdriver #webcomponents 

Query Selector Shadow Dom
Vinnie  Erdman

Vinnie Erdman

1645463880

How to build a Web Application, todoMVC Using Rust & PostgreSQL (3/3)

Rust Programming Web Development Tutorial for building a simple Web Application, todoMVC, from scratch with WARP,  SQLX, Database (PostgreSQL), and Native Web Components (#FrameworkLess, #RealDOM).

Part 1: https://morioh.com/p/686445388b9f?f=60d94d9586deb67e97e9ce27
Part 2: https://morioh.com/p/a63fd628de3a?f=60d94d9586deb67e97e9ce27

GitHub Source - https://github.com/jeremychone-channel/rust-todomvc

00:00 - Intro & Recap
00:58 - Build Setup
03:08 - First main.css
04:45 - First todo-mvc Native Web Components
07:00 - todo-input component
08:43 - First todo-item component
12:41 - First Todo type and todoItem set data
16:27 - First Todo request to server
19:13 - CSS file
23:05 - Todo update
28:43 - Todo Create

#rust  #webdevelopement  #sqlx  #postgresql  #webcomponents #html #typescript #database 

How to build a Web Application, todoMVC Using Rust & PostgreSQL (3/3)
Vinnie  Erdman

Vinnie Erdman

1645449420

How to Build A Web Application, TodoMVC using Rust & PostgreSQL (2/3)

Rust Programming Web Development Tutorial for building a simple Web Application, todoMVC, from scratch with WARP, SQLX, Database (PostgreSQL), and Native Web Components.

Part 1: https://morioh.com/p/686445388b9f?f=60d94d9586deb67e97e9ce27
Part 3: https://morioh.com/p/96449d803d67?f=60d94d9586deb67e97e9ce27

GitHub Source - https://github.com/jeremychone-channel/rust-todomvc

00:00 - Intro & Recap of Part 1
01:02 - First WARP filters and index.html
13:12 - First Todo Filter (todo list)
29:03 - WARP Error Model
31:10 - HTTP X-Auth-Token authentication
36:21 - WARP Error Recovery
41:49 - Finish todos REST filters
47:24 - Finish todos REST tests
51:47 - Add Filters to Web Server & Browser Demo
54:02 - What next, like, and subscribe

#rust  #webdevelopement  #sqlx  #postgresql  #webcomponents #html #typescript #database 

How to Build A Web Application, TodoMVC using Rust & PostgreSQL (2/3)
Vinnie  Erdman

Vinnie Erdman

1645434775

How to Build A Web Application, TodoMVC using Rust & PostgreSQL (1/3)

Rust Programming Web Development Tutorial for building a simple Web Application, todoMVC, from scratch with Database (PostgreSQL) and Native Web Components.

Part 2: https://morioh.com/p/a63fd628de3a?f=60d94d9586deb67e97e9ce27

Part 3: https://morioh.com/p/96449d803d67?f=60d94d9586deb67e97e9ce27

GitHub Source - https://github.com/jeremychone-channel/rust-todomvc

00:00 - Dev Setup
02:55 - Database creation and db_init()
19:45 - CRUD todo list
28:32 - CRUD todo create
35:25 - With SQL Builder (sqlb)
41:21 - First security infrastructure
45:09 - Finishing CRUD (get, update, delete)

#rust  #webdevelopement  #sqlx  #postgresql  #webcomponents #html #typescript #database 

How to Build A Web Application, TodoMVC using Rust & PostgreSQL (1/3)
Dylan  Iqbal

Dylan Iqbal

1645239000

Minze: A JavaScript Framework for Native Web Components

Minze

Dead-simple framework for shareable web components.

Minze (German shorthand for "Peppermint", pronounced /ˈmɪnt͡sə/) lets you rapidly build native web components.

It provides an intuitive abstraction layer around the web components API with its own fully typed JavaScript API. Including reactivity, lifecycle hooks, scoped styles, one-shot components registration, and more.

  1. You can create cross-framework component libraries or design systems and share them with your team or the world.
  2. You can add Minze to any web project and create components without even using any build tools.

Read the Docs to Learn More.

Features

  • 👶 Simple - Dive right in by scaffolding a project or using a CDN link.
  • ⚡ Fast - Tiny footprint ~2KB (minified and compressed).
  • 🚀 Modern - Based on the latest technologies around web components.
  • 📦 Shareable - Build component libraries or design systems. Define once, use everywhere.
  • 🎲 Framework Agnostic - Use Minze with any common framework - React, Vue, Angular ...
  • 🔒 Typed API - Scale your component library with ease by using TypeScript.

Packages

ProjectVersionDescription
minzeminze versionDead-simple framework for shareable web components.
create-minzecreate-minze versionScaffolding CLI tool for setting up a Minze Dev and Publishing environment.
@minzejs/elementsminze-elements versionNative web components built with Minze.
playgroundplayground privateInternal playground environment for Minze.
teststests privateInternal test environment for Minze.

Contribution

See Contributing Guide.

Download Details: 
Author: n6ai
Source Code: https://github.com/n6ai/minze 
License: MIT
#typescript #webcomponents #javascript #minze

Minze: A JavaScript Framework for Native Web Components
Reid  Rohan

Reid Rohan

1642560120

Riot: React-like Library, But with Very Small Size

Simple and elegant component-based UI library 

Custom components • Concise syntax • Simple API • Tiny Size

Riot brings custom components to all modern browsers. It is designed to offer you everything you wished the native web components API provided.

Tag definition

<timer>
  <p>Seconds Elapsed: { state.time }</p>

  <script>
    export default {
      tick() {
        this.update({ time: ++this.state.time })
      },
      onBeforeMount(props) {
        // create the component initial state
        this.state = {
          time: props.start
        }

        this.timer = setInterval(this.tick, 1000)
      },
      onUnmounted() {
        clearInterval(this.timer)
      }
    }
  </script>
</timer>

Open this example on Plunker

Mounting

// mount the timer with its initial props
riot.mount('timer', { start: 0 })

Nesting

Custom components let you build complex views with HTML.

<timetable>
  <timer start="0"></timer>
  <timer start="10"></timer>
  <timer start="20"></timer>
</timetable>

HTML syntax is the de facto language on the web and it's designed for building user interfaces. The syntax is explicit, nesting is inherent to the language and attributes offer a clean way to provide options for custom tags.

Performant and predictable

  • Absolutely the smallest possible amount of DOM updates and reflows.
  • Fast expressions bindings instead of virtual DOM memory performance issues and drawbacks.
  • One way data flow: updates and unmounts are propagated downwards from parent to children.
  • No "magic" or "smart" reactive properties or hooks
  • Expressions are pre-compiled and cached for high performance.
  • Lifecycle methods for more control.

Close to standards

  • No proprietary event system.
  • Future proof thanks to the javascript module syntax.
  • The rendered DOM can be freely manipulated with other tools.
  • No extra HTML root elements, data- attributes or fancy custom attributes.
  • No new syntax to learn.
  • Plays well with any frontend framework.

Use your dearest language and tools

Powerful and modular ecosystem

The Riot.js ecosystem is completely modular, it's designed to let you pick only the stuff you really need:

CDN hosting

How to contribute

If you are reading this it's already a good sign and I am thankful for it! I try my best working as much as I can on riot but your help is always appreciated.

If you want to contribute to riot helping the project maintenance please check first the list of open issues to understand whether there is a task where you could help.

Riot is mainly developed on UNIX systems so you will be able to run all the commands necessary to build and test the library using our Makefile. If you are on a Microsoft machine it could be harder to set up your development environment properly.

Following the steps below you should be able to properly submit your patch to the project

1) Clone the repo and browse to the riot folder

$ git clone git@github.com:riot/riot.git && cd riot

2) Set up your git branch

$ git checkout -b feature/my-awesome-patch

3) Install the npm dependencies

$ npm i

4) Build and test riot using the Makefile

# To build and test riot
$ make riot

# To build without testing
$ make raw

5) Pull request only against the dev branch making sure you have read our pull request template

6) Be patient

Credits

Riot is actively maintained with :heart: by:

 
Gianluca Guarini

Many thanks to all smart people from all over the world who helped improving it.

Official Website

https://riot.js.org

Author: Riot
Source Code: https://github.com/riot/riot 
License: View license

#javascript #frameworks #webcomponents 

Riot: React-like Library, But with Very Small Size
Jade Bird

Jade Bird

1640334017

An Introduction to Web Components

Let's talk about Web Components

Before the dawn of some of the most popular frameworks (read: React and Vue), there was Web components. Web Components take one of the best parts of these frameworks (reusable components) and combine it with the best parts of web development (native browser support and not needing to set up a build process). As if that's not enough, web components allow you use the same functions across any framework. If at this point, you're wondering "If web components are so awesome, why haven't I heard about them before?", then you're in luck because that's exactly what this talk is about. In this presentation, we'll take a look at what web components are, why web components are awesome, why web components can be a pain and how we can use web components both as a standalone tool and together with frameworks.

#webcomponents #webdev 

An Introduction to Web Components
Web  Dev

Web Dev

1633570297

An Introduction to Web Components

What are web components?

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 build on the Web Component standards, will work across modern browsers, and can be used with any JavaScript library or framework that works with HTML.

Web components are based on existing web standards. Features to support web components are currently being added to the HTML and DOM specs, letting web developers easily extend HTML with new elements with encapsulated styling and custom behavior.

Specifications

Web components are based on four main specifications:

Custom Elements

The Custom Elements specification lays the foundation for designing and using new types of DOM elements.

Shadow DOM

The shadow DOM specification defines how to use encapsulated style and markup in web components.

ES Modules

The ES Modules specification defines the inclusion and reuse of JS documents in a standards based, modular, performant way.

HTML Template

The HTML template element specification defines how to declare fragments of markup that go unused at page load, but can be instantiated later on at runtime.

How do I use a web component?

The components on this site provide new HTML elements that you can use in your web pages and web applications.

Using a custom element is as simple as importing it, and using the new tags in an HTML document. For example, to use the paper-button element:

<script type="module" src="node_modules/@polymer/paper-button/paper-button.js"></script>
...
<paper-button raised class="indigo">raised</paper-button>

There are a number of ways to install custom elements. When you find an element you want to use, look at its README for the commands to install it. Most elements today can be installed with NPM. NPM also handles installing the components' dependencies. For more information on NPM, see npmjs.com.

For example, the paper-button overview describes the install process with npm:

mkdir my-new-app && cd my-new-app
npm install --save @polymer/paper-button

How do I define a new HTML element?

This section describes the syntax for the cross-browser version of the Web Components specification.

Use JavaScript to define a new HTML element and its tag with the customElements global. Call customElements.define() with the tag name you want to create and a JavaScript class that extends the base HTMLElement.

For example, to define a mobile drawer panel, <app-drawer>:

class AppDrawer extends HTMLElement {...} window.customElements.define('app-drawer', AppDrawer);

To use the new tag:

<app-drawer></app-drawer>

Using a custom element is no different to using a <div> or any other element. Instances can be declared on the page, created dynamically in JavaScript, event listeners can be attached, etc.

<script>
// Create with javascript
var newDrawer = document.createElement('app-drawer');
// Add it to the page
document.body.appendChild(newDrawer);
// Attach event listeners
document.querySelector('app-drawer').addEventListener('open', function() {...});
</script>

Creating and using a shadow root

This section describes the syntax for creating shadow DOM with the new cross-browser version (v1) of the shadow DOM specification. Shadow DOM is a new DOM feature that helps you build components. You can think of shadow DOM as a scoped subtree inside your element.

A shadow root is a document fragment that gets attached to a "host" element. The act of attaching a shadow root is how the element gains its shadow DOM. To create shadow DOM for an element, call element.attachShadow():

const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().
// header.shadowRoot === shadowRoot
// shadowRoot.host === header

Libraries for building web components

Many libraries already exist that make it easier to build web components. The libraries section of the site has additional details but here are some you can try out:

  • Hybrids is a UI library for creating Web Components with simple and functional API.
  • LitElement uses lit-html to render into the element's Shadow DOM and adds API to help manage element properties and attributes.
  • Polymer provides a set of features for creating custom elements.
  • Slim.js is an opensource lightweight web component library that provides data-binding and extended capabilities for components, using es6 native class inheritance.
  • Stencil is an opensource compiler that generates standards-compliant web components.

Original article source at https://www.webcomponents.org

#webcomponents #webdev 

An Introduction to Web Components
Brook  Hudson

Brook Hudson

1631325600

How to Create A Modal Component with Stencil.js

Hey everyone! If you haven't seen "Getting started with stencil" then be sure to check that one our before watching this video. Hope you enjoy!

0:00 - Intro
1:10 - Generating Component
1:50 - Coding JSX
5:25 - Coding CSS
12:00 - Toggling Modal
15:20 - Passing Props
16:30 - Working with Arrays & Objects
20:30 - Working with Events

 #webcomponents 

How to Create A Modal Component with Stencil.js
Noah Saunders

Noah Saunders

1630544992

What Are Web Components?

Welcome to Web Components for Beginners! 

In this video, we will take a first look at what exactly web components are. 

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 build on the Web Component standards, will work across modern browsers, and can be used with any JavaScript library or framework that works with HTML.

Web components are based on existing web standards. Features to support web components are currently being added to the HTML and DOM specs, letting web developers easily extend HTML with new elements with encapsulated styling and custom behavior.

Specifications

Web components are based on four main specifications:

Custom Elements

The Custom Elements specification lays the foundation for designing and using new types of DOM elements.

Shadow DOM

The shadow DOM specification defines how to use encapsulated style and markup in web components.

ES Modules

The ES Modules specification defines the inclusion and reuse of JS documents in a standards based, modular, performant way.

HTML Template

The HTML template element specification defines how to declare fragments of markup that go unused at page load, but can be instantiated later on at runtime.

How do I use a web component?

The components on this site provide new HTML elements that you can use in your web pages and web applications.

Using a custom element is as simple as importing it, and using the new tags in an HTML document. For example, to use the paper-button element:

<script type="module" src="node_modules/@polymer/paper-button/paper-button.js"></script>
...
<paper-button raised class="indigo">raised</paper-button>

There are a number of ways to install custom elements. When you find an element you want to use, look at its README for the commands to install it. Most elements today can be installed with NPM. NPM also handles installing the components' dependencies. For more information on NPM, see npmjs.com.

For example, the paper-button overview describes the install process with npm:

mkdir my-new-app && cd my-new-app
npm install --save @polymer/paper-button

How do I define a new HTML element?

This section describes the syntax for the cross-browser version of the Web Components specification.

Use JavaScript to define a new HTML element and its tag with the customElements global. Call customElements.define() with the tag name you want to create and a JavaScript class that extends the base HTMLElement.

For example, to define a mobile drawer panel, <app-drawer>:

class AppDrawer extends HTMLElement {...}
window.customElements.define('app-drawer', AppDrawer);

To use the new tag:

<app-drawer></app-drawer>

Using a custom element is no different to using a <div> or any other element. Instances can be declared on the page, created dynamically in JavaScript, event listeners can be attached, etc.

<script>
// Create with javascript
var newDrawer = document.createElement('app-drawer');
// Add it to the page
document.body.appendChild(newDrawer);
// Attach event listeners
document.querySelector('app-drawer').addEventListener('open', function() {...});
</script>

Creating and using a shadow root

This section describes the syntax for creating shadow DOM with the new cross-browser version (v1) of the shadow DOM specification. Shadow DOM is a new DOM feature that helps you build components. You can think of shadow DOM as a scoped subtree inside your element.

A shadow root is a document fragment that gets attached to a "host" element. The act of attaching a shadow root is how the element gains its shadow DOM. To create shadow DOM for an element, call element.attachShadow():

const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().
// header.shadowRoot === shadowRoot
// shadowRoot.host === header

Libraries for building web components

Many libraries already exist that make it easier to build web components. The libraries section of the site has additional details but here are some you can try out:

  • Hybrids is a UI library for creating Web Components with simple and functional API.
  • LitElement uses lit-html to render into the element's Shadow DOM and adds API to help manage element properties and attributes.
  • Polymer provides a set of features for creating custom elements.
  • Slim.js is an opensource lightweight web component library that provides data-binding and extended capabilities for components, using es6 native class inheritance.
  • Stencil is an opensource compiler that generates standards-compliant web components.

#webdev #webcomponents 

What Are Web Components?
Lindsey  Koepp

Lindsey Koepp

1599296400

CSS Shadow | Override CSS in a Shadow Dom/Web Component

One of the main purposes of Web Components is to provide encapsulation — being able keeping the markup structure and style hidden and separate from other code on the page so that different parts do not clash; this way the code can be kept nice and clean.

Shadow DOM gives us scoped style encapsulation and a means to let in as much (or as little) of the outside world as we choose.

But what if I want to make my component customizable for some style properties?

This article covers the basics of using CSS Custom Properties to penetrate the Shadow DOM and let your web component be customizable.

Creating an HTML Element

We will create our custom HTML element using a JavaScript class that extends the base HTMLElement. Then we will call customElements.define()with the tag name we want to create and the class that we just created.

class AppCard extends HTMLElement {...}

	window.customElements.define('app-card', AppCard);

In this example, we will create this simple Material Design card. It will be shown when we add this element on an HTML: <app-card></app-card>

First, we create the Shadow DOM root, and then we assign the HTML and CSS string to the Shadow DOM root innerHTML, as shown below.

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

	    const shadowRoot = this.attachShadow({mode: 'open'});

	    shadowRoot.innerHTML = `
	      <style>
	        .card {
	          background-color: #fff;
	          ...
	        }
	      </style>
	      <div class="card">
	        <div>Card title</div>
	      </div>
	    `;
	  }
	}

	window.customElements.define('app-card', AppCard);

Override Attempt

In this example, we want to modify the background color of the card. If it was a simple div element in your HTML, you could override the card class or via CSS selectors to access the div element. But the following attempts won’t work:

// access the div 
app-card > div {
  background-color: #2196F3;
}
// override card class
app-card > .card {
  background-color: #2196F3;
}

Using CSS Custom Properties

To solve this issue we can use the CSS Custom Properties (CSS Variables). The CSS Custom Property defined in your CSS can be used in order to change some CSS property in your custom element.

Following our example, we will use the variable card-bg on the property background-color to get the color defined by who is using the custom element.

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

    const shadowRoot = this.attachShadow({mode: 'open'});
    
    shadowRoot.innerHTML = `
      <style>
        .card {
          background-color: var(--card-bg, #fff);
          ...
        }
      </style>
      <div class="card">
        <div>Card title</div>
      </div>
    `;
  }
}

window.customElements.define('app-card', AppCard);

Now we will use the app-card custom element and create the card-bg variable in the CSS of the Body element. We will assign the hex color #2196F3 to the variable.

<html>
  <head>
    <style>
      body {
        --card-bg: #2196F3;
      }
    </style>
  </head>
  <body>
    <app-card></app-card>
  </body>
</html>

 

Conclusion

Using this strategy we can have an encapsulated CSS element in your document, and at the same time we can allow some properties to be customizable using CSS. You can access a complete example here.

#webcomponents #css #programming

CSS Shadow | Override CSS in a Shadow Dom/Web Component
Liam Hurst

Liam Hurst

1558537150

Web Components Tutorial: Go from Zero to Hero

Web Components is a suite of different technologies allowing you to create reusable custom elements — with their functionality encapsulated away from the rest of your code — and utilize them in your web apps.

An introduction to writing raw web components

  • What are web components?
  • A Components lifecycle
  • Building our to-do app
  • Setting properties
  • Setting attributes
  • Reflecting properties to attributes
  • Events
  • Browser support and polyfills
  • Wrapping up

Web components are getting more and more traction. With the Edge team’s recent announcement of implementing Custom Elements and Shadow DOM, all major browsers will soon support web components natively. Companies like Github, Netflix, Youtube and ING are even already using web components in production. Neat! However, surprisingly enough, none of those huge, succesful companies have implemented a (you guessed it) to-do app!

So today, we’ll be making a to-do app, because the world doesn’t have enough implementations of to-do apps yet. You can take a look at what we’ll be making here.

Before we start, I’d like to add a little disclaimer that this blogpost is intended to get a better grasp of the basics of web components. Web components are low level, and should probably not be used to write full blown applications without the use of any helper libraries, nor should they be compared to full blown frameworks.

What are web components?

  • Make a demo
  • The boring stuff
  • Setting properties
  • Setting attributes
  • Reflecting properties to attributes
  • Events
  • Wrap it up

First things first: Web components are a set of standards that allow us to write modular, reusable and encapsulated HTML elements. And the best thing about them: since they’re based on web standards, we don’t have to install any framework or library to start using them. You can start writing web components using vanilla javascript, right now!

But before we start getting our hands dirty, lets take a look at the specifications that let us write web components.

 

Custom Elements

The Custom Elements api allows us to author our own DOM elements. Using the api, we can define a custom element, and inform the parser how to properly construct that element and how elements of that class should react to changes. Have you ever wanted your own HTML element, like <my-cool-element>? Now you can!

 

Shadow DOM

Shadow DOM gives us a way to encapsulate the styling and markup of our components. It’s a sub DOM tree attached to a DOM element, to make sure none of our styling leaks out, or gets overwritten by any external styles. This makes it great for modularity.

 

ES Modules

The ES Modules specification defines the inclusion and reuse of JS documents in a standards based, modular, performant way.

 

HTML Templates

The HTML <a href="https://html.spec.whatwg.org/multipage/scripting.html#the-template-element/" target="_blank"><template></a> tag allows us to write reusable chunks of DOM. Inside a template, scripts don’t run, images don’t load, and styling/mark up is not rendered. A template tag itself is not even considered to be in the document, until it’s activated. HTML templates are great, because for every instance of our element, only 1 template is used.

Now that we know which specifications web components leverage, let’s take a look at a custom element’s lifecycle. I know, I know, we’ll get to the code soon!

A component’s lifecycle

Let’s take a look at a custom element’s lifecycle. Consider the following element:

class MyElement extends HTMLElement {
    constructor() {
        // always call super() first
        super(); 
        console.log('constructed!');
    }
	
    connectedCallback() {
        console.log('connected!');
    }
	
    disconnectedCallback() {
        console.log('disconnected!');
    }
	
    attributeChangedCallback(name, oldVal, newVal) {
        console.log(`Attribute: ${name} changed!`);
    }
	
    adoptedCallback() {
        console.log('adopted!');
    }
}

window.customElements.define('my-element', MyElement);

 

constructor()

The constructor runs whenever an element is created, but before the element is attached to the document. We’ll use the constructor for setting some initial state, event listeners, and creating the shadow DOM.

connectedCallback()

The connectedCallback is called when the element is inserted to the DOM. It’s a good place to run setup code, like fetching data, or setting default attributes.

disconnectedCallback()

The disconnectedCallback is called whenever the element is removed from the DOM. Clean up time! We can use the disconnectedCallback to remove any event listeners, or cancel intervals.

attributeChangedCallback(name, oldValue, newValue)

The attributeChangedCallback is called any time your element’s observed attributes change. We can observe an element’s attributes by implementing a static observedAttributes getter, like so:

static get observedAttributes() {
    return ['my-attr'];
}

In this case, any time the my-attr attribute is changed, the attributeChangedCallback will run. We’ll go more in-depth on this later this blog post.

Hey! Listen!> Only attributes listed in the observedAttributes getter are affected in the attributeChangedCallback.### adoptedCallback()

The adoptedCallback is called each time the custom element is moved to a new document. You’ll only run into this use case when you have <iframe> elements in your page.

registering our element

And finally, though not part of the lifecycle, we register our element to the CustomElementRegistry like so:

window.customElements.define('my-element', MyElement);

The CustomElementRegistry is an interface that provides methods for registering custom elements and querying registered elements. The first argument of the registries’ define method will be the name of the element, so in this case it’ll register <my-element>, and the second argument passes the class we made.

Hey! Listen!> It’s important to note how we name our web components. Custom element names must always contain a hyphen. For example: <my-element> is correct, and <myelement> is not. This is done deliberately to avoid clashing element names, and to create a distinction between custom elements and regular elements.> Custom elements also cannot be self-closing because HTML only allows a few elements to be self-closing. These are called void elements, like <br/> or <img/>, or elements that don’t allow children nodes.> Allowing self-closing elements would require a change in the HTML parser, which is a problem since HTML parsing is security sensitive. HTML producers need to be able to rely on how a given piece of HTML parses in order to be able to implement XSS-safe HTML generation.##

Building our to do app

  • Make a demo
  • The boring stuff
  • Setting properties
  • Setting attributes
  • Reflecting properties to attributes
  • Events
  • Wrap it up

Now that we’re done with all the boring stuff, we can finally get our hands dirty and start building our to do app! Click here to see the end result.

Let’s start with an overview of what we’re going to build.

  • A <to-do-app> element:
    Contains an array of to-do’s as propertyAdds a to-doRemoves a to-doToggles a to-do* A <to-do-item> element:
    Contains a description attributeContains an index attributeContains a checked attribute
    Great! Let’s lay out the groundwork for our to-do-app:

to-do-app.js:

const template = document.createElement('template');
template.innerHTML = `
<style>
    :host {
	display: block;
	font-family: sans-serif;
	text-align: center;
    }

    button {
	border: none;
	cursor: pointer;
    }

    ul {
	list-style: none;
	padding: 0;
    }
</style>
<h1>To do</h1>

<input type="text" placeholder="Add a new to do"></input>
<button>✅</button>

<ul id="todos"></ul>
`;

class TodoApp extends HTMLElement {
    constructor() {
        super();
        this._shadowRoot = this.attachShadow({ 'mode': 'open' });
        this._shadowRoot.appendChild(template.content.cloneNode(true));
        this.$todoList = this._shadowRoot.querySelector('ul');
    }
}

window.customElements.define('to-do-app', TodoApp);

We’re going to take this step by step. We first create a <template> by calling const template = document.createElement('template');, and then we set some HTML in it. We only set the innerHTML on the template once. The reason we’re using a template is because cloning templates is much cheaper than calling .innerHTML for all instances of our component.

Next up, we can actually start defining our element. We’ll use our constructor to attach our shadowroot, and we’ll set it to open mode. Then we’ll clone our template to our shadowroot. Cool! We’ve now already made use of 2 web components specifications, and succesfully made an encapsulated sub DOM tree.

What this means is that we now have a DOM tree that will not leak any styles, or get any styles overwritten. Consider the following example:

We have a global h1 styling that makes any h1 in the light DOM a red color. But because we have our h1 in a shadow-root, it does not get overwritten by the global style.

Note how in our to-do-app component, we’ve used a :host pseudo class, this is how we can add styling to the component from the inside. An important thing to note is that the display is always set to display: inline;, which means you can’t set a width or height on your element. So make sure to set a :host display style (e.g. block, inline-block, flex) unless you prefer the default of inline.

Hey! Listen!> Shadow DOM can be a little confusing. Allow me to expand a little bit on terminology:###

Light DOM:

The light DOM lives outside the component’s shadow DOM, and is basically anything that is not shadow DOM. For example, the <h1>Hello world</h1> up there lives in the light DOM. The term light DOM is used to distinguish it from the Shadow DOM. It’s perfectly fine to make web components using light DOM, but you miss out on the great features of shadow DOM.

Open shadow DOM:

Since the latest version (V1) of the shadow DOM specification, we can now use open or closed shadow DOM. Open shadow DOM allows us to create a sub DOM tree next to the light DOM to provide encapsulation for our components. Our shadow DOM can still be pierced by javascript like so: document.querySelector('our-element').shadowRoot. One of the downsides of shadow DOM is that web components are still relatively young, and many external libraries don’t account for it.

Closed shadow DOM:

Closed shadow roots are not very applicable, as it prevents any external javascript from piercing the shadowroot. Closed shadow DOM makes your component less flexible for yourself and your end users and should generally be avoided.

Some examples of elements that do use a closed shadow DOM are the <video> element.

Setting properties

Cool. We’ve made our first web component, but as of now, it’s absolutely useless. It would be nice to be able to pass some data to it and render a list of to-do’s.

Let’s implement some getters and setters.

to-do-app.js:

class TodoApp extends HTMLElement {
    ...
 		
    _renderTodoList() {
        this.$todoList.innerHTML = '';

        this._todos.forEach((todo, index) => {
            let $todoItem = document.createElement('div');
            $todoItem.innerHTML = todo.text; 
            this.$todoList.appendChild($todoItem);
        });
    }

    set todos(value) {
        this._todos = value;
        this._renderTodoList();
    }

    get todos() {
        return this._todos;
    }
}

Now that we have some getters and setters, we can pass some rich data to our element! We can query for our component and set the data like so:

document.querySelector('to-do-app').todos = [
    {text: "Make a to-do list", checked: false}, 
    {text: "Finish blog post", checked: false}
];

We’ve now succesfully set some properties on our component, and it should currently look like this:

Great! Except it’s still useless because we cannot interact with anything without using the console. Let’s quickly implement some functionality to add new to-do’s to our list.

class TodoApp extends HTMLElement {
    ...

    constructor() {
        super();
        this._shadowRoot = this.attachShadow({ 'mode': 'open' });
        this._shadowRoot.appendChild(template.content.cloneNode(true));

        this.$todoList = this._shadowRoot.querySelector('ul');
        this.$input = this._shadowRoot.querySelector('input');

        this.$submitButton = this._shadowRoot.querySelector('button');
        this.$submitButton.addEventListener('click', this._addTodo.bind(this));
    }

    _addTodo() {
        if(this.$input.value.length > 0){
            this._todos.push({ text: this.$input.value, checked: false })
            this._renderTodoList();
            this.$input.value = '';
        }
    }

    ...
}

This should be easy enough to follow, we set up some querySelectors and addEventListeners in our constructor, and on a click event we want to push the input to our to-do’s list, render it, and clear the input again. Ez .

Setting attributes

  • Make a demo
  • The boring stuff
  • Setting properties
  • Setting attributes
  • Reflecting properties to attributes
  • Events
  • Wrap it up

This is where things will get confusing, as we’ll be exploring the differences between attributes and properties, and we’ll also be reflecting properties to attributes. Hold on tight!

First, let’s create a <to-do-item> element.

to-do-item.js:

const template = document.createElement('template');
template.innerHTML = `
<style>
    :host {
	display: block;
	font-family: sans-serif;
    }

    .completed {
	text-decoration: line-through;
    }

    button {
	border: none;
	cursor: pointer;
    }
</style>
<li class="item">
    <input type="checkbox">
    <label></label>
    <button>❌</button>
</li>
`;

class TodoItem extends HTMLElement {
    constructor() {
        super();
        this._shadowRoot = this.attachShadow({ 'mode': 'open' });
        this._shadowRoot.appendChild(template.content.cloneNode(true));

        this.$item = this._shadowRoot.querySelector('.item');
        this.$removeButton = this._shadowRoot.querySelector('button');
        this.$text = this._shadowRoot.querySelector('label');
        this.$checkbox = this._shadowRoot.querySelector('input');

        this.$removeButton.addEventListener('click', (e) => {
            this.dispatchEvent(new CustomEvent('onRemove', { detail: this.index }));
        });

        this.$checkbox.addEventListener('click', (e) => {
            this.dispatchEvent(new CustomEvent('onToggle', { detail: this.index }));
        });
    }

    connectedCallback() {
    	// We set a default attribute here; if our end user hasn't provided one,
    	// our element will display a "placeholder" text instead.
        if(!this.hasAttribute('text')) {
            this.setAttribute('text', 'placeholder');
        }

        this._renderTodoItem();
    }

    _renderTodoItem() {
        if (this.hasAttribute('checked')) {
            this.$item.classList.add('completed');
            this.$checkbox.setAttribute('checked', '');
        } else {
            this.$item.classList.remove('completed');
            this.$checkbox.removeAttribute('checked');
        }
        
        this.$text.innerHTML = this._text;
    }

    static get observedAttributes() {
        return ['text'];
    }

    attributeChangedCallback(name, oldValue, newValue) {
        this._text = newValue;
    }
}
window.customElements.define('to-do-item', TodoItem);

Note that since we’re using a ES Modules, we’re able to use const template = document.createElement('template'); again, without overriding the previous template we made.
And lets change our _renderTodolist function in to-do-app.js to this:

class TodoApp extends HTMLElement {

    	...

        _renderTodoList() {
            this.$todoList.innerHTML = '';

            this._todos.forEach((todo, index) => {
                let $todoItem = document.createElement('to-do-item');
                $todoItem.setAttribute('text', todo.text);
                this.$todoList.appendChild($todoItem);
            });
        }
        
        ...
		 
    }

Alright, a lot of different stuff is going on here. Let’s dive in. Previously, when passing some rich data (an array) to our <to-do-app> component, we set it like this:

document.querySelector('to-do-app').todos = [{ ... }];

We did that, because todos is a property of the element. Attributes are handled differently, and don’t allow rich data, in fact, they only allow a String type as a limitation of HTML. Properties are more flexible and can handle complex data types like Objects or Arrays.

The difference is that attributes are defined on HTML elements. When the browser parses the HTML, a corresponding DOM node will be created. This node is an object, and therefore it has properties. For example, when the browser parses: <to-do-item index="1">, a HTMLElement object will be created. This object already contains several properties, such as children, clientHeight, classList, etc, as well as some methods like appendChild() or click(). We can also implement our own properties, like we did in our to-do-app element, which we gave a todos property.

Here’s an example of this in action.

<img src="myimg.png" alt="my image"/>

The browser will parse this <img> element, create a DOM Element object, and conveniently set the properties for src and alt for us. It should be mentioned that this property reflection is not true for all attributes. (Eg: the value attribute on an <input> element does not reflect. The value property of the <input> will always be the current text content of the <input>, and the value attribute will be the initial text content.) We’ll go deeper into reflecting properties to attributes shortly.

So we now know that the alt and src attributes are handled as String types, and that if we’d want to pass our array of to-do’s to our <to-do-app> element like this:

<to-do-app todos="[{...}, {...}]"></to-do-app>

We would not get the desired result; we’re expecting an array, but actually, the value is simply a String that looks like an array.

Hey! Listen!* Aim to only accept rich data (objects, arrays) as properties.

  • Do not reflect rich data properties to attributes.

Setting attributes works differently from properties as well, notice how we didn’t implement any getters or setters. We added our text attribute to the static get observedAttributes getter, to allow us to watch for changes on the text attribute. And we implemented the attributesChangedCallback to react to those changes.

Our app should look like this, at this moment in time:

 

Boolean attributes

We’re not done with attributes just yet. It would be nice to be able to check off some of our to-do’s when we’re done with them, and we’ll be using attributes for that as well. We have to handle our Boolean attributes a little differently though.

The presence of a boolean attribute on an element represents the True value, and the absence of the attribute represents the False value.> If the attribute is present, its value must either be the empty string or a value that is an ASCII case-insensitive match for the attribute’s canonical name, with no leading or trailing whitespace.> The values “true” and “false” are not allowed on boolean attributes. To represent a false value, the attribute has to be omitted altogether. <div hidden="true"> is incorrect.
This means that only the following examples are acceptable for a true value:

<div hidden></div>
<div hidden=""></div>
<div hidden="hidden"></div>

And one for false:

<div></div>

So let’s implement the checked attribute for our <to-do-item> element!

Change your to-do-app.js to this:

_renderTodoList() {
    this.$todoList.innerHTML = '';

    this._todos.forEach((todo, index) => {
        let $todoItem = document.createElement('to-do-item');
        $todoItem.setAttribute('text', todo.text);

	// if our to-do is checked, set the attribute, else; omit it.
        if(todo.checked) {
            $todoItem.setAttribute('checked', '');                
        }

        this.$todoList.appendChild($todoItem);
    });
}

And change to-do-item to this:

 class TodoItem extends HTMLElement {

    ...

    static get observedAttributes() {
        return ['text', 'checked'];
    }

    attributeChangedCallback(name, oldValue, newValue) {
        switch(name){
            case 'text':
                this._text = newValue;
                break;
            case 'checked':
                this._checked = this.hasAttribute('checked');
                break;
        }
    }

    ...

}

Nice! Our application should look like this:

 

Reflecting properties to attributes

  • Make a demo
  • The boring stuff
  • Setting properties
  • Setting attributes
  • Reflecting properties to attributes
  • Events
  • Wrap it up

Cool, our app is coming along nicely. But it would be nice if our end user would be able to query for the status of checked of our to-do-item component. We’ve currently set it only as an attribute, but we would like to have it available as a property as well. This is called reflecting properties to attributes.

All we have to do for this is add some getters and setters. Add the following to your to-do-item.js:

get checked() {
    return this.hasAttribute('checked');
}

set checked(val) {
    if (val) {
        this.setAttribute('checked', '');
    } else {
        this.removeAttribute('checked');
    }
}

Now, every time we change the property or the attribute, the value will always be in sync.

Events

  • Make a demo
  • The boring stuff
  • Setting properties
  • Setting attributes
  • Reflecting properties to attributes
  • Events
  • Wrap it up

Phew, now that we’re done with the hard bits, it’s time to get to the fun stuff. Our application currently handles and exposes the data in a way we want to, but it doesn’t actually remove or toggle the to-do’s yet. Let’s take care of that.

First, we’re going to have to keep track of the index of our to-do-items. Let’s set up an attribute!

to-do-item.js:

static get observedAttributes() {
    return ['text', 'checked', 'index'];
}

attributeChangedCallback(name, oldValue, newValue) {
    switch(name){
        case 'text':
            this._text = newValue;
            break;
        case 'checked':
            this._checked = this.hasAttribute('checked');
            break;
        case 'index':
            this._index = parseInt(newValue);
            break;
    }
}

Note how we’re parsing the String type value to an integer here, since attributes only allow a String type, but we’d like the end user to be able to get the index property as an integer. And we also now have a nice example of how to deal with string/number/boolean attributes and how to handle attributes and properties as their actual type.

So let’s add some getters and setters to to-do-item.js:

set index(val) {
    this.setAttribute('index', val);
}

get index() {
    return this._index;
}

And change our _renderTodoList function in to-do-app.js to:

_renderTodoList() {
    this.$todoList.innerHTML = '';

    this._todos.forEach((todo, index) => {
        let $todoItem = document.createElement('to-do-item');
        $todoItem.setAttribute('text', todo.text);

        if(todo.checked) {
            $todoItem.setAttribute('checked', '');                
	}

        $todoItem.setAttribute('index', index);
        
        $todoItem.addEventListener('onRemove', this._removeTodo.bind(this));

        this.$todoList.appendChild($todoItem);
    });
}

Note how we’re setting $todoItem.setAttribute('index', index);. We now have some state to keep track of the index of the to-do. We’ve also set up an event listener to listen for an onRemove event on the to-do-item element.

Next, we’ll have to fire the event when we click the remove button. Change the constructor of to-do-item.js to the following:

constructor() {
    super();
    this._shadowRoot = this.attachShadow({ 'mode': 'open' });
    this._shadowRoot.appendChild(template.content.cloneNode(true));

    this.$item = this._shadowRoot.querySelector('.item');
    this.$removeButton = this._shadowRoot.querySelector('button');
    this.$text = this._shadowRoot.querySelector('label');
    this.$checkbox = this._shadowRoot.querySelector('input');

    this.$removeButton.addEventListener('click', (e) => {
        this.dispatchEvent(new CustomEvent('onRemove', { detail: this.index }));
    });
}

Hey! Listen!> We can set { detail: this.index, composed: true, bubbles: true } to let the event bubble out of our components shadow DOM.
And add the _removeTodo function in to-do-app.js:

_removeTodo(e) {
    this._todos.splice(e.detail, 1);
    this._renderTodoList();
}

Sweet! We’re able to delete to-do’s:

And finally, let’s create a toggle functionality as well.

to-do-app.js:

class TodoApp extends HTMLElement {
    ...
   
    _toggleTodo(e) {
        const todo = this._todos[e.detail];
        this._todos[e.detail] = Object.assign({}, todo, {
            checked: !todo.checked
        });
        this._renderTodoList();
    }


    _renderTodoList() {
        this.$todoList.innerHTML = '';

        this._todos.forEach((todo, index) => {
            let $todoItem = document.createElement('to-do-item');
            $todoItem.setAttribute('text', todo.text);

            if(todo.checked) {
                $todoItem.setAttribute('checked', '');                
            }

            $todoItem.setAttribute('index', index);
            $todoItem.addEventListener('onRemove', this._removeTodo.bind(this));
            $todoItem.addEventListener('onToggle', this._toggleTodo.bind(this));

            this.$todoList.appendChild($todoItem);
        });
    }
	
    ...

}

And to-do-item.js:

class TodoItem extends HTMLElement {

    ...
	
    constructor() {
        super();
        this._shadowRoot = this.attachShadow({ 'mode': 'open' });
        this._shadowRoot.appendChild(template.content.cloneNode(true));

        this.$item = this._shadowRoot.querySelector('.item');
        this.$removeButton = this._shadowRoot.querySelector('button');
        this.$text = this._shadowRoot.querySelector('label');
        this.$checkbox = this._shadowRoot.querySelector('input');

        this.$removeButton.addEventListener('click', (e) => {
            this.dispatchEvent(new CustomEvent('onRemove', { detail: this.index }));
        });

        this.$checkbox.addEventListener('click', (e) => {
            this.dispatchEvent(new CustomEvent('onToggle', { detail: this.index }));
        });
    }
    
    ...

}

Success! We can create, delete, and toggle to-do’s!

Browser support and polyfills

  • Make a demo
  • The boring stuff
  • Setting properties
  • Setting attributes
  • Reflecting properties to attributes
  • Events
  • Wrap it up

The last thing I’d like to address in this blog post is browser support. At time of writing, the Microsoft Edge team has recently announced that they’ll be implementing custom elements as well as shadow DOM, meaning that all major browsers will soon natively support web components.

Until that time, you can make use of the webcomponentsjs polyfills, maintained by Google. Simply import the polyfill:

<script src="https://unpkg.com/@webcomponents/webcomponentsjs@2.0.0/webcomponents-bundle.js"></script>

I used unpkg for simplicity’s sake, but you can also install webcomponentsjs with NPM. To make sure the polyfills have succesfully loaded, we can wait for the WebComponentsReady event to be fired, like so:

window.addEventListener('WebComponentsReady', function() {
    console.log('Web components ready!');
    // your web components here
});

 

Wrapping up

  • Make a demo
  • The boring stuff
  • Setting properties
  • Setting attributes
  • Reflecting properties to attributes
  • Events
  • Wrap it up

Done!
If you’ve made it all the way down here, congratulations! You’ve learned about the web components specifications, (light/open/closed) shadow DOM, templates, the difference between attributes and properties, and reflecting properties to attributes.

But as you can probably tell, a lot of the code that we’ve written may feel a little clunky, we’ve written quite a lot of boiler plate (getters, setters, queryselectors, etc), and a lot of things have been handled imperatively. Our updates to the to do list aren’t very performant, either.

Web components are neat, but I don’t want to spend all this time writing boiler plate and setting stuff imperatively, I want to write declarative code!”, you cry.

Supercharging Web Components with lit-html

Supercharging web components

  • Lit-html
  • Lit-html in practice
  • Supercharging our component
  • Attributes, properties, and events
  • Wrapping up

You’ll know the basics of web components by now. If you haven’t, I suggest you go back to part one and catch up, because we’ll be revisiting, and building on top of a lot of the concepts

We’ll be supercharging our to-do application with a rendering library called lit-html. But before we dive in, there’s a couple of things we need to discuss. If you’ve paid close attention, you’ll have noticed that I referred to our web component as being a raw web component before. I did that, because web components are low level, and don’t include templating or other features by design. Web components were always intended as a collection of standards that do very specific things that the platform didn’t allow yet.

I’d like to quote Justin Fagnani by saying that all web components do is give the developer a when and a where. The when being element creation, instantiation, connecting, disconnecting, etc. The where being the element instance and the shadowroot. What you do with that is up to you.

Additionally, lit-html is not a framework. It’s simply a javascript library that leverages standard javascript language features. The difference between libraries and frameworks is often a controversial subject, but I’d like to define it as this analogy by Dave Cheney:

Frameworks are based on the Hollywood Pattern; don’t call us, we’ll call you.
Lit-html is also extremely lightweight at <2kb, and renders fast.

Now that we’ve got that out of the way, let’s see how lit-html works.

Lit-html

  • Learn about Lit-html
  • Lit-html in practice
  • Supercharge our web component
  • Attributes, properties, and events
  • Wrapping up

Lit-html is a rendering library that lets you write HTML templates with javascript template literals, and efficiently render and re-render those templates to DOM. Tagged template literals are a feature of ES6 that can span multiple lines, and contain javascript expressions. A tagged template literal could look something like this:

const planet = "world";

html`hello ${planet}!`;

Tagged template literals are just standard ES6 syntax. And these tags are actually just functions! Consider the following example:

function customFunction(strings) {
    console.log(strings); // ["Hello universe!"]
}

customFunction`Hello universe!`;

They can also handle expressions:

const planet = "world";

function customFunction(strings, ...values) {
    console.log(strings); // ["Hello ", "! five times two equals "]
    console.log(values); // ["world", 10]
}

customFunction`Hello ${planet}! five times two equals ${ 5 * 2 }`;

And if we look in the source code we can see this is exactly how lit-html works as well:

/**
 * Interprets a template literal as an HTML template that can efficiently
 * render to and update a container.
 */
export const html = (strings: TemplateStringsArray, ...values: any[]) =>
    new TemplateResult(strings, values, 'html', defaultTemplateProcessor);

Now if we’d write something like this:

const planet = "world";

function customFunction(strings, ...values) {
    console.log(strings); // ["<h1>some static content</h1><p>hello ", "</p><span>more static content</span>"]
    console.log(values); // ["world"]
}

customFunction`
	<h1>some static content</h1>
	<p>hello ${planet}</p>
	<span>more static content</span>	
`;

You’ll notice that when we log our strings and values to the console, we’ve already separated the static content of our template, and the dynamic parts. This is great when we want to keep track of changes, and update our template with the corresponding data, because it allows us to only watch the dynamic parts for changes. This is also a big difference with how VDOM works because we already know the <h1> and the <span> are static, so we don’t have to do anything with them. We’re only interested in the dynamic parts, which can be any javascript expression.

So lit-html takes your template, replaces all the expressions with generic placeholders called Parts, and makes a <template> element out of the result. So we now have a HTML template, that knows where it has to put any data it will receive.

<template>
	<h1>some static content</h1>
	<p>hello {{}}</p> <-- here's our placeholder, or `Part`
	<span>more static content</span>	
</template>

Lit remembers where these placeholders are, which allows for easy and efficient updates. Lit will also efficiently reuse <template>s:

const sayHello = (name) => html`
	<h1>Hello ${name}</h1>
`;

sayHello('world');
sayHello('universe');

Both these templates will share the exact same <template> for efficiency, the only thing that’s different is the data we’re passing it. And if you paid close attention, you’ll remember that we used the same technique in part one of this blog series.

The dynamic Parts of our template can be any javascript expression. Lit-html doesn’t even have to do any magic to evaluate our expressions, javascript just does this for us. Here are some examples:

Simple:

customFunction`<p>${1 + 1}</p>`; // 2

Conditionals:

customFunction`<p>${truthy ? 'yes' : 'no'}</p>`; // 'yes'

And we can even work with arrays and nesting:

customFunction`<ul>${arr.map(item => customFunction`<li>${item}</li>`)}</ul>`;

Lit-html in practice

  • Learn about Lit-html
  • Lit-html in practice
  • Supercharge our web component
  • Attributes, properties, and events
  • Wrapping up

So let’s see how this works in practice:

You can see the full demo here or on github.

import { html, render } from 'lit-html';

class DemoElement extends HTMLElement {
  constructor() {
    super();
    this._counter = 0;
    this._title = "Hello universe!";
    this.root = this.attachShadow({ mode: "open"});
    setInterval(() => {this.counter++}, 1000);
  }

  get counter() {
    return this._counter;
  }

  set counter(val) {
    this._counter = val;
    render(this.template(), this.root);
  }

  template() {
    return html`
      <p>Some static DOM</p>
      <h1>${this.counter}</h1>
      <h2>${this._title}</h2>
      <p>more static content</p>
    `;
  }
}

window.customElements.define('demo-element', DemoElement);

If you’ve read the first blog post in this series, this should look familiar. We’ve made a simple web component, that increments a counter every second, and we’ve implemented lit-html to take care of our rendering for us.

The interesting bits are here:

    return html`
      <p>Some static DOM</p>
      <h1>${this.counter}</h1>
      <h2>${this._title}</h2>
      <p>more static content</p>
    `;

And the ouput in the DOM:

We can now see how lit only updates the part of our code that has changed (this.counter), and doesn’t even bother with the static parts. And it does all this without any framework magic or VDOM, and at less than 2kb library size! You also might notice a bunch of HTML comments in the output; Fear not, this is how lit-html keeps track of where static and dynamic parts are.

Supercharging our component

  • Learn about Lit-html
  • Lit-html in practice
  • Supercharge our web component
  • Attributes, properties, and events
  • Wrapping up

Now that we know how lit-html renders, lets put it in practice. You can see the full code here and on github. We’ll be walking through this step by step, but lets get an overview of our supercharged component first:

to-do-app.js:

import { html, render } from 'lit-html';
import './to-do-item.js';

class TodoApp extends HTMLElement {
    constructor() {
        super();
        this._shadowRoot = this.attachShadow({ 'mode': 'open' });

        this.todos = [
	    { text: 'Learn about Lit-html', checked: true },
	    { text: 'Lit-html in practice', checked: false },
	    { text: 'Supercharge our web component', checked: false },
	    { text: 'Attributes, properties, and events', checked: false },
	    { text: 'Wrapping up', checked: false }
	];

        render(this.template(), this._shadowRoot, {eventContext: this});

        this.$input = this._shadowRoot.querySelector('input');
    }

    _removeTodo(e) {
      this.todos = this.todos.filter((todo,index) => {
          return index !== e.detail;
      });
    }

    _toggleTodo(e) {
      this.todos = this.todos.map((todo, index) => {
          return index === e.detail ? {...todo, checked: !todo.checked} : todo;
      });
    }

    _addTodo(e) {
      e.preventDefault();
      if(this.$input.value.length > 0) {
          this.todos = [...this.todos, { text: this.$input.value, checked: false }];
          this.$input.value = '';
      }
    }

    template() {
        return html`
            <style>
                :host {
                    display: block;
                    font-family: sans-serif;
                    text-align: center;
                }
                button {
                    border: none;
                    cursor: pointer;
                    background-color: Transparent;
                }
                ul {
                    list-style: none;
                    padding: 0;
                }
            </style>
            <h3>Raw web components + lit-html</h3>
            <br>
            <h1>To do</h1>
            <form id="todo-input">
                <input type="text" placeholder="Add a new to do"></input>
                <button @click=${this._addTodo}>✅</button>
            </form>
            <ul id="todos">
              ${this.todos.map((todo, index) => html`
                    <to-do-item 
                        ?checked=${todo.checked}
                        .index=${index}
                        text=${todo.text}
                        @onRemove=${this._removeTodo}
                        @onToggle=${this._toggleTodo}>    
                    </to-do-item>
                  `
              )}
            </ul>
        `;
    }

    set todos(value) {
        this._todos = value;
        render(this.template(), this._shadowRoot, {eventContext: this});
    }

    get todos() {
        return this._todos;
    }
}

window.customElements.define('to-do-app', TodoApp);

Got the general overview? Great! You’ll find quite a lot things have changed in our code, so let’s take a closer look.

The first thing you might have noticed is that the way we handled the rendering of our component has completely changed. In our old app we had to imperatively create a template element, set its innerHTML, clone it, and append it to our shadowroot. When we wanted to update our component, we had to create a bunch of elements, set their attributes, add their event listeners and append them to the DOM. All by hand. I’m getting a headache just reading that. What we’ve done instead is delegate all the rendering to lit-html.

Now we only declare our template once, we can set attributes, properties and events declaratively in the template, and just call lit-html’s render function when we need to. The great thing about lit-html is that it’s fast and efficient at rendering; It looks only at the dynamic expressions, and changes only what needs to be updated. And all this without the overhead of a framework!

You’ll also notice we changed our _addTodo, _removeTodo and _toggleTodo methods to some immutable update patterns instead. This is nice because every time we set the value of todos, we’ll trigger a render of our component. This is an important concept that we’ll explore more in the third and final part of this blog series.

Attributes, properties, and events

  • Learn about Lit-html
  • Lit-html in practice
  • Supercharge our web component
  • Attributes, properties, and events
  • Wrapping up

Let’s continue and take a look at how lit-html handles attributes, properties, and events.

${this.todos.map((todo, index) => {
    return html`
        <to-do-item 
            ?checked=${todo.checked}
            .index=${index}
            text=${todo.text}
            @onRemove=${this._removeTodo}
            @onToggle=${this._toggleTodo}>    
        </to-do-item>
    `;
})}

You might have seen this weird syntax in the updated version of our component, and wonder what it means. Lit-html allows us to declaratively set our attributes, properties and event handlers in our templates, as opposed to setting them imperatively. Since we learned all about attributes, properties and events in part one of this series, this should be easy enough to follow. If you need a refresher, I got you covered.

Let’s walk through all of this step by step.

Attributes

text=${todo.text}

We set attributes in lit-html… Exactly like you’d set an attribute in standard HTML. The only difference is the fact that we’re using a dynamic value in a template string. Very anticlimactic, I know. We previously had to set our attributes imperatively like this: el.setAttribute('text', todo.text);.

Hey! Listen!> Regular attributes are still only limited to String types!###

Boolean attributes

?checked=${todo.checked}

As you’ll remember from the last blog post, Boolean attributes are generally handled a bit differently…

Flashback

…> This means that only the following examples are acceptable for a true value:

<div hidden></div>
<div hidden=""></div>
<div hidden="hidden"></div>

And one for false:

<div></div>

Conveniently enough, lit-html allows us to easily specify our attribute as a Boolean attribute by prefixing the attribute name with a ?, and then makes sure the attribute is either present on the element, or not.

Previously we set our boolean attributes as:

if(todo.checked){
	el.setAttribute('checked', '');
}

and omitted it altogether when our conditional was falsy.

Properties

.index=${index}

If we want to pass down some rich data like arrays or objects, or in this case, a number value, we can simply use the dot prefix.

Previously, to set properties on our components, we had to imperatively query for the component, and set the property. Thanks to lit-html, we can handle all this in our template instead.

Previously we set properties as:

el.index = index;

Events

@onRemove=${this._removeTodo}

And finally, we can declaratively specify our event listeners by prefixing them with an @. Whenever the to-do-item component fires an onRemove event, this._removeTodo is called. Easy peasy.

Just to give you another example, here’s how we could handle a click event:

<button @click=${this._handleClick}></button>

Hey! Listen!> Notice how we specified an eventContext in our render() function: render(this.template(), this._shadowRoot, {eventContext: this});. This makes sure we always have the correct reference to this in our event handlers, and makes it so we don’t have to manually .bind(this) our event handlers in the constructor.##

Wrapping up

  • Learn about Lit-html
  • Lit-html in practice
  • Supercharge our web component
  • Attributes, properties, and events
  • Wrapping up

If you made it all the way here, you’re on your way to becoming a real Web Components hero. You’ve learned about lit-html, how lit-html renders, how to use attributes, properties and events, and how to implement lit-html to take care of the rendering of your Web Component.

Great job! We supercharged our web component, and it now efficiently renders to-do’s, but we still have a bunch of boilerplate code, and a lot of property and attribute management to take care of. It would be great if there would be an easier way to handle all this…

Web Components hero with LitElement

Web Components hero with LitElement

  • Recap
  • Properties and attributes
  • Lifecycle and rerendering
  • Conclusion

Lit-html and LitElement finally got their official (1.0 and 2.0 respectively) releases, and that makes it a great time to wrap up the Web Components: from Zero to Hero blog series. I hope you’ve found these blogs useful as you’ve read them; they’ve been a blast to write, and I very much appreciate all the feedback and response I’ve gotten!

Huh? A 2.0 release? Lit-element has moved away from the @polymer/lit-element namespace, to simply: lit-element. The lit-element npm package was previously owned by someone else and had already had a release, hence the 2.0 release.
Let’s get to it!

In the last blog post we learned how to implement lit-html to take care of templating for our web component. Let’s quickly recap the distinction between lit-html and lit-element:

  • Lit-html is a render library. It provides the what and the how.
  • LitElement is a web component base class. It provides the when and the where.

I also want to stress that LitElement is not a framework. It is simply a base class that extends HTMLElement. We can look at LitElement as an enhancement of the standard HTMLElement class, that will take care of our properties and attributes management, as well as a more refined rendering pipeline for us.

Lets take a quick look at our to-do-item component, rewritten with LitElement. You can find the full demo here, and on the github page:

import { LitElement, html, css } from 'https://unpkg.com/lit-element@latest/lit-element.js?module';

class TodoItem extends LitElement {
    static get properties() {
        return {
            text: { 
                type: String,
                reflect: true
            },
            checked: { 
                type: Boolean, 
                reflect: true 
            },
            index: { type: Number }
        }
    }

    constructor() {
        super();
        // set some default values
        this.text = '';
        this.checked = false;
    }

    _fire(eventType) {
        this.dispatchEvent(new CustomEvent(eventType, { detail: this.index }));   
    }
    
    static get styles() {
      return css`
	  :host {
	    display: block;
	    font-family: sans-serif;
	  }
			
	  .completed {
	    text-decoration: line-through;
	  }
			
	  button {
	    cursor: pointer;
	    border: none;
	  }
      `;
    }

    render() {
        return html`
            <li class="item">
                <input 
                    type="checkbox" 
                    ?checked=${this.checked} 
                    @change=${() => this._fire('onToggle')}>
                </input>
                <label class=${this.checked ? 'completed' : ''}>${this.text}</label>
                <button @click=${() => this._fire('onRemove')}>❌</button>
            </li>
        `;
    }
}

 

Properties and attributes

  • Recap
  • Properties and attributes
  • Lifecycle and rerendering
  • Conclusion

Let’s get straight into it. The first thing you might notice is that all of our setters and getters are gone, and have been replaced with LitElement’s static properties getter. This is great, because we’ve abstracted away a lot of boiler plate code and instead let LitElement take care of it.

So lets see how this works:

static get properties() {
    return {
        text: { 
            type: String,
            reflect: true
        },
        checked: { 
            type: Boolean, 
            reflect: true 
        },
        index: { type: Number }
    }
}

We can use the static properties getter to declare any attributes and properties we might need, and even pass some options to them. In this code, we’ve set a text, checked, and index property, and we’ll reflect the text and checked properties to attributes as well. Just like that. Remember how much work that was before? We had a whole chapter dedicated to reflecting properties to attributes!

We can even specify how we want attributes to be reflected:

static get properties() {
    return {
        text: { 
            type: String,
            reflect: true,
            attribute: 'todo'
        }
    }
}

Will reflect the text property in our DOM as the following attribute:

<to-do-item todo="Finish blog"></to-do-item>

Are you still confused about how reflecting properties to attributes works? Consider re-visiting part one of this blog series to catch up.
Additionally, and perhaps most importantly, the static properties getter will react to changes and trigger a rerender when a property has changed. We no longer have to call render functions manually to update, we just need to update a property, and LitElement will do all the work for us.
Hey! Listen!> You can still use custom getters and setters, but you’ll have to manually call this.requestUpdate() to trigger a rerender. Custom getters and setters can be useful for computed properties.###

Lifecycle and rerendering

  • Recap
  • Properties and attributes
  • Lifecycle and rerendering
  • Conclusion

Finally, let’s take a look at our to-do-app component:

import { LitElement, html } from 'lit-element';
import { repeat } from 'lit-html/directives/repeat';
import './to-do-item.js';

class TodoApp extends LitElement {
    static get properties() {
        return {
            todos: { type: Array }
        }
    }

    constructor() {
        super();
        this.todos = [];
    }

    firstUpdated() {
        this.$input = this.shadowRoot.querySelector('input');
    }

    _removeTodo(e) {
        this.todos = this.todos.filter((todo, index) => {
            return index !== e.detail;
        });
    }

    _toggleTodo(e) {
        this.todos = this.todos.map((todo, index) => {
            return index === e.detail ? {...todo, checked: !todo.checked} : todo;
        });
    }

    _addTodo(e) {
        e.preventDefault();
        if(this.$input.value.length > 0) {
            this.todos = [...this.todos, { text: this.$input.value, checked: false }];
            this.$input.value = '';
        }
    }

	static get styles() {
	  return css`
	     :host {
	         display: block;
	         font-family: sans-serif;
	         text-align: center;
	     }
	     button {
	         border: none;
	         cursor: pointer;
	     }
	     ul {
	         list-style: none;
	         padding: 0;
	     }
          `;
	}

    render() {
        return html`
            <h1>To do</h1>
            <form id="todo-input">
                <input type="text" placeholder="Add a new to do"></input>
                <button @click=${this._addTodo}>✅</button>
            </form>
            <ul id="todos">
                ${repeat(this.todos, 
                   (todo) => todo.text, 
                   (todo, index) => html`
                     <to-do-item 
                       .checked=${todo.checked}
                       .index=${index}
                       .text=${todo.text}
                       @onRemove=${this._removeTodo}
                       @onToggle=${this._toggleTodo}>    
                    </to-do-item>`
                  )}
            </ul>
        `;
    }
}

window.customElements.define('to-do-app', TodoApp);

You’ll notice that we’ve changed our functions up a little bit. We did this, because in order for LitElement to pick up changes and trigger a rerender, we need to immutably set arrays or objects. You can still use mutable patterns to change nested object properties or objects in arrays, but you’ll have to request a rerender manually by calling this.requestUpdate(), which could look like this:

_someFunction(newValue) {
	this.myObj.value = newValue;
	this.requestUpdate();
}

Which brings us to LitElement’s lifecycle. It’s important to note that LitElement extends HTMLElement, which means that we’ll still have access to the standard lifecycle callbacks like connectedCallback, disconnectedCallback, etc.

Additionally, LitElement comes with some lifecycle callbacks of it’s own. You can see a full example of LitElement’s lifecycle here.

 

shouldUpdate()

You can implement shouldUpdate() to control if updating and rendering should occur when property values change or requestUpdate() is called. This can be useful for when you don’t want to rerender.

firstUpdated()

firstUpdated is called when… well, your element has been updated the first time. This method can be useful for querying dom in your component.

 

updated()

Called right after your element has been updated and rerendered. You can implement this to perform post-updating tasks via DOM APIs, for example, focusing an element. Setting properties inside this method will not trigger another update.

And as I mentioned before, you can still implement connectedCallback() and disconnectedCallback().

Conclusion

  • Recap
  • Properties and attributes
  • Lifecycle
  • Conclusion

If you’ve made it all this way; congratulations! You are now a web components super hero. I hope this blog series was helpful and informative to you, and that it may function as a reference for when you need to remember something about web components.

If you’re interested in getting started with Web Components, make sure to check out open-wc. Open-wc provides recommendations including anything betwixt and between: developing, linting, testing, tooling, demoing, publishing and automating, and will help you get started in no time.

#webdevelopment #javascript #webdev #webcomponents

Web Components Tutorial: Go from Zero to Hero