An Overview of the Most Exciting Proposals for the Web Platform Related to Web Components
Publikováno: 30.1.2019
As much as I like frameworks, I'm also a big fan of the native web platform, especially web components.
I look forward to the times when the implementation will be powerful eno...
As much as I like frameworks, I'm also a big fan of the native web platform, especially web components.
I look forward to the times when the implementation will be powerful enough to solve the common problems in web development. And we're getting closer.
In this article I want to take a look at the most exciting features that soon can be part of the web platform and will make web components a lot more powerful. But before I do that, I want to spend a little time to explain the concept of a platform.
This article would take me a lot longer to write and would be less comprehensive without the help of Justin Fagnani from the Polymer team at Google! ????
I work as a developer advocate at ag-Grid. If you’re curious to learn about data grids or looking for the ultimate Angular/React/Vue data grid solution, give it a try with our guide “Get started in 5 minutes” guide. I’m happy to answer any questions you may have. And follow me to stay tuned!
When we say "platform", what exactly do we mean?
In its most basic form a platform is environment to execute programs. It provides the actual hardware and/or the software required to run an application.
Historically, a "platform" typically referred to a computer's operating system. For example, a program running on PC would be considered to be running on a Windows platform. On the other hand, programs running on iMac are running on the Macintosh platform. Today we have a great number of platforms built on top of operating systems. Probably the two most dominant are Web and Mobile. Web components are part of the web platform.
In web development, we write programs to be executed by browsers. So, in essence, the web platform comes in a form of a browser. It's a browser's job to interact with native OS and provide tools to execute logic, I/O and rendering. The browser vendors are responsible for implementing the features of the platform to allow our web applications to run.
But who decides what features to implement? Who determines API to make network requests, read files, perform navigation etc. And how browsers know what to implement? Well, all features of the web platform are described in the specifications (standards). These standards are produced by a community of people often referred to as committees. For web standards, we have two - The World Wide Web Consortium (W3C) and The Web Hypertext Application Technology Working Group (WHATWG). You can read more about them, how they operate and how proposals get into the specification in this great article.
Okay, now that we know what is web platform and how it works, let's take a look at the most excited proposal related to web components in 2019.
Chromium engine in Microsoft Edge
One of the most discussed topics recently has been the decision of Microsoft to use Chromium engine for its web browser Edge. This means that Edge will have a complete up-to-date implementations of all specs enabling Web Components, particularly a very interesting capability of customizing built-in elements. On top of that, Chromium engine will add a whole host of other very exciting features like Template Instantiation, CSS Shadow Parts, Constructable Stylesheets, CSS Modules and Scoped Custom Element Definitions. Let's take a sneak peek at each of them.
Template Instantiation with substituted values
This is an amazing functionality that would allow to render a component's view using native browser mechanisms. Today, we have web frameworks that allow defining a template with expressions that use a component state. During change detection in Angular or reconciliation in React these expressions are evaluated and the values are used for rendering the screen. Frameworks save us a lot of effort of manually creating and updating DOM as a result of a component state changes. How can we use a native platform to do that?
We already have a template element in HTML5. Unfortunately it doesn't provide a native mechanism to instantiate DOM from a template with some values substituted during the instantiation process. Well, things have changed. The new proposal defines a mechanism to instantiate and update the template providing values for placeholders.
When this proposal gets implemented, we'll be able to define a template like this with placeholders for name
and email
:
<template id="person">
<section>
<h1>{{name}}</h1>
Email: <a href="mailto:{{email}}">{{email}}</a>
</section>
</template>
and then provide the values for the placeholders during an instantiation:
let template = document.querySelector('#person');
let instance = template.createInstance({name: "Ryosuke Niwa", email: "rniwa@webkit.org"});
The return type of the createInstance
method is TemplateInstance
which has a content
property of DocumentFragment
type. You could then use this property to get a hold of the resulting DOM and append it to the Shadow DOM like this:
shadowRoot.appendChild(instance.content);
and have a browser render it on the screen.
Moreover, the proposal also suggests adding the update
method to the resulting content
object that will make implementing change detection a breeze. Simply add a setter
to intercept the assignment and update the template as a side effect:
class Person extends HTMLElement {
constructor() {
super();
let template = document.querySelector('#person');
this.dom = template.createInstance({name: "Ryosuke Niwa", email: "rniwa@webkit.org"});
var shadow = this.attachShadow({mode: 'open'});
shadow.appendChild(this.dom);
}
set name(value) { this.dom.update({name: value}) }
set email(value) { this.dom.update({email: value}) }
}
This is just one of the many use-cases for this mechanism. You can learn a lot more about the proposal here and here.
CSS Shadow Parts
The web platform continues to evolve to give user code abilities that previously only native elements had. First we got the capability to introduce new element types through Custom Elements. Then Shadow DOM was added to bring true encapsulation to the world of web development. It basically means that styles defined outside of Shadow DOM cannot be applied to HTML elements inside the Shadow DOM. As you can see in the following demo, the styles applied to the span
elements couldn't cross the Shadow DOM boundary:
<html>
<head>
<style>
span { color: red; }
</style>
</head>
<body>
<span>color will be red</span>
<div class="shadow">
#shadow-root
<span>color will black</span>
</div>
</body>
</html>
And now we have a new pseudo-element added to CSS selectors that makes it easy to style elements inside Shadow DOM. For example, applying custom styles to a reusable component with Shadow DOM from a shared library.
The new pseudo-element is ::part(). It can be used to target elements that have been exposed via a part attribute.
This allow shadow hosts to selectively expose chosen elements from their DOM tree to the outside world for styling purposes. Unlike custom properties, it works on HTML elements, and unlike piercing operators **/deep/
and :shadow
** it allows precise control over which elements can be styled. If want to learn more about custom properties or piercing operators read here.
To use this mechanism you simply mark some HTML elements with the part
attribute:
<x-foo>
#shadow-root
<div part="some-box"><span>...</span></div>
<input part="some-input">
<div>...</div> /_ not styleable _/
</x-foo>
And then you can target these DOM elements from outside the Shadow DOM:
x-foo::part(some-box) { ... }
This feature is shipping in Chrome 74.
Constructable Stylesheet Objects
Because of the encapsulation mechanism, CSS styles defined on the top level of a document cannot be used to style elements inside the Shadow DOM. For styles to take effect inside the Shadow DOM, currently they must be specified inside the Shadow DOM using the style
element:
<x-foo>
#shadow-root
<style>p { color: green; }</style>
<div part="some-box"><span>...</span></div>
<input part="some-input">
<div>...</div> /_ not styleable
<x-foo>
_#shadow-root_
<style>p { color: green; }</style>
<div part="some-box"><span>...</span></div>
<input part="some-input">
<div>...</div> /_ not styleable
</x-foo>
</x-foo>
The problem with this approach is that browsers need to parse and store the style sheet rules once for every style
element. However, since Shadow DOM is an essential part of web components, each component will have a style
element inside. If you reuse the same component on the page, it will have the same styles so it doesn't make sense to parse and store the style sheet separately for each instance of the component. As a web page may contain tens of thousands of web components with Shadow DOM, this can easily have a large time and memory cost. Browsers though try to optimize that by deduplicating based on strings contents, so that if the content is the same the stylesheet isn't parsed again. However, the problem can be eliminated entirely by creating a stylesheet imperatively and reusing it wherever needed.
The current proposal defines an API for creating stylesheet objects from script removing the need for declarative style
elements. It also specifies a way to reuse the created stylesheets in multiple places. To use this approach, we first create a stylesheet like this:
const myElementSheet = new CSSStyleSheet();
and then apply the stylesheet to the Shadow DOM:
shadowRoot.adoptedStyleSheets = [myElementSheet];
Each stylesheet object can be added directly to any number of shadow roots (and/or the top level document).
The proposal also defines API to add, remove, or replace rules from a stylesheet object. The easiest way to initialize a stylesheet is to use the replaceSync
method and pass it a text with stylesheet rules:
const styles = 'p { color: green; }';
myElementSheet.replaceSync(styleText)
The following example demonstrates how this functionality can be used inside a web component:
class MyElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot.adoptedStyleSheets = [myElementSheet];
}
connectedCallback() {
if (myElementSheet.cssRules.length == 0) {
myElementSheet.replaceSync(styleText);
}
}
}
Notice that we only start parsing the stylesheet in the connectedCallback
when the first instance of the component is connected to the DOM. You can read more about the spec here
.
HTML Modules & CSS Modules
ECMAScript 6 brought the native modules into the JavaScript world. We no longer need to use 3rd party tools to load a script. Inside a script that is loaded through the script tag with the type="module"
we can write the following line:
import toUpperCase from './uppercase.js'
and rely on a browser to load, parse and instantiate the module code for us.
However, we still need to use various HTML and CSS loaders to get our HTML templates or stylesheets to the page. Imagine if we could write something like this in our ES module script and have the HTML template and CSS stylesheet loaded and instantiated by a browser:
import styles from './mycomponent.css';
import template from './mycomponent.html";
That's exactly the mechanism described by the proposals on HTML Modules (initial) and CSS Modules.
Initially HTML Modules (HTML imports) were proposed and implemented in Chromium, but they were developed independently of ES6 which had several limitations. The new proposed implementation is described in the explainer doc provided by the Edge team. It shows how HTML Modules can be integrated into the existing ES6 module system, rather than creating it as a standalone component.
To import HTML Modules you will use the same import
statements currently used for Script Modules:
<script type="module">
import {content} from "import.html"
document.body.appendChild(content);
</script>
To specify its exports HTML Module will use its inline script elements:
<div id="blogPost">
<p>Content...</p>
</div>
<script type="module">
let blogPost = import.meta.document.querySelector("#blogPost");
export {blogPost}
</script>
CSS Modules is a completely new thing.
To import the stylesheet, similarly to HTML Modules you'll use the import
statement:
import styles from './styles.css';
// push() doesn't actually exist yet
document.styleSheets.push(styles);
The only export of a CSS module would be a default export of the StyleSheet
object. The semantics for CSS Modules is very simple, and combined with Constructable Stylesheets we discussed above allow the importer to determine how the CSS should be applied to the document.
Here's how both can be used inside a Web Component:
import {content} from "import.html"
import styles from './styles.css';
class MyElement extends HTMLElement {
constructor() {
this.attachShadow({mode: open});
// push() doesn't actually exist yet this.shadowRoot.moreStyleSheets.push(styles);
this.shadowRoot.appendChild(content);
}
}
CSS Modules is shipping in Chrome 73.
Scoped Custom Element Definitions
Currently, to register a custom element we use the define method provided by the CustomElementRegistry
available through the window.customElement
:
class WordCount extends HTMLParagraphElement { ... };
customElements.define('word-count', WordCount, ...);
The problem is that the element registry accessible through window
is a global registry. This means that registering all custom elements on this registry may lead to name collisions arising from coincidence, or from an app trying to define multiple versions of the same element, or from more advanced scenarios like registering mocks during tests, or a component explicitly replacing an element definition for its scope.
Scoped Custom Element Registries proposal describes a possible solution to this problem. It adds the ability to imperatively construct CustomElementRegistry
s and chain them in order to inherit custom element definitions. It uses ShadowRoot
as a scope for definitions. ShadowRoot
can be associated with a CustomElementRegistry
when created and gains element creation methods, like createElement
. When new elements are created within a ShadowRoot
, that ShadowRoot
's registry is used to Custom Element upgrades.
The following example demonstrates how we can create our custom element registry and associate it with the Shadow DOM:
// Create a new registry that inherits from the global registry
const myRegistry = new CustomElementRegistry(window.customElements);
// Use the local registry when creating the ShadowRoot
element.attachShadow({mode: 'open', customElements: myRegistry});
We can then register a custom element with this registry and use the scoped element creation APIs to create elements:
// Define a trivial subclass of XFoo so that we can register it ourselves
class MyFoo extends XFoo {}
// Register it as `my-foo` locally.
myRegistry.define('my-foo', MyFoo);
// the definiton of my-foo will be resolved from the custom registry
const myFoo = this.shadowRoot.createElement('my-foo');
element.shadowRoot.appendChild(myFoo);