0
# Data Binding
1
2
Reactive data binding system supporting one-way, two-way, and event bindings with automatic dependency tracking and efficient updates for creating dynamic user interfaces.
3
4
## Capabilities
5
6
### One-Way Binding
7
8
Creates standard reactive bindings that automatically update when source data changes, supporting both function expressions and binding configurations.
9
10
```typescript { .api }
11
/**
12
* Creates a standard one-way binding
13
* @param expression - The binding expression to evaluate
14
* @param policy - The security policy to associate with the binding
15
* @param isVolatile - Indicates whether the binding is volatile or not
16
* @returns A binding configuration
17
*/
18
function oneWay<T = any>(
19
expression: Expression<T>,
20
policy?: DOMPolicy,
21
isVolatile?: boolean
22
): Binding<T>;
23
24
/**
25
* Creates an event listener binding
26
* @param expression - The binding to invoke when the event is raised
27
* @param options - Event listener options
28
* @returns A binding configuration
29
*/
30
function listener<T = any>(
31
expression: Expression<T>,
32
options?: AddEventListenerOptions
33
): Binding<T>;
34
```
35
36
**Usage Examples:**
37
38
```typescript
39
import { FASTElement, customElement, html, oneWay, listener, attr } from "@microsoft/fast-element";
40
41
const template = html<MyComponent>`
42
<!-- Simple property binding -->
43
<div class="status">${x => x.status}</div>
44
45
<!-- Conditional content binding -->
46
<div class="message">${x => x.showMessage ? x.message : ''}</div>
47
48
<!-- Complex expression binding -->
49
<span class="count">${x => `Items: ${x.items?.length ?? 0}`}</span>
50
51
<!-- Attribute binding -->
52
<input type="text"
53
value="${x => x.currentValue}"
54
?disabled="${x => x.isReadonly}"
55
class="${x => x.isValid ? 'valid' : 'invalid'}"
56
>
57
58
<!-- Event listener binding -->
59
<button @click="${x => x.handleClick}"
60
@mouseenter="${listener((x, e) => x.handleHover(e), { passive: true })}"
61
>
62
${x => x.buttonText}
63
</button>
64
65
<!-- Custom event binding with options -->
66
<custom-element @custom-event="${listener(
67
(x, e) => x.handleCustomEvent(e.detail),
68
{ once: true }
69
)}">
70
</custom-element>
71
`;
72
73
@customElement({
74
name: "my-component",
75
template
76
})
77
export class MyComponent extends FASTElement {
78
@attr status: string = "ready";
79
@attr message: string = "";
80
@attr showMessage: boolean = false;
81
@attr isReadonly: boolean = false;
82
@attr isValid: boolean = true;
83
@attr buttonText: string = "Click me";
84
@attr currentValue: string = "";
85
86
items: string[] = [];
87
88
handleClick() {
89
console.log("Button clicked");
90
this.showMessage = !this.showMessage;
91
}
92
93
handleHover(event: MouseEvent) {
94
console.log("Button hovered", event);
95
}
96
97
handleCustomEvent(detail: any) {
98
console.log("Custom event received", detail);
99
}
100
}
101
102
// Advanced one-way binding with security policy
103
const secureBinding = oneWay(
104
x => x.userContent,
105
myDOMPolicy, // Security policy for sanitization
106
false // Not volatile
107
);
108
109
// Volatile binding that always re-evaluates
110
const volatileBinding = oneWay(
111
x => Math.random(), // Always different
112
undefined,
113
true // Volatile - always re-evaluate
114
);
115
```
116
117
### Two-Way Binding
118
119
Creates bidirectional bindings that synchronize data between source and view, automatically detecting changes in both directions.
120
121
```typescript { .api }
122
/**
123
* Creates a two-way data binding
124
* @param expression - The binding expression to synchronize
125
* @param optionsOrChangeEvent - The binding options or change event name
126
* @param policy - The security policy to associate with the binding
127
* @param isBindingVolatile - Indicates whether the binding is volatile
128
* @returns A two-way binding configuration
129
*/
130
function twoWay<T = any>(
131
expression: Expression<T>,
132
optionsOrChangeEvent?: TwoWayBindingOptions | string,
133
policy?: DOMPolicy,
134
isBindingVolatile?: boolean
135
): Binding<T>;
136
137
/**
138
* Two-way binding configuration options
139
*/
140
interface TwoWayBindingOptions {
141
/** The event name to listen for changes from the view */
142
changeEvent?: string;
143
144
/** Function to transform values coming from the view */
145
fromView?: (value: any) => any;
146
}
147
148
/**
149
* Settings for configuring two-way binding behavior
150
*/
151
interface TwoWaySettings {
152
/**
153
* Determines which event to listen to for detecting view changes
154
* @param bindingSource - The directive to determine the change event for
155
* @param target - The target element to determine the change event for
156
*/
157
determineChangeEvent(bindingSource: BindingDirective, target: HTMLElement): string;
158
}
159
160
/**
161
* Utilities for configuring two-way binding system
162
*/
163
const TwoWaySettings: {
164
/**
165
* Configures global two-way binding behavior
166
* @param settings - The settings to use for the two-way binding system
167
*/
168
configure(settings: TwoWaySettings): void;
169
};
170
```
171
172
**Usage Examples:**
173
174
```typescript
175
import { FASTElement, customElement, html, attr, observable } from "@microsoft/fast-element";
176
import { twoWay } from "@microsoft/fast-element/binding/two-way.js";
177
178
const template = html<FormComponent>`
179
<!-- Basic two-way binding -->
180
<input type="text" :value="${x => x.name}">
181
182
<!-- Two-way binding with custom change event -->
183
<input type="text"
184
:value="${twoWay(x => x.email, 'input')}"
185
placeholder="Email">
186
187
<!-- Two-way binding with value transformation -->
188
<input type="number"
189
:value="${twoWay(
190
x => x.age,
191
{
192
changeEvent: 'input',
193
fromView: (value) => parseInt(value, 10) || 0
194
}
195
)}">
196
197
<!-- Two-way binding with checkbox -->
198
<input type="checkbox"
199
:checked="${twoWay(x => x.isSubscribed, 'change')}">
200
201
<!-- Two-way binding with select -->
202
<select :value="${twoWay(x => x.category)}">
203
<option value="web">Web Development</option>
204
<option value="mobile">Mobile Development</option>
205
<option value="data">Data Science</option>
206
</select>
207
208
<!-- Two-way binding with custom element -->
209
<custom-slider
210
:value="${twoWay(x => x.volume, 'value-changed')}"
211
min="0"
212
max="100">
213
</custom-slider>
214
215
<!-- Display bound values -->
216
<div class="preview">
217
<p>Name: ${x => x.name}</p>
218
<p>Email: ${x => x.email}</p>
219
<p>Age: ${x => x.age}</p>
220
<p>Subscribed: ${x => x.isSubscribed ? 'Yes' : 'No'}</p>
221
<p>Category: ${x => x.category}</p>
222
<p>Volume: ${x => x.volume}%</p>
223
</div>
224
`;
225
226
@customElement({
227
name: "form-component",
228
template
229
})
230
export class FormComponent extends FASTElement {
231
@observable name: string = "";
232
@observable email: string = "";
233
@observable age: number = 0;
234
@observable isSubscribed: boolean = false;
235
@observable category: string = "web";
236
@observable volume: number = 50;
237
}
238
239
// Configure custom two-way binding behavior
240
TwoWaySettings.configure({
241
determineChangeEvent(bindingSource, target) {
242
// Custom logic for determining change events
243
if (target.tagName === 'INPUT') {
244
const type = target.getAttribute('type');
245
switch (type) {
246
case 'text':
247
case 'email':
248
case 'password':
249
return 'input'; // Real-time updates
250
case 'checkbox':
251
case 'radio':
252
return 'change';
253
default:
254
return 'input';
255
}
256
}
257
if (target.tagName === 'SELECT') {
258
return 'change';
259
}
260
if (target.tagName === 'TEXTAREA') {
261
return 'input';
262
}
263
264
// Default for custom elements
265
return 'change';
266
}
267
});
268
269
// Custom two-way binding with complex transformation
270
const currencyBinding = twoWay(
271
x => x.price,
272
{
273
changeEvent: 'input',
274
fromView: (value: string) => {
275
// Transform currency input to number
276
const cleaned = value.replace(/[^0-9.]/g, '');
277
const parsed = parseFloat(cleaned);
278
return isNaN(parsed) ? 0 : parsed;
279
}
280
}
281
);
282
```
283
284
### One-Time Binding
285
286
Creates bindings that evaluate once during initial render and do not update automatically, useful for static content and performance optimization.
287
288
```typescript { .api }
289
/**
290
* Creates a one-time binding that evaluates once
291
* @param expression - The binding expression to evaluate
292
* @param policy - The security policy to associate with the binding
293
* @returns A one-time binding configuration
294
*/
295
function oneTime<T = any>(
296
expression: Expression<T>,
297
policy?: DOMPolicy
298
): Binding<T>;
299
```
300
301
**Usage Examples:**
302
303
```typescript
304
import { FASTElement, customElement, html, oneTime, attr } from "@microsoft/fast-element";
305
306
const template = html<StaticComponent>`
307
<!-- One-time binding for static content -->
308
<div class="header">${oneTime(x => x.appName)}</div>
309
<div class="version">Version: ${oneTime(x => x.version)}</div>
310
311
<!-- One-time binding for configuration -->
312
<div class="config" data-theme="${oneTime(x => x.theme)}">
313
Content that uses theme but doesn't need updates
314
</div>
315
316
<!-- Mixed bindings: static and dynamic -->
317
<div class="status-panel">
318
<h3>${oneTime(x => x.panelTitle)}</h3>
319
<span class="current-status">${x => x.currentStatus}</span>
320
<div class="build-info">
321
Built on: ${oneTime(x => x.buildDate)}
322
</div>
323
</div>
324
325
<!-- Performance optimization for expensive calculations -->
326
<div class="expensive-calc">
327
${oneTime(x => x.performExpensiveCalculation())}
328
</div>
329
`;
330
331
@customElement({
332
name: "static-component",
333
template
334
})
335
export class StaticComponent extends FASTElement {
336
@attr appName: string = "My Application";
337
@attr version: string = "1.0.0";
338
@attr theme: string = "light";
339
@attr panelTitle: string = "System Status";
340
@attr currentStatus: string = "Running";
341
@attr buildDate: string = new Date().toISOString();
342
343
// Expensive calculation that only needs to run once
344
performExpensiveCalculation(): string {
345
console.log("Performing expensive calculation...");
346
// Simulate expensive operation
347
return "Calculated result: " + Math.random().toString(36);
348
}
349
}
350
351
// One-time binding with security policy
352
const secureOneTime = oneTime(
353
x => x.trustedContent,
354
mySecurityPolicy
355
);
356
```
357
358
### Signal Binding
359
360
Creates signal-based bindings that update when specific signals are sent, providing a publish-subscribe pattern for reactive updates.
361
362
```typescript { .api }
363
/**
364
* Creates a signal binding configuration
365
* @param expression - The binding to refresh when signaled
366
* @param options - The signal name or expression to retrieve the signal name
367
* @param policy - The security policy to associate with the binding
368
* @returns A signal binding configuration
369
*/
370
function signal<T = any>(
371
expression: Expression<T>,
372
options: string | Expression<T>,
373
policy?: DOMPolicy
374
): Binding<T>;
375
376
/**
377
* Gateway to signal APIs for publish-subscribe communication
378
*/
379
const Signal: {
380
/**
381
* Subscribes to a signal
382
* @param signal - The signal name to subscribe to
383
* @param subscriber - The subscriber to receive notifications
384
*/
385
subscribe(signal: string, subscriber: Subscriber): void;
386
387
/**
388
* Unsubscribes from a signal
389
* @param signal - The signal name to unsubscribe from
390
* @param subscriber - The subscriber to remove
391
*/
392
unsubscribe(signal: string, subscriber: Subscriber): void;
393
394
/**
395
* Sends the specified signal to all subscribers
396
* @param signal - The signal name to send
397
*/
398
send(signal: string): void;
399
};
400
```
401
402
**Usage Examples:**
403
404
```typescript
405
import { FASTElement, customElement, html, attr } from "@microsoft/fast-element";
406
import { signal, Signal } from "@microsoft/fast-element/binding/signal.js";
407
408
const template = html<SignalComponent>`
409
<!-- Signal binding with static signal name -->
410
<div class="theme-indicator">
411
Current theme: ${signal(x => x.getCurrentTheme(), "theme-changed")}
412
</div>
413
414
<!-- Signal binding with dynamic signal name -->
415
<div class="user-status">
416
Status: ${signal(
417
x => x.getUserStatus(x.userId),
418
x => `user-${x.userId}-changed`
419
)}
420
</div>
421
422
<!-- Multiple signal bindings -->
423
<div class="notifications">
424
Messages: ${signal(x => x.getMessageCount(), "messages-updated")}
425
Alerts: ${signal(x => x.getAlertCount(), "alerts-updated")}
426
</div>
427
428
<!-- Signal binding for real-time data -->
429
<div class="live-data">
430
<span>Price: $${signal(x => x.getCurrentPrice(), "price-update")}</span>
431
<span>Change: ${signal(x => x.getPriceChange(), "price-update")}%</span>
432
</div>
433
434
<button @click="${x => x.refreshData}">Refresh All</button>
435
`;
436
437
@customElement({
438
name: "signal-component",
439
template
440
})
441
export class SignalComponent extends FASTElement {
442
@attr userId: string = "user123";
443
444
private currentTheme: string = "light";
445
private messageCount: number = 0;
446
private alertCount: number = 0;
447
private currentPrice: number = 100.0;
448
private priceChange: number = 0.0;
449
450
connectedCallback() {
451
super.connectedCallback();
452
453
// Setup signal listeners for external updates
454
this.setupSignalListeners();
455
}
456
457
getCurrentTheme(): string {
458
return this.currentTheme;
459
}
460
461
getUserStatus(userId: string): string {
462
// Simulate getting user status
463
return "online";
464
}
465
466
getMessageCount(): number {
467
return this.messageCount;
468
}
469
470
getAlertCount(): number {
471
return this.alertCount;
472
}
473
474
getCurrentPrice(): number {
475
return this.currentPrice;
476
}
477
478
getPriceChange(): number {
479
return this.priceChange;
480
}
481
482
refreshData() {
483
// Manually trigger updates
484
Signal.send("theme-changed");
485
Signal.send(`user-${this.userId}-changed`);
486
Signal.send("messages-updated");
487
Signal.send("alerts-updated");
488
Signal.send("price-update");
489
}
490
491
private setupSignalListeners() {
492
// Listen for external theme changes
493
window.addEventListener('theme-changed', () => {
494
this.currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
495
Signal.send("theme-changed");
496
});
497
498
// Listen for WebSocket updates
499
this.setupWebSocketListeners();
500
501
// Listen for user events
502
document.addEventListener('user-event', (e: CustomEvent) => {
503
Signal.send(`user-${e.detail.userId}-changed`);
504
});
505
}
506
507
private setupWebSocketListeners() {
508
// Simulate WebSocket connection
509
setInterval(() => {
510
// Simulate price updates
511
this.currentPrice += (Math.random() - 0.5) * 2;
512
this.priceChange = (Math.random() - 0.5) * 10;
513
Signal.send("price-update");
514
}, 1000);
515
516
setInterval(() => {
517
// Simulate message updates
518
this.messageCount = Math.floor(Math.random() * 10);
519
Signal.send("messages-updated");
520
}, 5000);
521
}
522
}
523
524
// External signal senders
525
class DataService {
526
static updateTheme(newTheme: string) {
527
// Update theme and signal change
528
document.documentElement.setAttribute('data-theme', newTheme);
529
Signal.send("theme-changed");
530
}
531
532
static updateMessages(count: number) {
533
// Signal message count change
534
Signal.send("messages-updated");
535
}
536
537
static updateAlerts(count: number) {
538
// Signal alert count change
539
Signal.send("alerts-updated");
540
}
541
}
542
```
543
544
### Binding Base Class
545
546
Abstract base class for all binding types, providing common functionality for expression evaluation and observer creation.
547
548
```typescript { .api }
549
/**
550
* Captures a binding expression along with related information and capabilities
551
*/
552
abstract class Binding<TSource = any, TReturn = any, TParent = any> {
553
/** Options associated with the binding */
554
options?: any;
555
556
/**
557
* Creates a binding
558
* @param evaluate - Function that evaluates the binding
559
* @param policy - The security policy to associate with this binding
560
* @param isVolatile - Indicates whether the binding is volatile
561
*/
562
constructor(
563
public evaluate: Expression<TSource, TReturn, TParent>,
564
public policy?: DOMPolicy,
565
public isVolatile: boolean = false
566
);
567
568
/**
569
* Creates an observer capable of notifying a subscriber when the binding output changes
570
* @param subscriber - The subscriber to changes in the binding
571
* @param directive - The binding directive to create the observer for
572
*/
573
abstract createObserver(
574
subscriber: Subscriber,
575
directive: BindingDirective
576
): ExpressionObserver<TSource, TReturn, TParent>;
577
}
578
579
/**
580
* The directive from which a binding originates
581
*/
582
interface BindingDirective {
583
/** The binding */
584
readonly dataBinding: Binding;
585
586
/** The evaluated target aspect */
587
readonly targetAspect?: string;
588
589
/** The type of aspect to target */
590
readonly aspectType?: DOMAspect;
591
}
592
```
593
594
**Usage Examples:**
595
596
```typescript
597
import { Binding, BindingDirective, Subscriber, ExpressionObserver } from "@microsoft/fast-element";
598
599
// Custom binding implementation
600
class CustomBinding<TSource = any, TReturn = any, TParent = any>
601
extends Binding<TSource, TReturn, TParent> {
602
603
createObserver(
604
subscriber: Subscriber,
605
directive: BindingDirective
606
): ExpressionObserver<TSource, TReturn, TParent> {
607
return new CustomObserver(this, subscriber, directive);
608
}
609
}
610
611
// Custom observer implementation
612
class CustomObserver<TSource = any, TReturn = any, TParent = any>
613
implements ExpressionObserver<TSource, TReturn, TParent> {
614
615
constructor(
616
private binding: CustomBinding<TSource, TReturn, TParent>,
617
private subscriber: Subscriber,
618
private directive: BindingDirective
619
) {}
620
621
bind(controller: ExpressionController<TSource, TParent>): TReturn {
622
// Custom binding logic
623
const result = this.binding.evaluate(controller.source, controller.context);
624
625
// Set up custom change detection
626
this.setupChangeDetection(controller);
627
628
return result;
629
}
630
631
unbind(controller: ExpressionController<TSource, TParent>): void {
632
// Custom cleanup logic
633
this.cleanupChangeDetection();
634
}
635
636
private setupChangeDetection(controller: ExpressionController<TSource, TParent>): void {
637
// Custom change detection implementation
638
}
639
640
private cleanupChangeDetection(): void {
641
// Custom cleanup implementation
642
}
643
}
644
645
// Factory function for custom binding
646
function customBinding<T = any>(
647
expression: Expression<T>,
648
options?: any
649
): Binding<T> {
650
const binding = new CustomBinding(expression);
651
binding.options = options;
652
return binding;
653
}
654
```
655
656
### Expression Normalization
657
658
Utilities for normalizing and processing binding expressions to ensure consistent behavior across different binding types.
659
660
```typescript { .api }
661
/**
662
* Normalizes binding expressions for consistent processing
663
* @param expression - The expression to normalize
664
* @returns The normalized binding expression
665
*/
666
function normalizeBinding<T = any>(expression: Expression<T>): Binding<T>;
667
```
668
669
## Types
670
671
```typescript { .api }
672
/**
673
* A function or string that represents a binding expression
674
*/
675
type Expression<TReturn = any, TSource = any, TParent = any> =
676
| ((source: TSource, context: ExecutionContext<TParent>) => TReturn)
677
| string;
678
679
/**
680
* Execution context for binding evaluation
681
*/
682
interface ExecutionContext<TParent = any> {
683
/** Current index in a repeat context */
684
index: number;
685
686
/** Length of the collection in a repeat context */
687
length: number;
688
689
/** Parent data source */
690
parent: TParent;
691
692
/** Parent execution context */
693
parentContext: ExecutionContext<TParent>;
694
}
695
696
/**
697
* Observer interface for expression changes
698
*/
699
interface ExpressionObserver<TSource = any, TReturn = any, TParent = any> {
700
/**
701
* Binds the observer to a controller
702
* @param controller - The expression controller
703
*/
704
bind(controller: ExpressionController<TSource, TParent>): TReturn;
705
706
/**
707
* Unbinds the observer from a controller
708
* @param controller - The expression controller
709
*/
710
unbind?(controller: ExpressionController<TSource, TParent>): void;
711
}
712
713
/**
714
* Controller for managing expression lifecycle
715
*/
716
interface ExpressionController<TSource = any, TParent = any> {
717
/** The data source */
718
source: TSource;
719
720
/** The execution context */
721
context: ExecutionContext<TParent>;
722
723
/**
724
* Registers a callback for when the controller unbinds
725
* @param callback - The callback to register
726
*/
727
onUnbind(callback: any): void;
728
}
729
730
/**
731
* Subscriber interface for change notifications
732
*/
733
interface Subscriber {
734
/**
735
* Handles changes in observed values
736
* @param source - The source of the change
737
* @param args - Arguments related to the change
738
*/
739
handleChange(source: any, args: any): void;
740
}
741
```