Web Components: A Closer Look
In the previous post, I covered the basics of web components,
starting with how to set up a web component, styling and how to provide spaces in your component
to be filled dynamically or from the template, using <slot>
tags. In this continuation, I’m
going to explore lifecycle hooks provided by the Web Components API as well as look at how we
can, from the HTML, provide parameters to the web component. Let’s get started.
Life-cycle Hooks
At the current state, our class constructor instantiates the Shadow DOM API, stores data for the
current page to localStorage
, starts a timer to periodically update the last-viewed stories
and appends the CSS styles as well as the UI elements for the list of stories. The basic code
looks like this:
class LastViewedStories extends HTMLElement {
shadowRoot = null;
constructor() {
super();
this.addCurrentPageToList();
this.shadow = this.attachShadow({mode: 'open'});
this.interval = setInterval(this.reloadAndRerender.bind(this), 60000);
this.shadow.appendChild(this.generateComponentStyles());
this.shadow.appendChild(this.renderUiElement());
}
...
}
This works, but we’re doing two different things in the constructor: It calls the base
class’ constructor (here HTMLElement
) with super()
sets up the Shadow DOM and the proceeds
to set up the timer as well as render the component.
Web Components offer a number of events covering the life cycle of the component. The ones
discussed here are the connectedCallback()
and disconnectedCallback()
, which get executed
when a component is added and removed from the DOM, respectively.
Since we are using a timer, it would be prudent to stop the timer when the component is removed
from the DOM, to avoid potential memory leaks. Thus, we can implement the disconnectedCallback()
like this:
interval = null;
constructor() {
super();
this.addCurrentPageToList();
this.shadow = this.attachShadow({mode: 'open'});
this.interval = setInterval(this.reloadAndRerender.bind(this), 60000);
this.shadow.appendChild(this.generateComponentStyles());
this.shadow.appendChild(this.renderUiElement());
}
disconnectedCallback() {
clearInterval(this.interval);
}
As you see, the interval gets populated by the constructor via a call to setInterval()
. The
resulting interval timer then gets cleared in the disconnectedCallback()
method.
The other detail is that in its current form, the creation of the web component via invocation of its constructor causes the entire rendering process to take place. A potential problem lies in the possibility that not all resources required by the web component are available at that time — It also burdens the constructor with too much heavy lifting.
For this purpose, the Custom Element paradigm provides an event handler function
connectedCallback()
which is used to separate the main instantiation from the setup tasks to
render the component and start its business logic.
So in this case, the functionality can be split up in the following manner:
shadowRoot = null;
interval = null;
constructor() {
super();
this.shadowRoot = this.attachShadow({mode: 'open'});
}
connectedCallback() {
this.addCurrentPageToList();
this.interval = setInterval(this.reloadAndRerender.bind(this), 60000);
this.shadowRoot.appendChild(this.generateComponentStyles());
this.shadowRoot.appendChild(this.renderUiElement());
}
disconnectedCallback() {
clearInterval(this.interval);
}
This causes the instantiation to only set up the Shadow DOM and invoke the parent class’ constructor when the Web Component is instantiated, setting it up to fulfill its other duties.
On the other hand, the connectedCallback()
method will be executed when this Web Component
is added to the DOM. At that point, the latest point possible, we can be assured that all the
browser environment is set up, and we then perform the tasks required to set up the data and
render the component to the DOM.
In addition to not overwhelming the constructor with unlike functionality, the use of these two event handler methods also has cleaned up the code and made it easier to follow.
Properties on the Web Component Tag
So far, the element rendered by this component takes all its cues for visual presentation
from its embedded CSS. It sometimes is nice to affect the presentation or functionality of an
element through its attributes — As we do on <a>
tags, for example, which provide
attributes to specify the link destination via href
and rel
for the relationship of the
linked URL or the various attributes, or the <form>
tag with its many attributes.
We can do the same thing with our web components. For this example, I’m going to provide two
attributes which affect the element’s max height
as well as its width
:
maxHeight;
elementWidth;
constructor() {
super();
this.maxHeight = this.getAttribute('height') || '350px';
this.elementWidth = this.getAttribute('width') || '250px';
this.shadow = this.attachShadow({mode: 'open'});
}
generateComponentStyles() {
const styleElement = document.createElement('style');
styleElement.setAttribute('type', 'text/css');
styleElement.textContent = `
ol {
max-height:${this.maxHeight};
overflow-y: scroll;
padding:0 13px;
}
...
`
The basic functionality is straight-forward: In our constructor, I’m reading the values of
the height
and/or width
attributes and assign the contained values to their corresponding
object properties. I also provide sensible (?) defaults for the values considering that the
main use of this web component will be in a sidebar. The populated properties will then be used
in our method that generates the CSS for this web component.
The use of this feature in HTML looks exactly like we’re used to from regular, standard HTML tags:
<div class="sidebar">
<last-viewed width="400px" height="200px"></last-viewed>
</div>
Application of external styles
Web Components behave like a closed system when it comes to styling, but there are a few finer points about when external styles will be correctly applies and when they will be ignored.
For this sections, let’s assume the component last-view
specifies the following template
in its JavaScript class:
<div>
<slot name="headline"><h3>Recent stories ...</h3></slot>
</div>
and the template using the web component looks like this, where I have added a style section where any of the “external” styling would be applied in this example:
<div class="sidebar" style="width:150px">
<last-viewed></last-viewed>
</div>
<style> ... </style>
Case 1: Trying to overwrite component-internal styles from HTML
As the code stands right now, we do not specify an override of the web component’s headline
so the default <h3>
tag included within the <slot>
tag pair will be used. If we’re trying to
style that headline from the outside with something like this
h3 {
font-size: 24px;
font-style: italic;
color: #941645;
}
It will lead to an output as shown on the left side of the following screenshot overview. This demonstrates that any external styles do not affect the styling of elements inside the web component — The two are separate!
Case 2: Providing a slot override without external styling
Let’s now remove the external styling in the template which embeds the web component and instead provide a custom headline as a child of the component tags:
<div class="sidebar" style="width:150px">
<last-viewed>
<h3 slot="headline">Some other headline</h3>
</last-viewed>
</div>
<style></style>
In this case, the element renders as shown in the middle of the three screenshots seen above. It shows that the headline we provide replaces the default headline in the web component, albeit without taking on any of its styling — There are no margins, no bottom border etc.
This means that our internal styles for the h3
element are not applied to the node coming
from the outside of the web component.
Case 3: Providing a slot override and external styling
In this case, we’re providing both a headline element and the styling for the containing web application/template. The code for the screenshot on the far right looks like this:
<div class="sidebar" style="width:150px">
<last-viewed>
<h3 slot="headline">Some other headline</h3>
</last-viewed>
</div>
<style>
h3 {
font-size: 24px;
font-style: italic;
color: #949645;
}
</style>
In this case, the externally defined style is applied to the externally provided h3
tag, which
means we’re overriding both the content and the styling from the outside.
Case 4: Styling a web component from the outside
So this strict separation between shadow DOM and the page DOM allows/condemns us to deal with styling separately since the style rules don’t "break the barrier." Or do they?
The latest CSS specification includes the var()
construct, which permits to globally define
CSS variables and use them throughout the site — even inside a web component/custom element.
This, in essence, is what CSS preprocessors have been providing for a while: Define variables
and use them in your stylesheet.
The basic approach is to define some variables in your global stylesheet. Variables can be
named however you like, but they are prefixed with two hyphens, so for example: --my-magic-size
.
You would define them ideally in your global stylesheet for the html
element, like this:
html {
--site-font-size: 24px;
--site-font-style: italic;
--site-color: #949645;
}
Now that these values are defined, you can use the CSS var()
function to read them
and at the same time specify a default value — in case the global stylesheet
does not define a value for it. The Mozilla developer docs for var() describes the approach:
h3 {
font-size: var(--site-font-size, 15px);
...
}
In this style definition, the web component attempts to use the --site-font-size
value
defined in the rules for the page’s html
tag and if that is not defined falls back to
use 15px as the value for the font size.
This approach is a "safe" approach, as the author of a web component gets to decide, which styles the component accepts from the outside for styling its content. It thus does not break encapsulation as it would if global styles were universally applied to web components.
Summary
In this short post, I’ve expanded on the capabilities of web components by providing life cycle
hooks to perform actions at critical points of a component’s life cycle and have implemented
attributes on the web component tags to permit the user of these components to affect the way
a browser renders a component on a page. The final part examined the separation of the barrier
between the shadow DOM and the regular DOM as it applies to CSS rules and how one can use CSS
var()
to define global styles that a web component can choose to apply.