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

attributes.mddocs/

0

# Attributes

1

2

Attribute system with automatic type conversion, reflection options, and custom converters for seamless property-attribute synchronization in custom elements.

3

4

## Capabilities

5

6

### Attribute Decorator

7

8

Decorator function for defining HTML attributes on custom element properties with automatic synchronization, type conversion, and reflection modes.

9

10

```typescript { .api }

11

/**

12

* Decorator: Specifies an HTML attribute with configuration

13

* @param config - The configuration for the attribute

14

* @returns Property decorator function

15

*/

16

function attr(

17

config?: DecoratorAttributeConfiguration

18

): (target: {}, property: string) => void;

19

20

/**

21

* Decorator: Specifies an HTML attribute with default behavior

22

* @param target - The class to define the attribute on

23

* @param prop - The property name to be associated with the attribute

24

*/

25

function attr(target: {}, prop: string): void;

26

27

/**

28

* Configuration for attribute decorator

29

*/

30

interface DecoratorAttributeConfiguration {

31

/** Custom attribute name (defaults to lowercase property name) */

32

attribute?: string;

33

34

/** Attribute behavior mode */

35

mode?: AttributeMode;

36

37

/** Value converter for type conversion */

38

converter?: ValueConverter;

39

}

40

41

/**

42

* Attribute behavior modes

43

*/

44

type AttributeMode =

45

| "reflect" // Two-way sync between property and attribute (default)

46

| "boolean" // Boolean attribute behavior (presence = true, absence = false)

47

| "fromView"; // One-way sync from attribute to property only

48

```

49

50

**Usage Examples:**

51

52

```typescript

53

import {

54

FASTElement,

55

customElement,

56

html,

57

attr,

58

booleanConverter,

59

nullableNumberConverter

60

} from "@microsoft/fast-element";

61

62

const template = html<AttributeExample>`

63

<div class="attribute-demo">

64

<!-- Display current attribute values -->

65

<div class="values">

66

<p>Name: ${x => x.name}</p>

67

<p>Age: ${x => x.age}</p>

68

<p>Disabled: ${x => x.disabled ? 'Yes' : 'No'}</p>

69

<p>Theme: ${x => x.theme}</p>

70

<p>Count: ${x => x.count}</p>

71

<p>Status: ${x => x.status}</p>

72

</div>

73

74

<!-- Controls to modify attributes -->

75

<div class="controls">

76

<input type="text"

77

placeholder="Name"

78

@input="${(x, e) => x.name = (e.target as HTMLInputElement).value}">

79

<input type="number"

80

placeholder="Age"

81

@input="${(x, e) => x.age = parseInt((e.target as HTMLInputElement).value)}">

82

<button @click="${x => x.disabled = !x.disabled}">

83

Toggle Disabled

84

</button>

85

<select @change="${(x, e) => x.theme = (e.target as HTMLSelectElement).value}">

86

<option value="light">Light</option>

87

<option value="dark">Dark</option>

88

<option value="auto">Auto</option>

89

</select>

90

</div>

91

</div>

92

