LitElement spread directive

Lit · TypeScript

Disclaimer: Do not use my code without considering its quite obvious short-comings! If you’d like to use a more robust implementation of a spread directive, read the last paragraph.

Context

When i was working on my time-tracker app i was confronted with repeatedly writing out attributes for a custom web component i have written. I was contemplating about how to approach this daunting repetition of code.

I couldn’t come up with an opinion quickly but i a simple intermediate workaround inspired by React sparked up in my mind.

In React one is able to just spread out attributes, or more generally props, to components by utilizing the ES6 spread operator:

1
2
3
4
5
function App() {
    return (
        <MyCustomComponent ...someProps />
    )
}

I wondered if something like this is possible with LitElement too. I remembered that directives where potentially capable of doing this. So i started writing my very first directive in Lit. And quite a useful one as it turned out.

Directives

Lit has, as it feels, a very selected set of built-in directives. A lot of these will solve one or many problems one might run into while writing WebComponents with Lit.

The Lit documentation has a whole page designated towards custom directives. It’s advisable to read that closely.

Types of directives

There’s only two types of directives:

Simple functions are, as it feels, very simple and limited as they have to return a renderable string (or more broadly speaking: anything that turns out to be renderable).

Class-based directives in contrast are very flexible as to how one is able to manipulate the output of the directive, which still has to be a string/renderable nonetheless. As one can hook into the lifecycle of a components with them, one can virtually do anything with them.

Writing the spread directive

Looking at the API Documentation for custom directives it’s easy to figure out, that implementing a class-based directive should not be too big of a feat.

There’s only two method one can implement (apart from the constructor):

The following is the full code i wrote for my spread directive:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { Directive, directive } from "lit/directive.js";
import { ElementPart, noChange, nothing } from "lit";

class SpreadDirective extends Directive {
	render(obj: Record<string, unknown> | object) {
		return nothing;
	}

	update(part: ElementPart, [obj]: Record<string, unknown>[]) {
		for (const k in obj) {
			part.element[k] = obj[k];
		}
		return noChange;
	}
}

export const spread = directive(SpreadDirective);

Ok, let’s make this a bit more digestible. The core to create your custom directive is the following:

1
2
3
4
5
6
7
8
9
import { Directive, directive } from "lit/directive.js";

class SpreadDirective extends Directive {
	render() {
		return "something";
	}
}

export const spread = directive(SpreadDirective);

That’s the bare minimum. When creating a class-based custom directive, the last line becomes obligatory to add. Calling the directive function with our custom directive class creates the actual custom directive that can then be exported.

Adding the call signature for our directive

Remember that the render method is used to derive the signature for our directive. This forced me to add the render function just for that purpose.

In order to signal Lit that render actually returns nothing to render, we add the nothing sentinel value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { Directive, directive } from "lit/directive.js";
import { nothing } from "lit";

class SpreadDirective extends Directive {
	render(obj: Record<string, unknown> | object) {
		return nothing;
	}
}

export const spread = directive(SpreadDirective);

update is where the magic happens

The update method is the key to the spread directive. It takes two parameters:

The Part type is crucial for our implementation. We want to use the ElementPart which is the one that can be used within element tags.

We extract the obj we are expecting out of the render argument’s array. That obj will be iterated over and all properties on it will override the same property on the target element.

The return value is the noChange sentinel value in order to indicate that we do not want to re-render anything. If a re-render of the component the directive is acting on is necessary, it will derive this on its own by the changes on its properties.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { Directive, directive } from "lit/directive.js";
import { ElementPart, noChange, nothing } from "lit";

class SpreadDirective extends Directive {
	render(obj: Record<string, unknown> | object) {
		return nothing;
	}

	update(part: ElementPart, [obj]: Record<string, unknown>[]) {
		for (const k in obj) {
			part.element[k] = obj[k];
		}
		return noChange;
	}
}

export const spread = directive(SpreadDirective);

Look further

After implementing the spread directive, i searched the web for an existing directive. Turns out, Google wrote one themselves but did not include with the built-in ones.

The spread directive is part of the lit-helpers module which doesn’t consist of too many helpers at the time of writing this.

The spread directive in that module is much more feature rich than the simple one i wrote. It is capable of attaching properties, attributes, boolean attributes and event listeners by spreading them. The sigils are necessary for those though.