0
# Custom Directive System
1
2
Framework for creating custom directives to extend template functionality with lifecycle management and state.
3
4
## Capabilities
5
6
### Directive Creation Function
7
8
Creates directive functions from directive classes for use in templates.
9
10
```typescript { .api }
11
/**
12
* Creates a user-facing directive function from a Directive class.
13
* @param c - Directive class constructor
14
* @returns Function that creates DirectiveResult instances
15
*/
16
function directive<C extends DirectiveClass>(
17
c: C
18
): (...values: DirectiveParameters<InstanceType<C>>) => DirectiveResult<C>;
19
```
20
21
**Usage Examples:**
22
23
```typescript
24
import { directive, Directive } from 'lit-html/directive.js';
25
import { html } from 'lit-html';
26
27
class MyDirective extends Directive {
28
render(value: string) {
29
return value.toUpperCase();
30
}
31
}
32
33
const myDirective = directive(MyDirective);
34
35
const template = html`<div>${myDirective('hello world')}</div>`;
36
// Renders: <div>HELLO WORLD</div>
37
```
38
39
### Base Directive Class
40
41
Abstract base class for creating custom directives with render lifecycle.
42
43
```typescript { .api }
44
/**
45
* Base class for creating custom directives. Users should extend this class,
46
* implement `render` and/or `update`, and then pass their subclass to `directive`.
47
*/
48
abstract class Directive implements Disconnectable {
49
constructor(partInfo: PartInfo);
50
51
/** Required render method that returns the directive's output */
52
abstract render(...props: Array<unknown>): unknown;
53
54
/** Optional update method called before render with the current Part */
55
update(part: Part, props: Array<unknown>): unknown;
56
57
/** Connection state from the parent part/directive */
58
get _$isConnected(): boolean;
59
}
60
```
61
62
**Usage Examples:**
63
64
```typescript
65
import { directive, Directive, PartInfo, PartType } from 'lit-html/directive.js';
66
import { html } from 'lit-html';
67
68
class HighlightDirective extends Directive {
69
constructor(partInfo: PartInfo) {
70
super(partInfo);
71
if (partInfo.type !== PartType.CHILD) {
72
throw new Error('highlight() can only be used in text expressions');
73
}
74
}
75
76
render(text: string, color: string = 'yellow') {
77
return html`<mark style="background-color: ${color}">${text}</mark>`;
78
}
79
}
80
81
const highlight = directive(HighlightDirective);
82
83
// Usage in templates
84
const template = html`
85
<p>This is ${highlight('important text', 'lightblue')} in a paragraph.</p>
86
`;
87
```
88
89
### Async Directive Class
90
91
Extended directive class with async capabilities and connection lifecycle management.
92
93
```typescript { .api }
94
/**
95
* An abstract `Directive` base class whose `disconnected` method will be
96
* called when the part containing the directive is cleared or disconnected.
97
*/
98
abstract class AsyncDirective extends Directive {
99
/** The connection state for this Directive */
100
isConnected: boolean;
101
102
/** Sets the value of the directive's Part outside the normal update/render lifecycle */
103
setValue(value: unknown): void;
104
105
/** Called when the directive is disconnected from the DOM */
106
protected disconnected(): void;
107
108
/** Called when the directive is reconnected to the DOM */
109
protected reconnected(): void;
110
}
111
```
112
113
**Usage Examples:**
114
115
```typescript
116
import { AsyncDirective, directive } from 'lit-html/async-directive.js';
117
import { html } from 'lit-html';
118
119
class TimerDirective extends AsyncDirective {
120
private timer?: number;
121
122
render(interval: number) {
123
return 0; // Initial value
124
}
125
126
override update(part: Part, [interval]: [number]) {
127
// Set up timer when connected
128
if (this.isConnected && !this.timer) {
129
let count = 0;
130
this.timer = setInterval(() => {
131
this.setValue(++count);
132
}, interval);
133
}
134
return this.render(interval);
135
}
136
137
protected override disconnected() {
138
// Clean up timer when disconnected
139
if (this.timer) {
140
clearInterval(this.timer);
141
this.timer = undefined;
142
}
143
}
144
145
protected override reconnected() {
146
// Timer will be recreated in update() when reconnected
147
}
148
}
149
150
const timer = directive(TimerDirective);
151
152
// Usage
153
const template = html`<div>Timer: ${timer(1000)}</div>`;
154
```
155
156
### Part Information
157
158
Information about the part a directive is bound to for validation and behavior.
159
160
```typescript { .api }
161
interface PartInfo {
162
readonly type: PartType;
163
}
164
165
interface ChildPartInfo {
166
readonly type: typeof PartType.CHILD;
167
}
168
169
interface AttributePartInfo {
170
readonly type:
171
| typeof PartType.ATTRIBUTE
172
| typeof PartType.PROPERTY
173
| typeof PartType.BOOLEAN_ATTRIBUTE
174
| typeof PartType.EVENT;
175
readonly strings?: ReadonlyArray<string>;
176
readonly name: string;
177
readonly tagName: string;
178
}
179
180
interface ElementPartInfo {
181
readonly type: typeof PartType.ELEMENT;
182
}
183
184
const PartType = {
185
ATTRIBUTE: 1,
186
CHILD: 2,
187
PROPERTY: 3,
188
BOOLEAN_ATTRIBUTE: 4,
189
EVENT: 5,
190
ELEMENT: 6,
191
} as const;
192
```
193
194
**Usage Examples:**
195
196
```typescript
197
import { directive, Directive, PartInfo, PartType } from 'lit-html/directive.js';
198
199
class AttributeOnlyDirective extends Directive {
200
constructor(partInfo: PartInfo) {
201
super(partInfo);
202
// Validate this directive is only used on attributes
203
if (partInfo.type !== PartType.ATTRIBUTE) {
204
throw new Error('This directive can only be used on attributes');
205
}
206
}
207
208
render(value: string) {
209
return value.toLowerCase();
210
}
211
}
212
213
class ElementDirective extends Directive {
214
constructor(partInfo: PartInfo) {
215
super(partInfo);
216
if (partInfo.type !== PartType.ELEMENT) {
217
throw new Error('This directive can only be used on elements');
218
}
219
}
220
221
update(part: ElementPart, [className]: [string]) {
222
part.element.className = className;
223
return this.render(className);
224
}
225
226
render(className: string) {
227
return undefined; // Element directives typically don't render content
228
}
229
}
230
```
231
232
### Directive Helper Functions
233
234
Utility functions for working with directives and parts.
235
236
```typescript { .api }
237
/**
238
* Tests if a value is a primitive value.
239
* @param value - Value to test
240
* @returns True if the value is null, undefined, boolean, number, string, symbol, or bigint
241
*/
242
function isPrimitive(value: unknown): value is Primitive;
243
244
/**
245
* Tests if a part represents a single expression.
246
* @param partInfo - Part information to test
247
* @returns True if the part has no static strings or only empty strings
248
*/
249
function isSingleExpression(partInfo: PartInfo): boolean;
250
251
/**
252
* Inserts a ChildPart into the given container ChildPart's DOM.
253
* @param containerPart - Container part to insert into
254
* @param refPart - Optional reference part to insert before
255
* @param part - Optional part to insert, if not provided creates a new one
256
* @returns The inserted ChildPart
257
*/
258
function insertPart(
259
containerPart: ChildPart,
260
refPart?: ChildPart,
261
part?: ChildPart
262
): ChildPart;
263
264
/**
265
* Gets the committed value from a ChildPart.
266
* @param part - ChildPart to get value from
267
* @returns The last committed value
268
*/
269
function getCommittedValue(part: ChildPart): unknown;
270
271
/**
272
* Removes a ChildPart from its parent and clears its DOM.
273
* @param part - ChildPart to remove
274
*/
275
function removePart(part: ChildPart): void;
276
277
/**
278
* Sets the committed value on a part.
279
* @param part - Part to set value on
280
* @param value - Value to commit
281
*/
282
function setCommittedValue(part: Part, value?: unknown): void;
283
284
/**
285
* Sets a value on a ChildPart.
286
* @param part - ChildPart to set value on
287
* @param value - Value to set
288
* @param directiveParent - Optional directive parent for context
289
* @returns The same ChildPart instance
290
*/
291
function setChildPartValue<T extends ChildPart>(
292
part: T,
293
value: unknown,
294
directiveParent?: DirectiveParent
295
): T;
296
```
297
298
**Usage Examples:**
299
300
```typescript
301
import { directive, Directive } from 'lit-html/directive.js';
302
import {
303
isPrimitive,
304
isSingleExpression,
305
insertPart,
306
getCommittedValue,
307
removePart
308
} from 'lit-html/directive-helpers.js';
309
310
class AdvancedDirective extends Directive {
311
constructor(partInfo: PartInfo) {
312
super(partInfo);
313
314
// Check if this is a single expression binding
315
if (!isSingleExpression(partInfo)) {
316
throw new Error('This directive only works with single expressions');
317
}
318
}
319
320
render(value: unknown) {
321
// Use helper to check if value is primitive
322
if (isPrimitive(value)) {
323
return `Primitive value: ${value}`;
324
}
325
326
return 'Complex value detected';
327
}
328
329
update(part: ChildPart, [value]: [unknown]) {
330
// Get the previously committed value
331
const previousValue = getCommittedValue(part);
332
333
if (previousValue !== value) {
334
// Value changed, process update
335
return this.render(value);
336
}
337
338
// No change, return noChange to skip update
339
return noChange;
340
}
341
}
342
343
// Advanced directive that manages child parts
344
class ListManagerDirective extends Directive {
345
private childParts: ChildPart[] = [];
346
347
render(items: unknown[]) {
348
// This directive manages its own child parts
349
return '';
350
}
351
352
update(part: ChildPart, [items]: [unknown[]]) {
353
// Add new parts if needed
354
while (this.childParts.length < items.length) {
355
const newPart = insertPart(part);
356
this.childParts.push(newPart);
357
}
358
359
// Remove excess parts
360
while (this.childParts.length > items.length) {
361
const partToRemove = this.childParts.pop()!;
362
removePart(partToRemove);
363
}
364
365
// Update remaining parts
366
for (let i = 0; i < items.length; i++) {
367
setChildPartValue(this.childParts[i], items[i]);
368
}
369
370
return this.render(items);
371
}
372
}
373
```
374
375
## Advanced Directive Patterns
376
377
### Stateful Directives
378
379
Directives can maintain internal state across renders:
380
381
```typescript
382
import { directive, Directive } from 'lit-html/directive.js';
383
384
class CounterDirective extends Directive {
385
private count = 0;
386
387
render(increment: number = 1) {
388
this.count += increment;
389
return `Count: ${this.count}`;
390
}
391
}
392
393
const counter = directive(CounterDirective);
394
395
// Each use maintains separate state
396
const template = html`
397
<div>${counter()}</div> <!-- Count: 1 -->
398
<div>${counter(2)}</div> <!-- Count: 3 -->
399
<div>${counter()}</div> <!-- Count: 4 -->
400
`;
401
```
402
403
### Async Directives with External Data
404
405
Directives that fetch or subscribe to external data:
406
407
```typescript
408
import { AsyncDirective, directive } from 'lit-html/async-directive.js';
409
410
class FetchDirective extends AsyncDirective {
411
private url?: string;
412
private abortController?: AbortController;
413
414
render(url: string, fallback: unknown = 'Loading...') {
415
if (url !== this.url) {
416
this.url = url;
417
this.fetchData(url);
418
}
419
return fallback;
420
}
421
422
private async fetchData(url: string) {
423
if (!this.isConnected) return;
424
425
// Cancel previous request
426
this.abortController?.abort();
427
this.abortController = new AbortController();
428
429
try {
430
const response = await fetch(url, {
431
signal: this.abortController.signal
432
});
433
const data = await response.text();
434
435
// Update the rendered value
436
if (this.isConnected) {
437
this.setValue(data);
438
}
439
} catch (error) {
440
if (error.name !== 'AbortError' && this.isConnected) {
441
this.setValue(`Error: ${error.message}`);
442
}
443
}
444
}
445
446
protected override disconnected() {
447
this.abortController?.abort();
448
}
449
}
450
451
const fetchData = directive(FetchDirective);
452
```
453
454
### Multi-Part Directives
455
456
Directives that work with attribute interpolations:
457
458
```typescript
459
import { directive, Directive, AttributePart } from 'lit-html/directive.js';
460
461
class PrefixDirective extends Directive {
462
render(prefix: string, value: string) {
463
return `${prefix}:${value}`;
464
}
465
466
override update(part: AttributePart, [prefix, value]: [string, string]) {
467
// Handle multi-part attribute bindings
468
if (part.strings && part.strings.length > 2) {
469
// This is a multi-part attribute binding
470
return `${prefix}:${value}`;
471
}
472
return this.render(prefix, value);
473
}
474
}
475
476
const prefix = directive(PrefixDirective);
477
478
// Usage in multi-part attribute
479
const template = html`<div class="base ${prefix('theme', 'dark')} extra"></div>`;
480
```
481
482
## Types
483
484
```typescript { .api }
485
interface DirectiveClass {
486
new (part: PartInfo): Directive;
487
}
488
489
interface DirectiveResult<C extends DirectiveClass = DirectiveClass> {
490
['_$litDirective$']: C;
491
values: DirectiveParameters<InstanceType<C>>;
492
}
493
494
type DirectiveParameters<C extends Directive> = Parameters<C['render']>;
495
496
interface Disconnectable {
497
_$parent?: Disconnectable;
498
_$disconnectableChildren?: Set<Disconnectable>;
499
_$isConnected: boolean;
500
}
501
502
type Primitive = null | undefined | boolean | number | string | symbol | bigint;
503
```
504
505
## Import Patterns
506
507
```typescript
508
// Basic directive system
509
import { directive, Directive } from 'lit-html/directive.js';
510
511
// Async directive system
512
import { AsyncDirective } from 'lit-html/async-directive.js';
513
514
// Directive helpers
515
import { isPrimitive, isSingleExpression } from 'lit-html/directive-helpers.js';
516
517
// Part types and info
518
import { PartInfo, PartType, AttributePartInfo } from 'lit-html/directive.js';
519
```