`;

93

94

@customElement({

95

name: "attribute-example",

96

template

97

})

98

export class AttributeExample extends FASTElement {

99

// Basic attribute (reflect mode by default)

100

@attr name: string = "";

101

102

// Attribute with custom name

103

@attr({ attribute: "user-age" })

104

age: number = 0;

105

106

// Boolean attribute

107

@attr({ mode: "boolean" })

108

disabled: boolean = false;

109

110

// Attribute with custom converter

111

@attr({ converter: customThemeConverter })

112

theme: Theme = Theme.Light;

113

114

// Nullable number attribute

115

@attr({ converter: nullableNumberConverter })

116

count: number | null = null;

117

118

// From-view only attribute (no reflection)

119

@attr({ mode: "fromView" })

120

status: string = "ready";

121

122

// Attribute change callbacks

123

nameChanged(oldValue: string, newValue: string) {

124

console.log(`Name changed from "${oldValue}" to "${newValue}"`);

125

}

126

127

ageChanged(oldValue: number, newValue: number) {

128

console.log(`Age changed from ${oldValue} to ${newValue}`);

129

if (newValue < 0) {

130

this.age = 0; // Validate and correct

131

}

132

}

133

134

disabledChanged(oldValue: boolean, newValue: boolean) {

135

console.log(`Disabled changed from ${oldValue} to ${newValue}`);

136

this.classList.toggle('disabled', newValue);

137

}

138

139

themeChanged(oldValue: Theme, newValue: Theme) {

140

console.log(`Theme changed from ${oldValue} to ${newValue}`);

141

document.documentElement.setAttribute('data-theme', newValue);

142

}

143

}

144

145

// Custom theme enum and converter

146

enum Theme {

147

Light = "light",

148

Dark = "dark",

149

Auto = "auto"

150

}

151

152

const customThemeConverter: ValueConverter = {

153

toView(value: Theme): string {

154

return value;

155

},

156

157

fromView(value: string): Theme {

158

return Object.values(Theme).includes(value as Theme)

159

? value as Theme

160

: Theme.Light;

161

}

162

};

163

164

// Advanced attribute patterns

165

@customElement("advanced-attributes")

166

export class AdvancedAttributeExample extends FASTElement {

167

// Multiple attributes with different behaviors

168

@attr({ mode: "reflect" })

169

title: string = "Default Title";

170

171

@attr({ mode: "boolean" })

172

expanded: boolean = false;

173

174

@attr({ mode: "boolean" })

175

loading: boolean = false;

176

177

@attr({ mode: "fromView", attribute: "data-id" })

178

dataId: string = "";

179

180

// Complex object attribute with JSON converter

181

@attr({ converter: jsonConverter })

182

config: Config = { width: 300, height: 200 };

183

184

// Array attribute with custom converter

185

@attr({ converter: arrayConverter })

186

tags: string[] = [];

187

188

// Validation in change callbacks

189

titleChanged(oldValue: string, newValue: string) {

190

if (newValue.length > 100) {

191

this.title = newValue.substring(0, 100);

192

console.warn("Title truncated to 100 characters");

193

}

194

}

195

196

configChanged(oldValue: Config, newValue: Config) {

197

// Validate config object

198

if (!newValue || typeof newValue !== 'object') {

199

this.config = { width: 300, height: 200 };

200

return;

201

}

202

203

// Ensure minimum values

204

if (newValue.width < 100) newValue.width = 100;

205

if (newValue.height < 100) newValue.height = 100;

206

}

207

208

static template = html<AdvancedAttributeExample>`

209

<div class="advanced-demo">

210

<h2>${x => x.title}</h2>

211

<div class="config">

212

Size: ${x => x.config.width} x ${x => x.config.height}

213

</div>

214

<div class="tags">

215

Tags: ${x => x.tags.join(', ')}

216

</div>

217

<div class="state">

218

<span ?hidden="${x => !x.loading}">Loading...</span>

219

<span ?hidden="${x => !x.expanded}">Expanded content</span>

220

</div>

221

</div>

222

`;

223

}

224

225

// Custom converters

226

interface Config {

227

width: number;

228

height: number;

229

}

230

231

const jsonConverter: ValueConverter = {

232

toView(value: any): string {

233

return JSON.stringify(value);

234

},

235

236

fromView(value: string): any {

237

try {

238

return JSON.parse(value);

239

} catch {

240

return null;

241

}

242

}

243

};

244

245

const arrayConverter: ValueConverter = {

246

toView(value: string[]): string {

247

return value.join(',');

248

},

249

250

fromView(value: string): string[] {

251

return value ? value.split(',').map(s => s.trim()).filter(Boolean) : [];

252

}

253

};

254

```

255

256

### Attribute Definition

257

258

Internal class that manages attribute behavior, type conversion, and property synchronization for custom element attributes.

259

260

