0
# Decorators
1
2
TypeScript decorators for enhanced development experience with declarative property and element configuration. Decorators provide a clean, declarative syntax for configuring reactive properties and DOM queries.
3
4
## Capabilities
5
6
### Custom Element Decorator
7
8
Registers a custom element with the browser's custom element registry.
9
10
```typescript { .api }
11
/**
12
* Class decorator to register a custom element
13
* @param tagName The tag name for the custom element (must contain a hyphen)
14
* @returns Class decorator function
15
*/
16
function customElement(tagName: string): ClassDecorator;
17
```
18
19
**Usage Examples:**
20
21
```typescript
22
import { LitElement, html, customElement } from "lit-element";
23
24
@customElement("my-button")
25
class MyButton extends LitElement {
26
render() {
27
return html`
28
<button>
29
<slot></slot>
30
</button>
31
`;
32
}
33
}
34
35
@customElement("user-card")
36
class UserCard extends LitElement {
37
render() {
38
return html`
39
<div class="card">
40
<slot name="avatar"></slot>
41
<slot name="name"></slot>
42
<slot name="bio"></slot>
43
</div>
44
`;
45
}
46
}
47
48
// Elements are automatically registered and can be used in HTML
49
// <my-button>Click me</my-button>
50
// <user-card>
51
// <img slot="avatar" src="..." />
52
// <h2 slot="name">John Doe</h2>
53
// <p slot="bio">Software developer</p>
54
// </user-card>
55
```
56
57
### Property Decorator
58
59
Declares a reactive property that triggers updates when changed.
60
61
```typescript { .api }
62
/**
63
* Property decorator for reactive properties
64
* @param options Configuration options for the property
65
* @returns Property decorator function
66
*/
67
function property(options?: PropertyDeclaration): PropertyDecorator;
68
```
69
70
**Usage Examples:**
71
72
```typescript
73
import { LitElement, html, customElement, property } from "lit-element";
74
75
@customElement("my-element")
76
class MyElement extends LitElement {
77
@property({ type: String })
78
name = "World";
79
80
@property({ type: Number })
81
count = 0;
82
83
@property({ type: Boolean })
84
disabled = false;
85
86
@property({ type: Array })
87
items: string[] = [];
88
89
@property({ type: Object })
90
config: any = {};
91
92
render() {
93
return html`
94
<div>
95
<h1>Hello, ${this.name}!</h1>
96
<p>Count: ${this.count}</p>
97
<button ?disabled=${this.disabled}>
98
${this.disabled ? 'Disabled' : 'Enabled'}
99
</button>
100
<ul>
101
${this.items.map(item => html`<li>${item}</li>`)}
102
</ul>
103
</div>
104
`;
105
}
106
}
107
```
108
109
### State Decorator
110
111
Declares internal reactive state that doesn't reflect to attributes.
112
113
```typescript { .api }
114
/**
115
* Property decorator for internal reactive state
116
* @param options Configuration options for the state property
117
* @returns Property decorator function
118
*/
119
function state(options?: StateDeclaration): PropertyDecorator;
120
```
121
122
**Usage Examples:**
123
124
```typescript
125
import { LitElement, html, customElement, property, state } from "lit-element";
126
127
@customElement("toggle-element")
128
class ToggleElement extends LitElement {
129
@property({ type: String })
130
label = "Toggle";
131
132
@state()
133
private _expanded = false;
134
135
@state()
136
private _loading = false;
137
138
@state()
139
private _data: any[] = [];
140
141
private async _toggle() {
142
this._loading = true;
143
144
if (!this._expanded) {
145
// Simulate loading data
146
await new Promise(resolve => setTimeout(resolve, 1000));
147
this._data = ['Item 1', 'Item 2', 'Item 3'];
148
}
149
150
this._expanded = !this._expanded;
151
this._loading = false;
152
}
153
154
render() {
155
return html`
156
<div>
157
<button @click=${this._toggle} ?disabled=${this._loading}>
158
${this._loading ? 'Loading...' : this.label}
159
</button>
160
161
${this._expanded ? html`
162
<div class="content">
163
<ul>
164
${this._data.map(item => html`<li>${item}</li>`)}
165
</ul>
166
</div>
167
` : ''}
168
</div>
169
`;
170
}
171
}
172
```
173
174
### Query Decorator
175
176
Queries the render root for an element matching a CSS selector.
177
178
```typescript { .api }
179
/**
180
* Property decorator for DOM queries
181
* @param selector CSS selector to query for
182
* @param cache Whether to cache the query result
183
* @returns Property decorator function
184
*/
185
function query(selector: string, cache?: boolean): PropertyDecorator;
186
```
187
188
**Usage Examples:**
189
190
```typescript
191
import { LitElement, html, customElement, query } from "lit-element";
192
193
@customElement("form-element")
194
class FormElement extends LitElement {
195
@query('#username')
196
usernameInput!: HTMLInputElement;
197
198
@query('button[type="submit"]')
199
submitButton!: HTMLButtonElement;
200
201
@query('.error-message')
202
errorMessage?: HTMLElement;
203
204
@query('form', true) // cached query
205
form!: HTMLFormElement;
206
207
private _handleSubmit(e: Event) {
208
e.preventDefault();
209
210
const username = this.usernameInput.value;
211
if (!username.trim()) {
212
if (this.errorMessage) {
213
this.errorMessage.textContent = 'Username is required';
214
this.errorMessage.style.display = 'block';
215
}
216
return;
217
}
218
219
// Hide error message
220
if (this.errorMessage) {
221
this.errorMessage.style.display = 'none';
222
}
223
224
// Disable submit button
225
this.submitButton.disabled = true;
226
227
console.log('Submitting:', username);
228
}
229
230
render() {
231
return html`
232
<form @submit=${this._handleSubmit}>
233
<div>
234
<label for="username">Username:</label>
235
<input id="username" type="text" required />
236
</div>
237
<div class="error-message" style="display: none; color: red;"></div>
238
<button type="submit">Submit</button>
239
</form>
240
`;
241
}
242
}
243
```
244
245
### Query All Decorator
246
247
Queries the render root for all elements matching a CSS selector.
248
249
```typescript { .api }
250
/**
251
* Property decorator for DOM queries returning NodeList
252
* @param selector CSS selector to query for
253
* @returns Property decorator function
254
*/
255
function queryAll(selector: string): PropertyDecorator;
256
```
257
258
**Usage Examples:**
259
260
```typescript
261
import { LitElement, html, customElement, queryAll } from "lit-element";
262
263
@customElement("list-element")
264
class ListElement extends LitElement {
265
@queryAll('.item')
266
items!: NodeListOf<HTMLElement>;
267
268
@queryAll('input[type="checkbox"]')
269
checkboxes!: NodeListOf<HTMLInputElement>;
270
271
@queryAll('.draggable')
272
draggableElements!: NodeListOf<HTMLElement>;
273
274
private _selectAll() {
275
this.checkboxes.forEach(checkbox => {
276
checkbox.checked = true;
277
});
278
}
279
280
private _clearAll() {
281
this.checkboxes.forEach(checkbox => {
282
checkbox.checked = false;
283
});
284
}
285
286
private _highlightAll() {
287
this.items.forEach(item => {
288
item.classList.add('highlighted');
289
});
290
}
291
292
render() {
293
return html`
294
<div>
295
<div class="controls">
296
<button @click=${this._selectAll}>Select All</button>
297
<button @click=${this._clearAll}>Clear All</button>
298
<button @click=${this._highlightAll}>Highlight All</button>
299
</div>
300
301
<div class="item">
302
<input type="checkbox" /> Item 1
303
</div>
304
<div class="item">
305
<input type="checkbox" /> Item 2
306
</div>
307
<div class="item">
308
<input type="checkbox" /> Item 3
309
</div>
310
</div>
311
`;
312
}
313
}
314
```
315
316
### Query Async Decorator
317
318
Performs an async query that resolves when the element is found.
319
320
```typescript { .api }
321
/**
322
* Property decorator for async DOM queries
323
* @param selector CSS selector to query for
324
* @returns Property decorator function returning Promise<Element>
325
*/
326
function queryAsync(selector: string): PropertyDecorator;
327
```
328
329
**Usage Examples:**
330
331
```typescript
332
import { LitElement, html, customElement, queryAsync } from "lit-element";
333
334
@customElement("async-element")
335
class AsyncElement extends LitElement {
336
@queryAsync('#dynamic-content')
337
dynamicContent!: Promise<HTMLElement>;
338
339
@queryAsync('.lazy-loaded')
340
lazyElement!: Promise<HTMLElement>;
341
342
private async _handleDynamicContent() {
343
try {
344
const element = await this.dynamicContent;
345
element.textContent = 'Content loaded!';
346
element.style.color = 'green';
347
} catch (error) {
348
console.error('Failed to find dynamic content:', error);
349
}
350
}
351
352
private async _loadContent() {
353
// Trigger re-render to add the dynamic element
354
this.requestUpdate();
355
356
// Wait for the element to be available
357
await this._handleDynamicContent();
358
}
359
360
render() {
361
return html`
362
<div>
363
<button @click=${this._loadContent}>Load Content</button>
364
<div id="dynamic-content">Loading...</div>
365
</div>
366
`;
367
}
368
}
369
```
370
371
### Query Assigned Elements Decorator
372
373
Queries for elements assigned to a slot.
374
375
```typescript { .api }
376
/**
377
* Property decorator for querying slot assigned elements
378
* @param options Query options including slot name and selector
379
* @returns Property decorator function
380
*/
381
function queryAssignedElements(options?: QueryAssignedElementsOptions): PropertyDecorator;
382
383
interface QueryAssignedElementsOptions {
384
slot?: string;
385
selector?: string;
386
flatten?: boolean;
387
}
388
```
389
390
**Usage Examples:**
391
392
```typescript
393
import { LitElement, html, customElement, queryAssignedElements } from "lit-element";
394
395
@customElement("tab-container")
396
class TabContainer extends LitElement {
397
@queryAssignedElements({ slot: 'tab' })
398
tabs!: HTMLElement[];
399
400
@queryAssignedElements({ slot: 'panel' })
401
panels!: HTMLElement[];
402
403
@queryAssignedElements({ selector: '.special' })
404
specialElements!: HTMLElement[];
405
406
private _activeTab = 0;
407
408
private _selectTab(index: number) {
409
this._activeTab = index;
410
411
// Update tab states
412
this.tabs.forEach((tab, i) => {
413
tab.classList.toggle('active', i === index);
414
});
415
416
// Update panel visibility
417
this.panels.forEach((panel, i) => {
418
panel.style.display = i === index ? 'block' : 'none';
419
});
420
}
421
422
render() {
423
return html`
424
<div class="tab-header">
425
<slot name="tab" @slotchange=${this._handleTabsChange}></slot>
426
</div>
427
<div class="tab-content">
428
<slot name="panel" @slotchange=${this._handlePanelsChange}></slot>
429
</div>
430
<div class="special-content">
431
<slot @slotchange=${this._handleSpecialChange}></slot>
432
</div>
433
`;
434
}
435
436
private _handleTabsChange() {
437
this.tabs.forEach((tab, index) => {
438
tab.addEventListener('click', () => this._selectTab(index));
439
});
440
}
441
442
private _handlePanelsChange() {
443
this._selectTab(this._activeTab);
444
}
445
446
private _handleSpecialChange() {
447
console.log('Special elements:', this.specialElements);
448
}
449
}
450
451
// Usage:
452
// <tab-container>
453
// <button slot="tab">Tab 1</button>
454
// <button slot="tab">Tab 2</button>
455
// <div slot="panel">Panel 1 content</div>
456
// <div slot="panel">Panel 2 content</div>
457
// <div class="special">Special content</div>
458
// </tab-container>
459
```
460
461
### Query Assigned Nodes Decorator
462
463
Queries for nodes (including text nodes) assigned to a slot.
464
465
```typescript { .api }
466
/**
467
* Property decorator for querying slot assigned nodes
468
* @param options Query options including slot name and flatten
469
* @returns Property decorator function
470
*/
471
function queryAssignedNodes(options?: QueryAssignedNodesOptions): PropertyDecorator;
472
473
interface QueryAssignedNodesOptions {
474
slot?: string;
475
flatten?: boolean;
476
}
477
```
478
479
**Usage Examples:**
480
481
```typescript
482
import { LitElement, html, customElement, queryAssignedNodes } from "lit-element";
483
484
@customElement("content-analyzer")
485
class ContentAnalyzer extends LitElement {
486
@queryAssignedNodes()
487
allNodes!: Node[];
488
489
@queryAssignedNodes({ slot: 'header' })
490
headerNodes!: Node[];
491
492
@queryAssignedNodes({ flatten: true })
493
flattenedNodes!: Node[];
494
495
private _analyzeContent() {
496
console.log('All nodes:', this.allNodes);
497
console.log('Text nodes:', this.allNodes.filter(node => node.nodeType === Node.TEXT_NODE));
498
console.log('Element nodes:', this.allNodes.filter(node => node.nodeType === Node.ELEMENT_NODE));
499
console.log('Header nodes:', this.headerNodes);
500
}
501
502
render() {
503
return html`
504
<div>
505
<button @click=${this._analyzeContent}>Analyze Content</button>
506
<div class="header-section">
507
<slot name="header" @slotchange=${this._handleSlotChange}></slot>
508
</div>
509
<div class="main-content">
510
<slot @slotchange=${this._handleSlotChange}></slot>
511
</div>
512
</div>
513
`;
514
}
515
516
private _handleSlotChange() {
517
this._analyzeContent();
518
}
519
}
520
```
521
522
### Event Options Decorator
523
524
Configures event listener options for methods.
525
526
```typescript { .api }
527
/**
528
* Method decorator for configuring event listener options
529
* @param options Event listener options
530
* @returns Method decorator function
531
*/
532
function eventOptions(options: AddEventListenerOptions): MethodDecorator;
533
534
interface AddEventListenerOptions {
535
capture?: boolean;
536
once?: boolean;
537
passive?: boolean;
538
signal?: AbortSignal;
539
}
540
```
541
542
**Usage Examples:**
543
544
```typescript
545
import { LitElement, html, customElement, eventOptions } from "lit-element";
546
547
@customElement("event-element")
548
class EventElement extends LitElement {
549
@eventOptions({ passive: true })
550
private _handleScroll(e: Event) {
551
// Passive scroll handling for better performance
552
console.log('Scroll event (passive)');
553
}
554
555
@eventOptions({ once: true })
556
private _handleFirstClick(e: Event) {
557
// This handler will only run once
558
console.log('First click only');
559
}
560
561
@eventOptions({ capture: true })
562
private _handleCaptureClick(e: Event) {
563
// Handle in capture phase
564
console.log('Capture phase click');
565
}
566
567
render() {
568
return html`
569
<div
570
@scroll=${this._handleScroll}
571
@click=${this._handleFirstClick}
572
@click=${this._handleCaptureClick}
573
style="height: 200px; overflow-y: scroll; border: 1px solid #ccc;"
574
>
575
<div style="height: 500px; padding: 16px;">
576
<p>Scrollable content with event handling</p>
577
<button>Click me (once only + capture)</button>
578
</div>
579
</div>
580
`;
581
}
582
}
583
```