Getting the context of Web Components (lit)
The default nature of Web Components and one of their main selling points, is that they are closed off. However, there are occasions when it is very useful to have the context of your component. When styling for example.
There may be subtle variations depending on where your component lives. A different background if in the sidebar area than the main content, for example.
This can be surprisingly long-winded to deal with, but what follows is what I have found to be an effective way.
Passing context down
My context here is Lit. If you want to pass context, or any other properties down, in the absence of the new context package you would typically do this with a property that gets passed down through the tree of components. The outermost relevant component sends in .context=${"sidebar"}
and then each subsequest child component recieves and passes on this context, <other-element .context=${this.context}></other-element>
and this goes on and on down the chain. Forget a link in the chain and you won’t get any context.
Turns out you can also reach from within a component up.
Reaching up for context
There is a method of the Node interface called getRootNode()
and we can use that to get the tag name of the next parent element like this:
this.getRootNode()?.host?.tagName
We have the optional chaining operator (?
) in there just to stop it erroring if it fails to find something.
And that will return you a string something like SIDE-BAR
, always in uppercase. So you may want to massage that a little more, maybe lower-casing it:
this.getRootNode()?.host?.tagName.toLowerCase()
Multiple levels
Although it doesn’t look particularly elegant, you can walk up the tree of hosts like this (for two hosts up):
this.getRootNode()?.host?.getRootNode()?.host?.toLowerCase()
And chain it on as many times as you like.
TypeScript
The double-edged sword of TypeScript may whinge at you that it doesn’t know what host
is. You may need to add something like this to the bottom of your component:
declare global {
interface Node {
host: any;
}
}
A wrapping utility function
My first inclination was to try and build a function up with a loop. But I think this could only be achieved with ultimately using either . That seemed like a particularly bad idea.new Function()
or eval()
on a string made to be a function
Thanks to Shiv in the comments, here is a lovely and elegant solution:
function getAncestorHost(component: Element, level: number = 1) {
let host = component;
let current = level;
while (current-- > 0) {
const h = (host.getRootNode() as ShadowRoot | undefined)?.host;
if (h === undefined) {
console.warn(`Could not find host (level ${current + 1}/${level})`);
return host;
}
host = h;
}
return host.tagName.toLowerCase();
}
Now you can write getAncestorHost(this, 5)
, rather than writing:
this.getRootNode()?.host?
.getRootNode()?.host?
.getRootNode()?.host?
.getRootNode()?.host?
.getRootNode()?.host?
.tagName.toLowerCase()
The odd time you need to.
Summary
Whichever method you choose, there is a convenient(ish) way to get the context of your web component for styling. For my case I typically add the context as an attribute:
<my-component data-context="${getAncestorHost(this," 2)}></my-component>
Which then let’s me style it like this:
:host([data-context="whatever-the-context-is"]) {
/* styles */
}
Which is usually preferable to using the method of passing properties down the chain (which I have subsequently heard described at ‘prop drilling’). Until the Context Protocol is established at least.
I think you can support arbitrary depth (or height, depending on how you look at it) in `getAncestorHost` without `eval` &c. using a loop (optimistically assuming Markdown is supported):
“`
export function getAncestorHost(component: Element, level: number = 1): string | undefined {
let host = component;
let current = level;
while (current– > 0) {
const h = (component.getRootNode() as ShadowRoot | undefined)?.host;
if (h === undefined) {
console.warn(`Could not find host (level ${current + 1}/${level})`);
return h;
}
host = h;
}
return host.tagName.toLowerCase();
}
“`