or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

built-in-directives.mdcore-templates.mdcustom-directives.mdindex.mdstatic-templates.md
tile.json

custom-directives.mddocs/

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

```