```typescript { .api }

261

/**

262

* An implementation of Accessor that supports reactivity, change callbacks,

263

* attribute reflection, and type conversion for custom elements

264

*/

265

class AttributeDefinition implements Accessor {

266

/** The class constructor that owns this attribute */

267

readonly Owner: Function;

268

269

/** The name of the property associated with the attribute */

270

readonly name: string;

271

272

/** The name of the attribute in HTML */

273

readonly attribute: string;

274

275

/** The AttributeMode that describes the behavior of this attribute */

276

readonly mode: AttributeMode;

277

278

/** A ValueConverter that integrates with the property getter/setter */

279

readonly converter?: ValueConverter;

280

281

/**

282

* Creates an instance of AttributeDefinition

283

* @param Owner - The class constructor that owns this attribute

284

* @param name - The name of the property associated with the attribute

285

* @param attribute - The name of the attribute in HTML

286

* @param mode - The AttributeMode that describes the behavior

287

* @param converter - A ValueConverter for type conversion

288

*/

289

constructor(

290

Owner: Function,

291

name: string,

292

attribute?: string,

293

mode?: AttributeMode,

294

converter?: ValueConverter

295

);

296

297

/**

298

* Gets the value of the property on the source object

299

* @param source - The source object to access

300

*/

301

getValue(source: any): any;

302

303

/**

304

* Sets the value of the property on the source object

305

* @param source - The source object to access

306

* @param value - The value to set the property to

307

*/

308

setValue(source: any, value: any): void;

309

310

/**

311

* Sets the value based on a string from an HTML attribute

312

* @param source - The source object

313

* @param value - The string value from the attribute

314

*/

315

onAttributeChangedCallback(source: any, value: string): void;

316

}

317

318

/**

319

* Metadata used to configure a custom attribute's behavior

320

*/

321

interface AttributeConfiguration {

322

/** The property name */

323

property: string;

324

325

/** The attribute name in HTML */

326

attribute?: string;

327

328

/** The behavior mode */

329

mode?: AttributeMode;

330

331

/** The value converter */

332

converter?: ValueConverter;

333

}

334

335

/**

336

* Utilities for managing attribute configurations

337

*/

338

const AttributeConfiguration: {

339

/**

340

* Locates all attribute configurations associated with a type

341

*/

342

locate(target: any): AttributeConfiguration[];

343

};

344

```

345

346

**Usage Examples:**

347

348

```typescript

349

import { AttributeDefinition, AttributeConfiguration } from "@microsoft/fast-element";

350

351

// Manual attribute definition

352

class ManualAttributeExample {

353

private _value: string = "";

354

355

static attributes: AttributeDefinition[] = [];

356

357

constructor() {

358

// Create attribute definition manually

359

const valueAttr = new AttributeDefinition(

360

ManualAttributeExample,

361

"value",

362

"data-value",

363

"reflect"

364

);

365

366

ManualAttributeExample.attributes.push(valueAttr);

367

368

// Define property with attribute behavior

369

Object.defineProperty(this, "value", {

370

get: () => valueAttr.getValue(this),

371

set: (newValue) => valueAttr.setValue(this, newValue)

372

});

373

}

374

375

get value(): string {

376

return this._value;

377

}

378

379

set value(newValue: string) {

380

const oldValue = this._value;

381

this._value = newValue;

382

383

// Trigger change callback if exists

384

if ((this as any).valueChanged) {

385

(this as any).valueChanged(oldValue, newValue);

386

}

387

}

388

}

389

390

// Runtime attribute inspection

391

class AttributeInspector {

392

static inspectElement(elementClass: any): AttributeDefinition[] {

393

const configs = AttributeConfiguration.locate(elementClass);

394

return configs.map(config =>

395

new AttributeDefinition(

396

elementClass,

397

config.property,

398

config.attribute,

399

config.mode,

400

config.converter

401

)

402

);

403

}

404

405

static getAttributeNames(elementClass: any): string[] {

406

return this.inspectElement(elementClass)

407

.map(attr => attr.attribute);

408

}

409

410

static getPropertyNames(elementClass: any): string[] {

411

return this.inspectElement(elementClass)

412

.map(attr => attr.name);

413

}

414

}

415

416

// Usage

417

const myElementAttrs = AttributeInspector.inspectElement(AttributeExample);

418

console.log("Attributes:", myElementAttrs.map(a => a.attribute));

419

console.log("Properties:", myElementAttrs.map(a => a.name));

420

```

