0
# Template Directives
1
2
Built-in template directives for common patterns like conditionals, loops, references, and DOM node observation, providing powerful declarative capabilities for dynamic UI construction.
3
4
## Capabilities
5
6
### When Directive
7
8
Conditional rendering directive that shows or hides template content based on boolean expressions, with optional else templates.
9
10
```typescript { .api }
11
/**
12
* A directive that enables basic conditional rendering in a template
13
* @param condition - The condition to test for rendering
14
* @param templateOrTemplateBinding - The template to render when condition is true
15
* @param elseTemplateOrTemplateBinding - Optional template to render when condition is false
16
* @returns A capture type for template interpolation
17
*/
18
function when<TSource = any, TReturn = any, TParent = any>(
19
condition: Expression<TSource, TReturn, TParent> | boolean,
20
templateOrTemplateBinding:
21
| SyntheticViewTemplate<TSource, TParent>
22
| Expression<TSource, SyntheticViewTemplate<TSource, TParent>, TParent>,
23
elseTemplateOrTemplateBinding?:
24
| SyntheticViewTemplate<TSource, TParent>
25
| Expression<TSource, SyntheticViewTemplate<TSource, TParent>, TParent>
26
): CaptureType<TSource, TParent>;
27
```
28
29
**Usage Examples:**
30
31
```typescript
32
import { FASTElement, customElement, html, when, attr, observable } from "@microsoft/fast-element";
33
34
const template = html<ConditionalExample>`
35
<div class="container">
36
<!-- Simple boolean condition -->
37
${when(x => x.isVisible, html`<p>This content is visible!</p>`)}
38
39
<!-- Condition with else template -->
40
${when(
41
x => x.user,
42
html`<p>Welcome, ${x => x.user.name}!</p>`,
43
html`<p>Please log in.</p>`
44
)}
45
46
<!-- Complex condition -->
47
${when(
48
x => x.items.length > 0,
49
html`
50
<div class="items-container">
51
<h3>Items (${x => x.items.length})</h3>
52
<ul>
53
${x => x.items.map(item => `<li>${item}</li>`).join('')}
54
</ul>
55
</div>
56
`,
57
html`<p class="empty">No items available.</p>`
58
)}
59
60
<!-- Nested conditions -->
61
${when(
62
x => x.isLoggedIn,
63
html`
64
<div class="user-area">
65
${when(
66
x => x.isAdmin,
67
html`<button class="admin-btn">Admin Panel</button>`
68
)}
69
${when(
70
x => x.notifications.length > 0,
71
html`
72
<div class="notifications">
73
${x => x.notifications.length} new notification(s)
74
</div>
75
`
76
)}
77
</div>
78
`
79
)}
80
81
<!-- Dynamic template selection -->
82
${when(
83
x => x.viewMode === 'list',
84
x => x.listTemplate,
85
x => x.gridTemplate
86
)}
87
88
<!-- Multiple conditions -->
89
${when(
90
x => x.status === 'loading',
91
html`<div class="spinner">Loading...</div>`
92
)}
93
${when(
94
x => x.status === 'error',
95
html`<div class="error">Error: ${x => x.errorMessage}</div>`
96
)}
97
${when(
98
x => x.status === 'success',
99
html`<div class="success">Operation completed!</div>`
100
)}
101
</div>
102
`;
103
104
@customElement({
105
name: "conditional-example",
106
template
107
})
108
export class ConditionalExample extends FASTElement {
109
@observable isVisible: boolean = true;
110
@observable isLoggedIn: boolean = false;
111
@observable isAdmin: boolean = false;
112
@observable user: { name: string } | null = null;
113
@observable items: string[] = [];
114
@observable notifications: string[] = [];
115
@observable viewMode: 'list' | 'grid' = 'list';
116
@observable status: 'loading' | 'error' | 'success' | 'idle' = 'idle';
117
@observable errorMessage: string = "";
118
119
// Dynamic templates
120
listTemplate = html`<div class="list-view">List View Content</div>`;
121
gridTemplate = html`<div class="grid-view">Grid View Content</div>`;
122
123
// Methods to change state
124
toggleVisibility() {
125
this.isVisible = !this.isVisible;
126
}
127
128
login(user: { name: string }) {
129
this.user = user;
130
this.isLoggedIn = true;
131
}
132
133
logout() {
134
this.user = null;
135
this.isLoggedIn = false;
136
this.isAdmin = false;
137
}
138
}
139
140
// Advanced conditional patterns
141
const advancedTemplate = html<ConditionalExample>`
142
<!-- Conditional classes -->
143
<div class="${x => when(x.isActive, 'active', 'inactive')}">
144
Content with conditional styling
145
</div>
146
147
<!-- Conditional attributes -->
148
<button ?disabled="${x => !x.canSubmit}">
149
${when(x => x.isSubmitting, 'Submitting...', 'Submit')}
150
</button>
151
152
<!-- Conditional content with interpolation -->
153
${when(
154
x => x.errorCount > 0,
155
html`
156
<div class="error-summary">
157
${x => x.errorCount === 1 ? '1 error' : `${x.errorCount} errors`} found:
158
<ul>
159
${x => x.errors.map(error => `<li>${error}</li>`).join('')}
160
</ul>
161
</div>
162
`
163
)}
164
`;
165
```
166
167
### Repeat Directive
168
169
Loop directive for rendering template content for each item in an array, with efficient DOM updates and optional view recycling.
170
171
```typescript { .api }
172
/**
173
* A directive that renders a template for each item in an array
174
* @param expression - Expression that returns the array to iterate over
175
* @param template - The template to render for each item
176
* @param options - Configuration options for repeat behavior
177
* @returns A RepeatDirective instance
178
*/
179
function repeat<TSource = any, TParent = any>(
180
expression: Expression<any[], TSource, TParent>,
181
template: SyntheticViewTemplate<any, TSource>,
182
options?: RepeatOptions
183
): RepeatDirective<TSource, TParent>;
184
185
/**
186
* Options for configuring repeat behavior
187
*/
188
interface RepeatOptions {
189
/** Enables index, length, and dependent positioning updates in item templates */
190
positioning?: boolean;
191
192
/** Enables view recycling for performance */
193
recycle?: boolean;
194
}
195
196
/**
197
* Directive class for repeat functionality
198
*/
199
class RepeatDirective<TSource = any, TParent = any> implements HTMLDirective {
200
/**
201
* Creates HTML for the repeat directive
202
* @param add - Function to add view behavior factories
203
*/
204
createHTML(add: AddViewBehaviorFactory): string;
205
}
206
207
/**
208
* Behavior that manages the repeat rendering lifecycle
209
*/
210
class RepeatBehavior<TSource = any> implements ViewBehavior, Subscriber {
211
/**
212
* Binds the repeat behavior to a controller
213
* @param controller - The view controller
214
*/
215
bind(controller: ViewController): void;
216
217
/**
218
* Unbinds the repeat behavior
219
* @param controller - The view controller
220
*/
221
unbind(controller: ViewController): void;
222
223
/**
224
* Handles changes to the source array
225
* @param source - The source of the change
226
* @param args - Change arguments (splice, sort, etc.)
227
*/
228
handleChange(source: any, args: any): void;
229
}
230
```
231
232
**Usage Examples:**
233
234
```typescript
235
import { FASTElement, customElement, html, repeat, observable } from "@microsoft/fast-element";
236
237
// Item template
238
const itemTemplate = html<Item>`
239
<div class="item" data-id="${x => x.id}">
240
<h3>${x => x.title}</h3>
241
<p>${x => x.description}</p>
242
<span class="price">$${x => x.price}</span>
243
</div>
244
`;
245
246
// User template with context access
247
const userTemplate = html<User, UserListComponent>`
248
<tr class="user-row">
249
<td>${x => x.name}</td>
250
<td>${x => x.email}</td>
251
<td>${(x, c) => c.index + 1}</td>
252
<td>
253
<button @click="${(x, c) => c.parent.editUser(x)}">Edit</button>
254
<button @click="${(x, c) => c.parent.deleteUser(x.id)}">Delete</button>
255
</td>
256
</tr>
257
`;
258
259
// Advanced positioning template
260
const positionedTemplate = html<string, PositionedList>`
261
<li class="list-item ${(x, c) => c.isFirst ? 'first' : ''} ${(x, c) => c.isLast ? 'last' : ''}">
262
<span class="index">${(x, c) => c.index + 1}</span>
263
<span class="content">${x => x}</span>
264
<span class="total">of ${(x, c) => c.length}</span>
265
</li>
266
`;
267
268
const template = html<RepeatExample>`
269
<div class="repeat-examples">
270
<!-- Basic repeat -->
271
<section class="basic-repeat">
272
<h2>Basic Items</h2>
273
<div class="items-grid">
274
${repeat(x => x.items, itemTemplate)}
275
</div>
276
</section>
277
278
<!-- Repeat with positioning -->
279
<section class="positioned-repeat">
280
<h2>Positioned List</h2>
281
<ol class="positioned-list">
282
${repeat(x => x.names, positionedTemplate, { positioning: true })}
283
</ol>
284
</section>
285
286
<!-- Users table with context -->
287
<section class="users-table">
288
<h2>Users</h2>
289
<table>
290
<thead>
291
<tr>
292
<th>Name</th>
293
<th>Email</th>
294
<th>#</th>
295
<th>Actions</th>
296
</tr>
297
</thead>
298
<tbody>
299
${repeat(x => x.users, userTemplate, { positioning: true, recycle: true })}
300
</tbody>
301
</table>
302
</section>
303
304
<!-- Nested repeats -->
305
<section class="nested-repeat">
306
<h2>Categories</h2>
307
${repeat(
308
x => x.categories,
309
html<Category, RepeatExample>`
310
<div class="category">
311
<h3>${x => x.name}</h3>
312
<div class="category-items">
313
${repeat(x => x.items, itemTemplate)}
314
</div>
315
</div>
316
`
317
)}
318
</section>
319
320
<!-- Conditional repeat -->
321
<section class="conditional-repeat">
322
<h2>Search Results</h2>
323
${when(
324
x => x.searchResults.length > 0,
325
html`
326
<div class="results">
327
${repeat(x => x.searchResults, itemTemplate)}
328
</div>
329
`,
330
html`<p class="no-results">No results found.</p>`
331
)}
332
</section>
333
</div>
334
335
<div class="controls">
336
<button @click="${x => x.addItem()}">Add Item</button>
337
<button @click="${x => x.removeItem()}">Remove Item</button>
338
<button @click="${x => x.shuffleItems()}">Shuffle</button>
339
<button @click="${x => x.clearItems()}">Clear All</button>
340
</div>
341
`;
342
343
@customElement({
344
name: "repeat-example",
345
template
346
})
347
export class RepeatExample extends FASTElement {
348
@observable items: Item[] = [
349
{ id: 1, title: "Item 1", description: "Description 1", price: 10.99 },
350
{ id: 2, title: "Item 2", description: "Description 2", price: 15.99 },
351
{ id: 3, title: "Item 3", description: "Description 3", price: 8.99 }
352
];
353
354
@observable names: string[] = ["Alice", "Bob", "Charlie", "Diana"];
355
356
@observable users: User[] = [
357
{ id: 1, name: "John Doe", email: "john@example.com" },
358
{ id: 2, name: "Jane Smith", email: "jane@example.com" }
359
];
360
361
@observable categories: Category[] = [
362
{
363
name: "Electronics",
364
items: [
365
{ id: 4, title: "Laptop", description: "Gaming laptop", price: 999.99 },
366
{ id: 5, title: "Mouse", description: "Wireless mouse", price: 29.99 }
367
]
368
}
369
];
370
371
@observable searchResults: Item[] = [];
372
373
// Array manipulation methods
374
addItem() {
375
const newItem: Item = {
376
id: Date.now(),
377
title: `Item ${this.items.length + 1}`,
378
description: `Description ${this.items.length + 1}`,
379
price: Math.random() * 100
380
};
381
this.items.push(newItem);
382
}
383
384
removeItem() {
385
if (this.items.length > 0) {
386
this.items.pop();
387
}
388
}
389
390
shuffleItems() {
391
this.items = [...this.items].sort(() => Math.random() - 0.5);
392
}
393
394
clearItems() {
395
this.items = [];
396
}
397
398
editUser(user: User) {
399
console.log("Edit user:", user);
400
}
401
402
deleteUser(userId: number) {
403
this.users = this.users.filter(user => user.id !== userId);
404
}
405
}
406
407
// Performance-optimized repeat with recycling
408
const recycledTemplate = html<LargeDataItem>`
409
<div class="data-item">
410
<span class="id">${x => x.id}</span>
411
<span class="value">${x => x.value}</span>
412
<span class="timestamp">${x => x.timestamp.toLocaleString()}</span>
413
</div>
414
`;
415
416
@customElement("performance-repeat")
417
export class PerformanceRepeatExample extends FASTElement {
418
@observable largeDataSet: LargeDataItem[] = [];
419
420
connectedCallback() {
421
super.connectedCallback();
422
this.generateLargeDataSet();
423
}
424
425
generateLargeDataSet() {
426
const items: LargeDataItem[] = [];
427
for (let i = 0; i < 10000; i++) {
428
items.push({
429
id: i,
430
value: Math.random() * 1000,
431
timestamp: new Date()
432
});
433
}
434
this.largeDataSet = items;
435
}
436
437
static template = html<PerformanceRepeatExample>`
438
<div class="performance-demo">
439
<p>Rendering ${x => x.largeDataSet.length} items with recycling enabled</p>
440
<div class="large-list" style="height: 400px; overflow-y: auto;">
441
${repeat(x => x.largeDataSet, recycledTemplate, {
442
positioning: false,
443
recycle: true
444
})}
445
</div>
446
</div>
447
`;
448
}
449
450
interface Item {
451
id: number;
452
title: string;
453
description: string;
454
price: number;
455
}
456
457
interface User {
458
id: number;
459
name: string;
460
email: string;
461
}
462
463
interface Category {
464
name: string;
465
items: Item[];
466
}
467
468
interface LargeDataItem {
469
id: number;
470
value: number;
471
timestamp: Date;
472
}
473
```
474
475
### Ref Directive
476
477
Reference directive for obtaining direct access to DOM elements within templates, enabling imperative DOM operations when needed.
478
479
```typescript { .api }
480
/**
481
* A directive that captures a reference to the DOM element
482
* @param propertyName - The property name on the source to assign the element to
483
* @returns A RefDirective instance
484
*/
485
function ref<TSource = any, TParent = any>(
486
propertyName: keyof TSource
487
): RefDirective;
488
489
/**
490
* Directive class for element references
491
*/
492
class RefDirective implements HTMLDirective {
493
/**
494
* Creates HTML for the ref directive
495
* @param add - Function to add view behavior factories
496
*/
497
createHTML(add: AddViewBehaviorFactory): string;
498
}
499
```
500
501
**Usage Examples:**
502
503
```typescript
504
import { FASTElement, customElement, html, ref, observable } from "@microsoft/fast-element";
505
506
const template = html<RefExample>`
507
<div class="ref-examples">
508
<!-- Basic element reference -->
509
<input ${ref("nameInput")}
510
type="text"
511
placeholder="Enter your name"
512
@input="${x => x.handleNameInput()}">
513
514
<!-- Canvas reference for drawing -->
515
<canvas ${ref("canvas")}
516
width="400"
517
height="200"
518
style="border: 1px solid #ccc;">
519
</canvas>
520
521
<!-- Video element reference -->
522
<video ${ref("videoPlayer")}
523
controls
524
width="400">
525
<source src="sample.mp4" type="video/mp4">
526
</video>
527
528
<!-- Form reference for validation -->
529
<form ${ref("form")} @submit="${x => x.handleSubmit}">
530
<input ${ref("emailInput")} type="email" required>
531
<button type="submit">Submit</button>
532
</form>
533
534
<!-- Multiple refs for focus management -->
535
<div class="focus-chain">
536
<input ${ref("input1")} placeholder="First">
537
<input ${ref("input2")} placeholder="Second">
538
<input ${ref("input3")} placeholder="Third">
539
</div>
540
541
<!-- Container for dynamic content -->
542
<div ${ref("dynamicContainer")} class="dynamic-content"></div>
543
</div>
544
545
<div class="controls">
546
<button @click="${x => x.focusName()}">Focus Name</button>
547
<button @click="${x => x.drawOnCanvas()}">Draw</button>
548
<button @click="${x => x.playVideo()}">Play Video</button>
549
<button @click="${x => x.validateForm()}">Validate</button>
550
<button @click="${x => x.cycleFocus()}">Cycle Focus</button>
551
<button @click="${x => x.addDynamicContent()}">Add Content</button>
552
</div>
553
`;
554
555
@customElement({
556
name: "ref-example",
557
template
558
})
559
export class RefExample extends FASTElement {
560
// Element references
561
nameInput!: HTMLInputElement;
562
canvas!: HTMLCanvasElement;
563
videoPlayer!: HTMLVideoElement;
564
form!: HTMLFormElement;
565
emailInput!: HTMLInputElement;
566
input1!: HTMLInputElement;
567
input2!: HTMLInputElement;
568
input3!: HTMLInputElement;
569
dynamicContainer!: HTMLDivElement;
570
571
@observable currentFocusIndex: number = 0;
572
573
// Methods using element references
574
focusName() {
575
if (this.nameInput) {
576
this.nameInput.focus();
577
this.nameInput.select();
578
}
579
}
580
581
handleNameInput() {
582
if (this.nameInput) {
583
console.log("Name input value:", this.nameInput.value);
584
}
585
}
586
587
drawOnCanvas() {
588
if (this.canvas) {
589
const ctx = this.canvas.getContext('2d');
590
if (ctx) {
591
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
592
ctx.fillStyle = '#007ACC';
593
ctx.fillRect(50, 50, 100, 100);
594
ctx.fillStyle = 'white';
595
ctx.font = '16px Arial';
596
ctx.fillText('FAST Element', 60, 110);
597
}
598
}
599
}
600
601
playVideo() {
602
if (this.videoPlayer) {
603
this.videoPlayer.play().catch(console.error);
604
}
605
}
606
607
validateForm() {
608
if (this.form) {
609
const isValid = this.form.checkValidity();
610
console.log("Form is valid:", isValid);
611
612
if (!isValid) {
613
this.form.reportValidity();
614
}
615
}
616
}
617
618
handleSubmit(event: Event) {
619
event.preventDefault();
620
if (this.emailInput) {
621
console.log("Email submitted:", this.emailInput.value);
622
}
623
}
624
625
cycleFocus() {
626
const inputs = [this.input1, this.input2, this.input3];
627
const currentInput = inputs[this.currentFocusIndex];
628
629
if (currentInput) {
630
currentInput.focus();
631
this.currentFocusIndex = (this.currentFocusIndex + 1) % inputs.length;
632
}
633
}
634
635
addDynamicContent() {
636
if (this.dynamicContainer) {
637
const element = document.createElement('div');
638
element.textContent = `Dynamic content added at ${new Date().toLocaleTimeString()}`;
639
element.style.cssText = 'padding: 8px; margin: 4px; background: #f0f0f0; border-radius: 4px;';
640
this.dynamicContainer.appendChild(element);
641
}
642
}
643
}
644
645
// Advanced ref usage with custom elements
646
@customElement("advanced-ref-example")
647
export class AdvancedRefExample extends FASTElement {
648
chartContainer!: HTMLDivElement;
649
editorContainer!: HTMLDivElement;
650
651
private chart?: any; // External chart library instance
652
private editor?: any; // External editor library instance
653
654
connectedCallback() {
655
super.connectedCallback();
656
657
// Wait for next frame to ensure refs are available
658
requestAnimationFrame(() => {
659
this.initializeChart();
660
this.initializeEditor();
661
});
662
}
663
664
disconnectedCallback() {
665
super.disconnectedCallback();
666
667
// Clean up external library instances
668
if (this.chart) {
669
this.chart.destroy();
670
}
671
if (this.editor) {
672
this.editor.destroy();
673
}
674
}
675
676
private initializeChart() {
677
if (this.chartContainer) {
678
// Initialize external chart library
679
// this.chart = new ChartLibrary(this.chartContainer, options);
680
console.log("Chart initialized in:", this.chartContainer);
681
}
682
}
683
684
private initializeEditor() {
685
if (this.editorContainer) {
686
// Initialize external editor library
687
// this.editor = new EditorLibrary(this.editorContainer, options);
688
console.log("Editor initialized in:", this.editorContainer);
689
}
690
}
691
692
static template = html<AdvancedRefExample>`
693
<div class="advanced-refs">
694
<div ${ref("chartContainer")} class="chart-container"></div>
695
<div ${ref("editorContainer")} class="editor-container"></div>
696
</div>
697
`;
698
}
699
```
700
701
### Children Directive
702
703
Directive for observing child elements, providing reactive access to element children with filtering and observation capabilities.
704
705
```typescript { .api }
706
/**
707
* A directive that observes the child nodes of an element
708
* @param propertyName - The property name to assign the child collection to
709
* @param options - Configuration options for child observation
710
* @returns A ChildrenDirective instance
711
*/
712
function children<TSource = any, TParent = any>(
713
propertyName: keyof TSource,
714
options?: ChildrenDirectiveOptions
715
): ChildrenDirective;
716
717
/**
718
* Options for configuring child observation
719
*/
720
interface ChildrenDirectiveOptions {
721
/** CSS selector to filter child elements */
722
filter?: ElementsFilter;
723
724
/** Whether to observe all descendants (subtree) */
725
subtree?: boolean;
726
727
/** Specific child list options */
728
childList?: boolean;
729
730
/** Attribute change observation */
731
attributes?: boolean;
732
733
/** Character data change observation */
734
characterData?: boolean;
735
}
736
737
/**
738
* Interface for filtering elements
739
*/
740
interface ElementsFilter {
741
/**
742
* Filters elements based on criteria
743
* @param node - The node to evaluate
744
* @param index - The index of the node
745
* @param nodes - All nodes being filtered
746
*/
747
(node: Node, index: number, nodes: Node[]): boolean;
748
}
749
750
/**
751
* Directive class for child observation
752
*/
753
class ChildrenDirective implements HTMLDirective {
754
/**
755
* Creates HTML for the children directive
756
* @param add - Function to add view behavior factories
757
*/
758
createHTML(add: AddViewBehaviorFactory): string;
759
}
760
```
761
762
**Usage Examples:**
763
764
```typescript
765
import { FASTElement, customElement, html, children, observable } from "@microsoft/fast-element";
766
767
// Element filter functions
768
const buttonFilter = (node: Node): node is HTMLElement =>
769
node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName === 'BUTTON';
770
771
const inputFilter = (node: Node): node is HTMLInputElement =>
772
node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName === 'INPUT';
773
774
const template = html<ChildrenExample>`
775
<div class="children-examples">
776
<!-- Observe all child elements -->
777
<div ${children("allChildren")} class="all-children-container">
778
<p>This paragraph will be observed</p>
779
<span>This span will be observed</span>
780
<button>This button will be observed</button>
781
</div>
782
783
<!-- Observe only button children -->
784
<div ${children("buttons", { filter: buttonFilter })} class="buttons-container">
785
<button>Button 1</button>
786
<span>Not a button</span>
787
<button>Button 2</button>
788
<p>Not a button either</p>
789
<button>Button 3</button>
790
</div>
791
792
<!-- Observe form inputs -->
793
<form ${children("formInputs", { filter: inputFilter })} class="form-container">
794
<input type="text" placeholder="Name">
795
<label>Not an input</label>
796
<input type="email" placeholder="Email">
797
<textarea placeholder="Comments"></textarea>
798
<input type="submit" value="Submit">
799
</form>
800
801
<!-- Observe with subtree option -->
802
<div ${children("allDescendants", { subtree: true })} class="subtree-container">
803
<div class="level-1">
804
<p>Level 1 paragraph</p>
805
<div class="level-2">
806
<span>Level 2 span</span>
807
<div class="level-3">
808
<strong>Level 3 strong</strong>
809
</div>
810
</div>
811
</div>
812
</div>
813
</div>
814
815
<div class="children-info">
816
<p>All children count: ${x => x.allChildren?.length ?? 0}</p>
817
<p>Button count: ${x => x.buttons?.length ?? 0}</p>
818
<p>Form inputs count: ${x => x.formInputs?.length ?? 0}</p>
819
<p>All descendants count: ${x => x.allDescendants?.length ?? 0}</p>
820
</div>
821
822
<div class="controls">
823
<button @click="${x => x.addChild()}">Add Child</button>
824
<button @click="${x => x.addButton()}">Add Button</button>
825
<button @click="${x => x.addInput()}">Add Input</button>
826
<button @click="${x => x.removeChildren()}">Remove Children</button>
827
</div>
828
`;
829
830
@customElement({
831
name: "children-example",
832
template
833
})
834
export class ChildrenExample extends FASTElement {
835
// Child collections populated by children directive
836
allChildren!: HTMLElement[];
837
buttons!: HTMLButtonElement[];
838
formInputs!: HTMLInputElement[];
839
allDescendants!: HTMLElement[];
840
841
private get allChildrenContainer(): HTMLElement {
842
return this.shadowRoot?.querySelector('.all-children-container') as HTMLElement;
843
}
844
845
private get buttonsContainer(): HTMLElement {
846
return this.shadowRoot?.querySelector('.buttons-container') as HTMLElement;
847
}
848
849
private get formContainer(): HTMLElement {
850
return this.shadowRoot?.querySelector('.form-container') as HTMLElement;
851
}
852
853
connectedCallback() {
854
super.connectedCallback();
855
856
// React to children changes
857
this.setupChildrenObservers();
858
}
859
860
private setupChildrenObservers() {
861
// Watch for changes in child collections
862
// These will be automatically updated by the children directive
863
864
// You can add custom logic here to react to children changes
865
const observer = new MutationObserver((mutations) => {
866
mutations.forEach(mutation => {
867
if (mutation.type === 'childList') {
868
console.log('Children changed:', mutation);
869
}
870
});
871
});
872
873
// Observe changes to containers
874
if (this.allChildrenContainer) {
875
observer.observe(this.allChildrenContainer, { childList: true });
876
}
877
}
878
879
addChild() {
880
if (this.allChildrenContainer) {
881
const child = document.createElement('div');
882
child.textContent = `Child ${this.allChildren?.length ?? 0 + 1}`;
883
child.style.cssText = 'padding: 4px; margin: 2px; background: #e0e0e0;';
884
this.allChildrenContainer.appendChild(child);
885
}
886
}
887
888
addButton() {
889
if (this.buttonsContainer) {
890
const button = document.createElement('button');
891
button.textContent = `Button ${this.buttons?.length ?? 0 + 1}`;
892
this.buttonsContainer.appendChild(button);
893
}
894
}
895
896
addInput() {
897
if (this.formContainer) {
898
const input = document.createElement('input');
899
input.type = 'text';
900
input.placeholder = `Input ${this.formInputs?.length ?? 0 + 1}`;
901
this.formContainer.appendChild(input);
902
}
903
}
904
905
removeChildren() {
906
// Remove last child from each container
907
if (this.allChildren?.length > 3) { // Keep original children
908
this.allChildrenContainer.removeChild(
909
this.allChildrenContainer.lastElementChild!
910
);
911
}
912
913
if (this.buttons?.length > 3) { // Keep original buttons
914
this.buttonsContainer.removeChild(
915
this.buttonsContainer.lastElementChild!
916
);
917
}
918
919
if (this.formInputs?.length > 3) { // Keep original inputs
920
this.formContainer.removeChild(
921
this.formContainer.lastElementChild!
922
);
923
}
924
}
925
}
926
927
// Advanced children observation with custom filtering
928
@customElement("advanced-children-example")
929
export class AdvancedChildrenExample extends FASTElement {
930
// Custom filter for data elements
931
customElements!: HTMLElement[];
932
933
static customElementFilter = (node: Node): node is HTMLElement => {
934
if (node.nodeType !== Node.ELEMENT_NODE) return false;
935
const element = node as HTMLElement;
936
return element.hasAttribute('data-item') ||
937
element.classList.contains('custom-item');
938
};
939
940
static template = html<AdvancedChildrenExample>`
941
<div ${children("customElements", {
942
filter: AdvancedChildrenExample.customElementFilter
943
})} class="custom-container">
944
<div data-item="1">Custom Item 1</div>
945
<div>Regular div</div>
946
<div class="custom-item">Custom Item 2</div>
947
<p>Regular paragraph</p>
948
<div data-item="2" class="custom-item">Custom Item 3</div>
949
</div>
950
951
<p>Custom elements found: ${x => x.customElements?.length ?? 0}</p>
952
`;
953
}
954
```
955
956
### Slotted Directive
957
958
Directive for observing slotted content in Shadow DOM, providing reactive access to distributed content with filtering capabilities.
959
960
```typescript { .api }
961
/**
962
* A directive that observes slotted content
963
* @param propertyName - The property name to assign slotted elements to
964
* @param options - Configuration options for slotted observation
965
* @returns A SlottedDirective instance
966
*/
967
function slotted<TSource = any, TParent = any>(
968
propertyName: keyof TSource,
969
options?: SlottedDirectiveOptions
970
): SlottedDirective;
971
972
/**
973
* Options for configuring slotted content observation
974
*/
975
interface SlottedDirectiveOptions {
976
/** CSS selector to filter slotted elements */
977
filter?: ElementsFilter;
978
979
/** Whether to flatten assigned nodes */
980
flatten?: boolean;
981
}
982
983
/**
984
* Directive class for slotted content observation
985
*/
986
class SlottedDirective implements HTMLDirective {
987
/**
988
* Creates HTML for the slotted directive
989
* @param add - Function to add view behavior factories
990
*/
991
createHTML(add: AddViewBehaviorFactory): string;
992
}
993
```
994
995
**Usage Examples:**
996
997
```typescript
998
import { FASTElement, customElement, html, slotted, observable } from "@microsoft/fast-element";
999
1000
// Slot filters
1001
const headerFilter = (node: Node): node is HTMLElement =>
1002
node.nodeType === Node.ELEMENT_NODE &&
1003
['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes((node as Element).tagName);
1004
1005
const buttonFilter = (node: Node): node is HTMLButtonElement =>
1006
node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName === 'BUTTON';
1007
1008
const template = html<SlottedExample>`
1009
<div class="slotted-container">
1010
<!-- Default slot observation -->
1011
<div class="content-section">
1012
<h2>Content</h2>
1013
<slot ${slotted("defaultSlottedContent")}></slot>
1014
</div>
1015
1016
<!-- Named slot with header filter -->
1017
<header class="header-section">
1018
<slot name="header" ${slotted("headerContent", { filter: headerFilter })}></slot>
1019
</header>
1020
1021
<!-- Action slot with button filter -->
1022
<footer class="actions-section">
1023
<slot name="actions" ${slotted("actionButtons", { filter: buttonFilter })}></slot>
1024
</footer>
1025
1026
<!-- Sidebar slot with flattening -->
1027
<aside class="sidebar">
1028
<slot name="sidebar" ${slotted("sidebarContent", { flatten: true })}></slot>
1029
</aside>
1030
</div>
1031
1032
<div class="slot-info">
1033
<p>Default content items: ${x => x.defaultSlottedContent?.length ?? 0}</p>
1034
<p>Header elements: ${x => x.headerContent?.length ?? 0}</p>
1035
<p>Action buttons: ${x => x.actionButtons?.length ?? 0}</p>
1036
<p>Sidebar items: ${x => x.sidebarContent?.length ?? 0}</p>
1037
</div>
1038
`;
1039
1040
@customElement({
1041
name: "slotted-example",
1042
template
1043
})
1044
export class SlottedExample extends FASTElement {
1045
// Slotted content collections
1046
defaultSlottedContent!: HTMLElement[];
1047
headerContent!: HTMLElement[];
1048
actionButtons!: HTMLButtonElement[];
1049
sidebarContent!: HTMLElement[];
1050
1051
connectedCallback() {
1052
super.connectedCallback();
1053
this.setupSlotObservers();
1054
}
1055
1056
private setupSlotObservers() {
1057
// React to slotted content changes
1058
this.addEventListener('slotchange', (e) => {
1059
const slot = e.target as HTMLSlotElement;
1060
console.log(`Slot '${slot.name || 'default'}' content changed`);
1061
1062
// Perform actions based on slotted content
1063
this.updateSlottedContentStyles();
1064
});
1065
}
1066
1067
private updateSlottedContentStyles() {
1068
// Apply styles based on slotted content
1069
if (this.headerContent?.length > 0) {
1070
this.headerContent.forEach((header, index) => {
1071
header.style.color = index === 0 ? '#007ACC' : '#666';
1072
});
1073
}
1074
1075
if (this.actionButtons?.length > 0) {
1076
this.actionButtons.forEach((button, index) => {
1077
button.classList.toggle('primary', index === 0);
1078
});
1079
}
1080
}
1081
}
1082
1083
// Usage of slotted component
1084
@customElement("slotted-consumer")
1085
export class SlottedConsumer extends FASTElement {
1086
static template = html`
1087
<div>
1088
<h1>Using Slotted Component</h1>
1089
1090
<slotted-example>
1091
<!-- Default slot content -->
1092
<p>This goes in the default slot</p>
1093
<div>Another default slot item</div>
1094
1095
<!-- Named slot content -->
1096
<h1 slot="header">Main Header</h1>
1097
<h2 slot="header">Sub Header</h2>
1098
1099
<!-- Action slot content -->
1100
<button slot="actions" @click="${() => console.log('Save')}">Save</button>
1101
<button slot="actions" @click="${() => console.log('Cancel')}">Cancel</button>
1102
1103
<!-- Sidebar slot content -->
1104
<nav slot="sidebar">
1105
<ul>
1106
<li><a href="#home">Home</a></li>
1107
<li><a href="#about">About</a></li>
1108
<li><a href="#contact">Contact</a></li>
1109
</ul>
1110
</nav>
1111
</slotted-example>
1112
</div>
1113
`;
1114
}
1115
1116
// Advanced slotted content management
1117
@customElement("advanced-slotted")
1118
export class AdvancedSlottedExample extends FASTElement {
1119
allSlottedContent!: Node[];
1120
imageContent!: HTMLImageElement[];
1121
linkContent!: HTMLAnchorElement[];
1122
1123
@observable hasImages: boolean = false;
1124
@observable hasLinks: boolean = false;
1125
1126
// Custom filters
1127
static imageFilter = (node: Node): node is HTMLImageElement =>
1128
node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName === 'IMG';
1129
1130
static linkFilter = (node: Node): node is HTMLAnchorElement =>
1131
node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName === 'A';
1132
1133
slottedContentChanged() {
1134
// Update observable properties based on slotted content
1135
this.hasImages = this.imageContent?.length > 0;
1136
this.hasLinks = this.linkContent?.length > 0;
1137
1138
// Apply lazy loading to images
1139
if (this.imageContent) {
1140
this.imageContent.forEach(img => {
1141
if (!img.hasAttribute('loading')) {
1142
img.setAttribute('loading', 'lazy');
1143
}
1144
});
1145
}
1146
1147
// Add security attributes to external links
1148
if (this.linkContent) {
1149
this.linkContent.forEach(link => {
1150
if (link.hostname !== window.location.hostname) {
1151
link.setAttribute('rel', 'noopener noreferrer');
1152
link.setAttribute('target', '_blank');
1153
}
1154
});
1155
}
1156
}
1157
1158
static template = html<AdvancedSlottedExample>`
1159
<div class="advanced-slotted">
1160
<slot ${slotted("allSlottedContent")}
1161
@slotchange="${x => x.slottedContentChanged()}"></slot>
1162
1163
<!-- Hidden slots for filtered content -->
1164
<slot ${slotted("imageContent", { filter: AdvancedSlottedExample.imageFilter })}
1165
style="display: none;"></slot>
1166
<slot ${slotted("linkContent", { filter: AdvancedSlottedExample.linkFilter })}
1167
style="display: none;"></slot>
1168
1169
<div class="content-summary">
1170
${when(x => x.hasImages, html`<p>๐ธ Contains images</p>`)}
1171
${when(x => x.hasLinks, html`<p>๐ Contains links</p>`)}
1172
</div>
1173
</div>
1174
`;
1175
}
1176
```
1177
1178
## Types
1179
1180
```typescript { .api }
1181
/**
1182
* Base interface for HTML directives
1183
*/
1184
interface HTMLDirective {
1185
/**
1186
* Creates HTML to be used within a template
1187
* @param add - Can be used to add behavior factories to a template
1188
*/
1189
createHTML(add: AddViewBehaviorFactory): string;
1190
}
1191
1192
/**
1193
* Function for adding view behavior factories during template compilation
1194
*/
1195
interface AddViewBehaviorFactory {
1196
(factory: ViewBehaviorFactory): string;
1197
}
1198
1199
/**
1200
* Factory for creating view behaviors
1201
*/
1202
interface ViewBehaviorFactory {
1203
/** Unique identifier for the factory */
1204
id?: string;
1205
1206
/**
1207
* Creates a view behavior
1208
* @param targets - The targets for the behavior
1209
*/
1210
createBehavior(targets: ViewBehaviorTargets): ViewBehavior;
1211
}
1212
1213
/**
1214
* Targets for view behavior attachment
1215
*/
1216
interface ViewBehaviorTargets {
1217
[key: string]: Node;
1218
}
1219
1220
/**
1221
* Base interface for view behaviors
1222
*/
1223
interface ViewBehavior {
1224
/**
1225
* Binds the behavior to a controller
1226
* @param controller - The view controller
1227
*/
1228
bind(controller: ViewController): void;
1229
1230
/**
1231
* Unbinds the behavior from a controller
1232
* @param controller - The view controller
1233
*/
1234
unbind(controller: ViewController): void;
1235
}
1236
1237
/**
1238
* Controller for managing view lifecycle
1239
*/
1240
interface ViewController {
1241
/** The source data */
1242
source: any;
1243
1244
/** Execution context */
1245
context: ExecutionContext;
1246
1247
/** Whether the controller is bound */
1248
isBound: boolean;
1249
}
1250
```