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
```