421

422

### Built-in Value Converters

423

424

Pre-built converters for common data types including booleans, numbers, and nullable variants.

425

426

```typescript { .api }

427

/**

428

* Represents objects that can convert values between view and model representations

429

*/

430

interface ValueConverter {

431

/**

432

* Converts a value from model representation to view representation

433

* @param value - The value to convert to a view representation

434

*/

435

toView(value: any): any;

436

437

/**

438

* Converts a value from view representation to model representation

439

* @param value - The value to convert to a model representation

440

*/

441

fromView(value: any): any;

442

}

443

444

/**

445

* A ValueConverter that converts to and from boolean values

446

* Used automatically when the "boolean" AttributeMode is selected

447

*/

448

const booleanConverter: ValueConverter;

449

450

/**

451

* A ValueConverter that converts to and from boolean values

452

* null, undefined, "", and void values are converted to null

453

*/

454

const nullableBooleanConverter: ValueConverter;

455

456

/**

457

* A ValueConverter that converts to and from number values

458

* This converter allows for nullable numbers, returning null if the

459

* input was null, undefined, or NaN

460

*/

461

const nullableNumberConverter: ValueConverter;

462

```

463

464

**Usage Examples:**

465

466

```typescript

467

import {

468

ValueConverter,

469

booleanConverter,

470

nullableBooleanConverter,

471

nullableNumberConverter,

472

attr,

473

FASTElement,

474

customElement

475

} from "@microsoft/fast-element";

476

477

@customElement("converter-example")

478

export class ConverterExample extends FASTElement {

479

// Boolean converter (automatic with boolean mode)

480

@attr({ mode: "boolean" })

481

visible: boolean = false;

482

483

// Explicit boolean converter

484

@attr({ converter: booleanConverter })

485

enabled: boolean = true;

486

487

// Nullable boolean converter

488

@attr({ converter: nullableBooleanConverter })

489

optional: boolean | null = null;

490

491

// Nullable number converter

492

@attr({ converter: nullableNumberConverter })

493

score: number | null = null;

494

495

// Custom date converter

496

@attr({ converter: dateConverter })

497

created: Date = new Date();

498

499

// Custom enum converter

500

@attr({ converter: priorityConverter })

501

priority: Priority = Priority.Medium;

502

503

// Custom URL converter

504

@attr({ converter: urlConverter })

505

homepage: URL | null = null;

506

}

507

508

// Custom converters

509

const dateConverter: ValueConverter = {

510

toView(value: Date): string {

511

return value ? value.toISOString() : "";

512

},

513

514

fromView(value: string): Date {

515

return value ? new Date(value) : new Date();

516

}

517

};

518

519

enum Priority {

520

Low = "low",

521

Medium = "medium",

522

High = "high"

523

}

524

525

const priorityConverter: ValueConverter = {

526

toView(value: Priority): string {

527

return value;

528

},

529

530

fromView(value: string): Priority {

531

return Object.values(Priority).includes(value as Priority)

532

? value as Priority

533

: Priority.Medium;

534

}

535

};

536

537

const urlConverter: ValueConverter = {

538

toView(value: URL | null): string {

539

return value ? value.toString() : "";

540

},

541

542

fromView(value: string): URL | null {

543

try {

544

return value ? new URL(value) : null;

545

} catch {

546

return null;

547

}

548

}

549

};

550

551

// Advanced converter with validation

552

const emailConverter: ValueConverter = {

553

toView(value: string): string {

554

return value || "";

555

},

556

557

fromView(value: string): string {

558

// Basic email validation

559

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

560

return emailRegex.test(value) ? value : "";

561

}

562

};

563

564

const currencyConverter: ValueConverter = {

565

toView(value: number): string {

566

return value ? value.toFixed(2) : "0.00";

567

},

568

569

fromView(value: string): number {

570

const num = parseFloat(value.replace(/[^0-9.-]+/g, ""));

571

return isNaN(num) ? 0 : num;

572

}

573

};

574

575

// Converter with options

576

function createRangeConverter(min: number, max: number): ValueConverter {

577

return {

578

toView(value: number): string {

579

return value.toString();

580

},

581

582

fromView(value: string): number {

583

const num = parseInt(value, 10);

584

if (isNaN(num)) return min;

585

return Math.max(min, Math.min(max, num));

586

}

587

};

588

}

589

590

// Usage with range converter

591

@customElement("range-example")

592

export class RangeExample extends FASTElement {

593

@attr({ converter: createRangeConverter(0, 100) })

594

percentage: number = 50;

595

596

@attr({ converter: createRangeConverter(1, 5) })

597

rating: number = 3;

598

599

static template = html<RangeExample>`

