or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

attributes.mdcontext-system.mdcss-styling.mddata-binding.mddependency-injection.mdhtml-templates.mdindex.mdobservable-system.mdssr-hydration.mdstate-management.mdtemplate-directives.mdtesting-utilities.mdutilities.mdweb-components.md

template-directives.mddocs/

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

```