A library for constructing Web Components
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Memory-efficient observable system with automatic dependency tracking, batch updates, and array observation capabilities for building reactive applications with minimal overhead.
Decorator function for making class properties observable, enabling automatic change detection and notification when property values change.
/**
* Decorator: Defines an observable property on the target
* @param target - The target to define the observable on
* @param nameOrAccessor - The property name or accessor to define the observable as
*/
function observable(target: {}, nameOrAccessor: string | Accessor): void;
/**
* Overloaded signature for use with custom accessors
* @param target - The target object
* @param nameOrAccessor - The property accessor
* @param descriptor - The property descriptor
*/
function observable<T, K extends keyof T>(
target: T,
nameOrAccessor: K
): void;
function observable<T, K extends keyof T>(
target: T,
nameOrAccessor: K,
descriptor: PropertyDescriptor
): PropertyDescriptor;Usage Examples:
import { FASTElement, customElement, html, observable, attr } from "@microsoft/fast-element";
@customElement("observable-example")
export class ObservableExample extends FASTElement {
// Basic observable property
@observable firstName: string = "";
@observable lastName: string = "";
@observable age: number = 0;
// Observable arrays
@observable items: string[] = [];
@observable users: User[] = [];
// Observable objects
@observable settings: UserSettings = {
theme: "light",
notifications: true
};
// Computed properties (getters that depend on observables)
get fullName(): string {
return `${this.firstName} ${this.lastName}`.trim();
}
get isAdult(): boolean {
return this.age >= 18;
}
get itemCount(): number {
return this.items?.length ?? 0;
}
// Methods that modify observable properties
addItem(item: string) {
this.items.push(item);
// Observable array mutation is automatically tracked
}
updateSettings(newSettings: Partial<UserSettings>) {
Object.assign(this.settings, newSettings);
// Object property changes are tracked when accessed through observables
}
clearAll() {
this.firstName = "";
this.lastName = "";
this.age = 0;
this.items = [];
this.users = [];
}
}
// Template that uses observable properties
const template = html<ObservableExample>`
<div class="user-info">
<h2>${x => x.fullName || 'No name'}</h2>
<p>Age: ${x => x.age} ${x => x.isAdult ? '(Adult)' : '(Minor)'}</p>
<p>Items: ${x => x.itemCount}</p>
</div>
<div class="items">
${x => x.items.map(item => `<div class="item">${item}</div>`).join('')}
</div>
<div class="settings">
Theme: ${x => x.settings.theme}
Notifications: ${x => x.settings.notifications ? 'On' : 'Off'}
</div>
`;
// Observable properties in nested objects
export class NestedObservables {
@observable user = {
profile: {
name: "John",
email: "john@example.com"
},
preferences: {
theme: "dark",
language: "en"
}
};
// Method to update nested properties
updateUserName(name: string) {
// This will trigger change notifications
this.user.profile.name = name;
}
updateTheme(theme: string) {
// This will trigger change notifications
this.user.preferences.theme = theme;
}
}Central object providing utilities for observable creation, tracking, and notification management.
/**
* Common Observable APIs for managing reactive properties and expressions
*/
const Observable: {
/**
* Defines an observable property on a target object
* @param target - The object to define the property on
* @param nameOrAccessor - The property name or accessor
*/
defineProperty(target: any, nameOrAccessor: string | Accessor): void;
/**
* Gets the notifier for a source object
* @param source - The object to get the notifier for
* @returns The notifier for change notifications
*/
getNotifier(source: any): Notifier;
/**
* Creates a binding observer for an expression
* @param evaluate - The expression to observe
* @param observer - The observer to notify on changes
* @param isVolatile - Whether the binding is volatile
* @returns An expression observer
*/
binding<T>(
evaluate: Expression<T>,
observer: ExpressionObserver,
isVolatile?: boolean
): ExpressionObserver<any, T>;
/**
* Tracks property access for dependency collection
* @param target - The target object
* @param propertyName - The property being accessed
*/
track(target: any, propertyName: PropertyKey): void;
/**
* Notifies observers of property changes
* @param source - The source object that changed
* @param args - Arguments describing the change
*/
notify(source: any, args: any): void;
/**
* Determines if an expression should be treated as volatile
* @param expression - The expression to check
* @returns True if the expression is volatile
*/
isVolatileBinding(expression: Expression<any>): boolean;
/**
* Marks the current evaluation as volatile
*/
trackVolatile(): void;
/**
* Sets the array observer factory
* @param factory - Function that creates array observers
*/
setArrayObserverFactory(factory: (array: any[]) => Notifier): void;
};Usage Examples:
import { Observable, Notifier, Subscriber } from "@microsoft/fast-element";
// Manual observable property definition
class ManualObservable {
private _value: string = "";
constructor() {
// Define observable property manually
Observable.defineProperty(this, "value");
}
get value(): string {
return this._value;
}
set value(newValue: string) {
if (this._value !== newValue) {
const oldValue = this._value;
this._value = newValue;
// Notify observers of the change
Observable.notify(this, { oldValue, newValue });
}
}
}
// Custom subscriber for observable changes
class CustomSubscriber implements Subscriber {
handleChange(source: any, args: any): void {
console.log("Property changed:", args);
}
}
// Track property access and subscribe to changes
const obj = new ManualObservable();
const notifier = Observable.getNotifier(obj);
const subscriber = new CustomSubscriber();
notifier.subscribe(subscriber, "value");
obj.value = "new value"; // Triggers subscriber notification
// Custom expression observer
class LoggingObserver implements Subscriber {
handleChange(source: any, args: any): void {
console.log("Expression changed:", source, args);
}
}
const target = { name: "John", age: 30 };
Observable.defineProperty(target, "name");
Observable.defineProperty(target, "age");
// Create expression observer
const fullNameExpression = (source: typeof target) => `${source.name} (${source.age})`;
const observer = new LoggingObserver();
const binding = Observable.binding(fullNameExpression, observer, false);
// Simulate binding
const controller = {
source: target,
context: { index: 0, length: 1, parent: null, parentContext: null },
isBound: true,
onUnbind: () => {},
sourceLifetime: undefined
};
binding.bind(controller); // Sets up observation
target.name = "Jane"; // Triggers expression re-evaluation and logging
target.age = 31; // Triggers expression re-evaluation and loggingImplementation that manages change notifications for individual properties on objects.
/**
* Manages property change notifications for an object
*/
class PropertyChangeNotifier implements Notifier {
/**
* Creates a notifier for the specified source object
* @param source - The object to create notifications for
*/
constructor(source: any);
/**
* Subscribes to changes for a specific property
* @param subscriber - The subscriber to notify of changes
* @param propertyToWatch - The property name to watch
*/
subscribe(subscriber: Subscriber, propertyToWatch?: string): void;
/**
* Unsubscribes from changes for a specific property
* @param subscriber - The subscriber to remove
* @param propertyToWatch - The property name to stop watching
*/
unsubscribe(subscriber: Subscriber, propertyToWatch?: string): void;
/**
* Notifies subscribers of property changes
* @param args - Arguments describing the change
*/
notify(args: any): void;
}
/**
* Set of subscribers for efficient notification management
*/
class SubscriberSet {
/**
* Adds a subscriber to the set
* @param subscriber - The subscriber to add
*/
add(subscriber: Subscriber): boolean;
/**
* Removes a subscriber from the set
* @param subscriber - The subscriber to remove
*/
remove(subscriber: Subscriber): boolean;
/**
* Notifies all subscribers in the set
* @param args - Arguments to pass to subscribers
*/
notify(args: any): void;
}Usage Examples:
import { PropertyChangeNotifier, Subscriber, Observable } from "@microsoft/fast-element";
// Custom object with manual change notifications
class DataModel {
private _name: string = "";
private _status: string = "pending";
private notifier: PropertyChangeNotifier;
constructor() {
this.notifier = new PropertyChangeNotifier(this);
}
get name(): string {
Observable.track(this, "name");
return this._name;
}
set name(value: string) {
if (this._name !== value) {
const oldValue = this._name;
this._name = value;
this.notifier.notify({
type: "change",
propertyName: "name",
oldValue,
newValue: value
});
}
}
get status(): string {
Observable.track(this, "status");
return this._status;
}
set status(value: string) {
if (this._status !== value) {
const oldValue = this._status;
this._status = value;
this.notifier.notify({
type: "change",
propertyName: "status",
oldValue,
newValue: value
});
}
}
// Subscribe to specific property changes
onNameChanged(callback: (oldValue: string, newValue: string) => void): void {
const subscriber: Subscriber = {
handleChange: (source, args) => {
if (args.propertyName === "name") {
callback(args.oldValue, args.newValue);
}
}
};
this.notifier.subscribe(subscriber, "name");
}
onStatusChanged(callback: (oldValue: string, newValue: string) => void): void {
const subscriber: Subscriber = {
handleChange: (source, args) => {
if (args.propertyName === "status") {
callback(args.oldValue, args.newValue);
}
}
};
this.notifier.subscribe(subscriber, "status");
}
}
// Usage
const model = new DataModel();
model.onNameChanged((oldVal, newVal) => {
console.log(`Name changed from ${oldVal} to ${newVal}`);
});
model.onStatusChanged((oldVal, newVal) => {
console.log(`Status changed from ${oldVal} to ${newVal}`);
});
model.name = "John"; // Logs: "Name changed from to John"
model.status = "active"; // Logs: "Status changed from pending to active"Specialized system for tracking changes in arrays, including splice operations, sorting, and length changes.
/**
* Observes changes to arrays, tracking splices and mutations
*/
class ArrayObserver implements Notifier {
/**
* Creates an array observer for the specified array
* @param array - The array to observe
*/
constructor(array: any[]);
/** Subscribes to array change notifications */
subscribe(subscriber: Subscriber): void;
/** Unsubscribes from array change notifications */
unsubscribe(subscriber: Subscriber): void;
/** Notifies subscribers of array changes */
notify(args: any): void;
}
/**
* Observes array length changes specifically
*/
class LengthObserver {
/**
* Creates a length observer for the specified array
* @param array - The array to observe length changes for
*/
constructor(array: any[]);
/** Gets the current length of the observed array */
getValue(): number;
}
/**
* Gets an observable version of array length
* @param array - The array to get the length of
* @returns Observable length value
*/
function lengthOf(array: any[]): number;
/**
* Represents a splice operation on an array
*/
class Splice {
/** Indicates that this splice represents a complete array reset */
reset?: boolean;
/**
* Creates a splice record
* @param index - The index where the splice occurs
* @param removed - The items that were removed
* @param addedCount - The number of items that were added
*/
constructor(
public index: number,
public removed: any[],
public addedCount: number
);
/**
* Adjusts the splice index based on the provided array
* @param array - The array to adjust to
* @returns The same splice, mutated based on the reference array
*/
adjustTo(array: any[]): this;
}
/**
* Represents a sort operation on an array
*/
class Sort {
/**
* Creates a sort update record
* @param sorted - The updated index positions of sorted items
*/
constructor(public sorted?: number[]);
}
/**
* Strategy for handling array splice operations
*/
interface SpliceStrategy {
/** The level of support provided by this strategy */
readonly support: SpliceStrategySupport;
/**
* Processes array changes and returns splice records
* @param array - The array that changed
* @param oldArray - The previous state of the array
* @returns Array of splice records describing the changes
*/
process(array: any[], oldArray?: any[]): Splice[];
/** Resets the strategy state */
reset(): void;
}
/** Available splice strategy support levels */
const SpliceStrategySupport: {
readonly reset: 1;
readonly splice: 2;
readonly optimized: 3;
};Usage Examples:
import {
ArrayObserver,
lengthOf,
Splice,
Sort,
Subscriber,
Observable
} from "@microsoft/fast-element";
// Observable array in a component
@customElement("array-example")
export class ArrayExample extends FASTElement {
@observable items: string[] = ["apple", "banana", "orange"];
@observable numbers: number[] = [1, 2, 3, 4, 5];
// Computed property using lengthOf
get itemCount(): number {
return lengthOf(this.items);
}
// Methods that modify arrays
addItem(item: string) {
this.items.push(item); // Triggers array change notification
}
removeItem(index: number) {
this.items.splice(index, 1); // Triggers splice notification
}
sortItems() {
this.items.sort(); // Triggers sort notification
}
clearItems() {
this.items.length = 0; // Triggers array reset
}
replaceItems(newItems: string[]) {
this.items = newItems; // Triggers complete array replacement
}
// Bulk operations
addMultipleItems(items: string[]) {
this.items.push(...items); // Single splice operation
}
insertItem(index: number, item: string) {
this.items.splice(index, 0, item); // Insert at specific index
}
}
// Custom array subscriber
class ArrayChangeLogger implements Subscriber {
handleChange(source: any[], args: Splice | Sort): void {
if (args instanceof Splice) {
console.log("Array splice:", {
index: args.index,
removed: args.removed,
added: args.addedCount,
reset: args.reset
});
} else if (args instanceof Sort) {
console.log("Array sorted:", args.sorted);
}
}
}
// Manual array observation
const observableArray = ["a", "b", "c"];
Observable.setArrayObserverFactory((array) => new ArrayObserver(array));
const arrayNotifier = Observable.getNotifier(observableArray);
const logger = new ArrayChangeLogger();
arrayNotifier.subscribe(logger);
observableArray.push("d"); // Logs splice operation
observableArray.sort(); // Logs sort operation
observableArray.splice(1, 2, "x", "y"); // Logs splice with removal and addition
// Template that reacts to array changes
const arrayTemplate = html<ArrayExample>`
<div class="summary">
Total items: ${x => lengthOf(x.items)}
</div>
<ul class="items">
${x => x.items.map((item, index) =>
`<li data-index="${index}">${item}</li>`
).join('')}
</ul>
<div class="numbers">
Sum: ${x => x.numbers.reduce((sum, num) => sum + num, 0)}
</div>
<button @click="${x => x.addItem('new item')}">Add Item</button>
<button @click="${x => x.sortItems()}">Sort Items</button>
<button @click="${x => x.clearItems()}">Clear All</button>
`;
// Advanced array operations with custom splice strategy
class CustomSpliceStrategy implements SpliceStrategy {
readonly support = SpliceStrategySupport.optimized;
process(array: any[], oldArray?: any[]): Splice[] {
// Custom logic for optimizing splice detection
if (!oldArray) {
return [new Splice(0, [], array.length)];
}
// Implement custom diffing algorithm
const splices: Splice[] = [];
// ... custom splice detection logic
return splices;
}
reset(): void {
// Reset strategy state
}
}System for marking properties as volatile, ensuring they are always re-evaluated even when dependencies haven't changed.
/**
* Decorator: Marks a property getter as having volatile observable dependencies
* @param target - The target that the property is defined on
* @param name - The property name or accessor
* @param descriptor - The existing property descriptor
* @returns Modified property descriptor that tracks volatility
*/
function volatile(
target: {},
name: string | Accessor,
descriptor: PropertyDescriptor
): PropertyDescriptor;Usage Examples:
import { FASTElement, customElement, html, observable, volatile } from "@microsoft/fast-element";
@customElement("volatile-example")
export class VolatileExample extends FASTElement {
@observable currentTime: Date = new Date();
@observable counter: number = 0;
// Volatile property - always re-evaluates
@volatile
get timestamp(): string {
return new Date().toISOString(); // Always returns current time
}
// Volatile computed property
@volatile
get randomValue(): number {
return Math.random(); // Always returns a new random number
}
// Volatile property that depends on external state
@volatile
get windowWidth(): number {
return window.innerWidth; // Always gets current window width
}
// Non-volatile computed property (for comparison)
get formattedCounter(): string {
return `Count: ${this.counter}`;
}
// Volatile property with complex logic
@volatile
get systemStatus(): string {
// This might depend on external factors that can't be tracked
const isOnline = navigator.onLine;
const memoryUsage = (performance as any).memory?.usedJSHeapSize;
const currentLoad = Math.random(); // Simulated system load
if (!isOnline) return "offline";
if (memoryUsage > 50000000) return "high-memory";
if (currentLoad > 0.8) return "high-load";
return "normal";
}
connectedCallback() {
super.connectedCallback();
// Update non-volatile observable periodically
setInterval(() => {
this.currentTime = new Date();
this.counter++;
}, 1000);
}
}
// Template that uses volatile properties
const volatileTemplate = html<VolatileExample>`
<div class="timestamps">
<p>Stored Time: ${x => x.currentTime.toISOString()}</p>
<p>Current Time: ${x => x.timestamp}</p>
</div>
<div class="values">
<p>Counter: ${x => x.formattedCounter}</p>
<p>Random: ${x => x.randomValue}</p>
<p>Window Width: ${x => x.windowWidth}px</p>
</div>
<div class="status">
System Status: ${x => x.systemStatus}
</div>
`;
// Example showing difference between volatile and non-volatile
@customElement("comparison-example")
export class ComparisonExample extends FASTElement {
@observable triggerUpdate: number = 0;
// Non-volatile: only re-evaluates when triggerUpdate changes
get nonVolatileRandom(): number {
console.log("Non-volatile random calculated");
return Math.random();
}
// Volatile: re-evaluates on every template update
@volatile
get volatileRandom(): number {
console.log("Volatile random calculated");
return Math.random();
}
triggerChange() {
this.triggerUpdate++;
}
}
const comparisonTemplate = html<ComparisonExample>`
<div>
<p>Trigger: ${x => x.triggerUpdate}</p>
<p>Non-volatile: ${x => x.nonVolatileRandom}</p>
<p>Volatile: ${x => x.volatileRandom}</p>
<button @click="${x => x.triggerChange()}">Update</button>
</div>
`;
// Custom volatile behavior
class CustomVolatileClass {
@observable baseValue: number = 10;
// Volatile getter that depends on external timing
@volatile
get timeBasedValue(): number {
const seconds = new Date().getSeconds();
return this.baseValue * (seconds % 10);
}
// Volatile getter for external API calls
@volatile
get externalData(): Promise<any> {
// Always fetches fresh data
return fetch('/api/current-data').then(r => r.json());
}
}Batch update system that manages asynchronous property change notifications for optimal performance.
/**
* Manages batched updates for optimal performance
*/
const Updates: {
/**
* Enqueues a task for batch processing
* @param callable - The task to enqueue
*/
enqueue(callable: Callable): void;
/**
* Processes all enqueued updates immediately
*/
process(): void;
/**
* Sets the update mode
* @param isAsync - Whether updates should be processed asynchronously
*/
setMode(isAsync: boolean): void;
};
/**
* Update queue for managing batched notifications
*/
const UpdateQueue: {
/**
* Enqueues an observer for update processing
* @param observer - The observer to enqueue
*/
enqueue(observer: any): void;
/**
* Processes the update queue
*/
process(): void;
};/**
* Represents a getter/setter property accessor on an object
*/
interface Accessor {
/** The name of the property */
name: string;
/**
* Gets the value of the property on the source object
* @param source - The source object to access
*/
getValue(source: any): any;
/**
* Sets the value of the property on the source object
* @param source - The source object to access
* @param value - The value to set the property to
*/
setValue(source: any, value: any): void;
}
/**
* A record of observable property access
*/
interface ObservationRecord {
/** The source object with an observable property that was accessed */
propertySource: any;
/** The name of the observable property that was accessed */
propertyName: string;
}
/**
* Describes how the source's lifetime relates to its controller's lifetime
*/
type SourceLifetime = undefined | 1; // unknown | coupled
/**
* Controls the lifecycle of an expression and provides relevant context
*/
interface ExpressionController<TSource = any, TParent = any> {
/** The source the expression is evaluated against */
readonly source: TSource;
/** Indicates how the source's lifetime relates to the controller's lifetime */
readonly sourceLifetime?: SourceLifetime;
/** The context the expression is evaluated against */
readonly context: ExecutionContext<TParent>;
/** Indicates whether the controller is bound */
readonly isBound: boolean;
/**
* Registers an unbind handler with the controller
* @param behavior - An object to call when the controller unbinds
*/
onUnbind(behavior: { unbind(controller: ExpressionController<TSource, TParent>): void }): void;
}
/**
* Observes an expression for changes
*/
interface ExpressionObserver<TSource = any, TReturn = any, TParent = any> {
/**
* Binds the expression to the source
* @param controller - The controller that manages the lifecycle and context
*/
bind(controller: ExpressionController<TSource, TParent>): TReturn;
}
/**
* Provides additional contextual information available to behaviors and expressions
*/
interface ExecutionContext<TParent = any> {
/** The index of the current item within a repeat context */
index: number;
/** The length of the current collection within a repeat context */
length: number;
/** The parent data source within a nested context */
parent: TParent;
/** The parent execution context when in nested context scenarios */
parentContext: ExecutionContext<TParent>;
/** The current event within an event handler */
readonly event: Event;
/** Indicates whether the current item has an even index */
readonly isEven: boolean;
/** Indicates whether the current item has an odd index */
readonly isOdd: boolean;
/** Indicates whether the current item is the first item */
readonly isFirst: boolean;
/** Indicates whether the current item is in the middle */
readonly isInMiddle: boolean;
/** Indicates whether the current item is the last item */
readonly isLast: boolean;
/** Returns the typed event detail of a custom event */
eventDetail<TDetail>(): TDetail;
/** Returns the typed event target of the event */
eventTarget<TTarget extends EventTarget>(): TTarget;
}