600

<div>

601

<p>Percentage: ${x => x.percentage}%</p>

602

<p>Rating: ${x => x.rating}/5 stars</p>

603

</div>

604

`;

605

}

606

```

607

608

### Attribute Modes

609

610

Different synchronization modes controlling how properties and attributes interact with each other.

611

612

```typescript { .api }

613

/**

614

* The mode that specifies the runtime behavior of the attribute

615

*/

616

type AttributeMode = "reflect" | "boolean" | "fromView";

617

618

/**

619

* Attribute mode behaviors:

620

*

621

* "reflect" - Two-way synchronization (default)

622

* - Property changes reflect to DOM attribute

623

* - DOM attribute changes update property

624

*

625

* "boolean" - Boolean attribute behavior

626

* - Presence of attribute = true

627

* - Absence of attribute = false

628

* - Property changes add/remove attribute

629

*

630

* "fromView" - One-way from DOM to property

631

* - DOM attribute changes update property

632

* - Property changes do NOT reflect to DOM

633

*/

634

```

635

636

**Usage Examples:**

637

638

```typescript

639

import { FASTElement, customElement, html, attr } from "@microsoft/fast-element";

640

641

@customElement("mode-example")

642

export class AttributeModeExample extends FASTElement {

643

// Reflect mode (default) - two-way sync

644

@attr({ mode: "reflect" })

645

title: string = "Default Title";

646

647

// Boolean mode - presence/absence behavior

648

@attr({ mode: "boolean" })

649

disabled: boolean = false;

650

651

@attr({ mode: "boolean" })

652

hidden: boolean = false;

653

654

@attr({ mode: "boolean" })

655

readonly: boolean = false;

656

657

// FromView mode - one-way from attribute to property

658

@attr({ mode: "fromView" })

659

dataId: string = "";

660

661

@attr({ mode: "fromView", attribute: "aria-label" })

662

ariaLabel: string = "";

663

664

// Demonstrate mode behaviors

665

testReflectMode() {

666

// This will update both property and attribute

667

this.title = "New Title";

668

console.log("Title attribute:", this.getAttribute("title"));

669

}

670

671

testBooleanMode() {

672

// This will add/remove the 'disabled' attribute

673

this.disabled = !this.disabled;

674

console.log("Has disabled attribute:", this.hasAttribute("disabled"));

675

}

676

677

testFromViewMode() {

678

// This will NOT update the DOM attribute

679

this.dataId = "new-id";

680

console.log("data-id attribute:", this.getAttribute("data-id"));

681

682

// But setting the attribute will update the property

683

this.setAttribute("data-id", "from-dom");

684

console.log("dataId property:", this.dataId);

685

}

686

687

static template = html<AttributeModeExample>`

688

<div class="mode-demo">

689

<h3>${x => x.title}</h3>

690

691

<div class="controls">

692

<button @click="${x => x.testReflectMode()}">

693

Test Reflect Mode

694

</button>

695

696

<button @click="${x => x.testBooleanMode()}">

697

Test Boolean Mode (Disabled: ${x => x.disabled})

698

</button>

699

700

<button @click="${x => x.testFromViewMode()}">

701

Test FromView Mode

702

</button>

703

</div>

704

705

<div class="state">

706

<p>Title: "${x => x.title}"</p>

707

<p>Disabled: ${x => x.disabled}</p>

708

<p>Data ID: "${x => x.dataId}"</p>

709

<p>ARIA Label: "${x => x.ariaLabel}"</p>

710

</div>

711

</div>

712

`;

713

}

714

715

// Practical mode usage patterns

716

@customElement("practical-modes")

717

export class PracticalModesExample extends FASTElement {

718

// Reflect mode for user-configurable properties

719

@attr({ mode: "reflect" })

720

variant: "primary" | "secondary" = "primary";

721

722

@attr({ mode: "reflect" })

723

size: "small" | "medium" | "large" = "medium";

724

725

// Boolean mode for states and flags

726

@attr({ mode: "boolean" })

727

loading: boolean = false;

728

729

@attr({ mode: "boolean" })

730

selected: boolean = false;

731

732

@attr({ mode: "boolean" })

733

expanded: boolean = false;

734

735

// FromView mode for read-only external data

736

@attr({ mode: "fromView", attribute: "data-testid" })

737

testId: string = "";

738

739

@attr({ mode: "fromView", attribute: "role" })

740

role: string = "";

741

742

@attr({ mode: "fromView", attribute: "tabindex" })

743

tabIndex: string = "0";

744

745

// Change callbacks for different modes

746

variantChanged(oldValue: string, newValue: string) {

747

console.log(`Variant reflect: ${oldValue} → ${newValue}`);

748

this.classList.remove(`variant-${oldValue}`);

749

this.classList.add(`variant-${newValue}`);

750

}

751

752

loadingChanged(oldValue: boolean, newValue: boolean) {

753

console.log(`Loading boolean: ${oldValue} → ${newValue}`);

754

this.classList.toggle('loading', newValue);

755

}

756

757

testIdChanged(oldValue: string, newValue: string) {

758

console.log(`Test ID fromView: ${oldValue} → ${newValue}`);

759

// Only reacts to external attribute changes

760

}

761

762

static template = html<PracticalModesExample>`

763

<div class="practical-modes ${x => x.variant} ${x => x.size}">

764

<div class="content" ?hidden="${x => x.loading}">

765

Content (variant: ${x => x.variant}, size: ${x => x.size})

766

</div>

767

768

<div class="loading-spinner" ?hidden="${x => !x.loading}">

769

Loading...

770

</div>

771

772

<div class="metadata">

773

Test ID: ${x => x.testId}

774

Role: ${x => x.role}

775

Tab Index: ${x => x.tabIndex}

776

</div>

777

</div>

778

`;

779

}

780

```

781

782

## Types

783

784

```typescript { .api }

785

/**

786

* Property accessor interface for reactive properties

787

*/

788

interface Accessor {

789

/** The name of the property */

790

name: string;

791

792

/**

793

* Gets the value of the property on the source object

794

* @param source - The source object to access

795

*/

796

getValue(source: any): any;

797

798

/**

799

* Sets the value of the property on the source object

800

* @param source - The source object to access

801

* @param value - The value to set the property to

802

*/

803

setValue(source: any, value: any): void;

804

}

805

806

/**

807

* Complete attribute configuration interface

808

*/

809

interface AttributeConfiguration {

810

/** The property name */

811

property: string;

812

813

/** The attribute name in HTML */

814

attribute?: string;

815

816

/** The behavior mode */

817

mode?: AttributeMode;

818

819

/** The value converter */

820

converter?: ValueConverter;

821

}

822

823

/**

824

* Attribute configuration for decorators (excludes property)

825

*/

826

type DecoratorAttributeConfiguration = Omit<AttributeConfiguration, "property">;

827

828

/**

829

* Available attribute behavior modes

830

*/

831

type AttributeMode =

832

| "reflect" // Two-way synchronization (default)

833

| "boolean" // Boolean attribute behavior

834

| "fromView"; // One-way from attribute to property

835

```