2

Getting the context of Web Components (lit)

 1 year ago
source link: https://benfrain.com/getting-the-context-of-web-components-lit/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Getting the context of Web Components (lit)

10.11.2022 0 comments
0 days since last revision. Content should be accurate.

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

I’m not convinced a wrapping function is actually worth it. 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 new Function() or eval() on a string made to be a function. That seemed like a particularly bad idea.

The alternative would be doing things long-hand where you pass the element/component and the level you want to ‘go up to’ as a number and get something back like this:

export function getAncestorHost(component: any, level: number) {
  let host;
  if (level === 1) {
    return component.getRootNode()?.host?.tagName.toLowerCase();
  } else if (level === 2) {
    host = component
      .getRootNode()
      ?.host?.getRootNode()
      ?.host?.tagName.toLowerCase();
  } else if (level === 3) {
    host = component
      .getRootNode()
      ?.host?.getRootNode()
      ?.host?.getRootNode()
      ?.host?.tagName.toLowerCase();
  } else if (level === 4)
    host = component
      .getRootNode()
      ?.host?.getRootNode()
      ?.host?.getRootNode()
      ?.host?.getRootNode()
      ?.host?.tagName.toLowerCase();
  else if (level === 5) {
    host = component
      .getRootNode()
      ?.host?.getRootNode()
      ?.host?.getRootNode()
      ?.host?.getRootNode()
      ?.host?.getRootNode()
      ?.host?.tagName.toLowerCase();
  } else {
    console.warn("Cannot find host");
  }
  if (host !== undefined) {
    return host;
  } else {
    console.warn(`Cannot find host at level ${level}`);
  }
}

But that’s limited to however many levels you can be bothered to write and needs to be imported each time you want to use it. But perhaps it is worth it to 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.

If this post was useful, Say thanks with a coffee. Every donation helps keep me going! 🙏

Latest Video Course Web Animations with the JavaScript Web Animations API

Learn how to chain and sequence animations with one of the Web Platform's best kept secrets.

Passing context downReaching up for contextTypeScriptA wrapping utility functionSummary


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK