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
Reactive state management with reactive objects, computed values, and change watchers for complex application state management beyond component boundaries.
Functions for creating reactive state values that can be shared across components and automatically trigger updates when changed.
/**
* Creates a reactive state value
* @param value - The initial state value
* @param options - Options to customize the state or a friendly name
* @returns A State instance
*/
function state<T>(
value: T,
options?: string | StateOptions
): State<T>;
/**
* Options for creating state
*/
interface StateOptions {
/** Indicates whether to deeply make the state value observable */
deep?: boolean;
/** A friendly name for the state */
name?: string;
}
/**
* A read/write stateful value
*/
interface State<T> extends ReadonlyState<T> {
/** Gets or sets the current state value */
current: T;
/**
* Sets the current state value
* @param value - The new state value
*/
set(value: T): void;
/** Creates a readonly version of the state */
asReadonly(): ReadonlyState<T>;
}
/**
* A readonly stateful value
*/
interface ReadonlyState<T> {
/** Gets the current state value */
(): T;
/** Gets the current state value */
readonly current: T;
}Usage Examples:
import { FASTElement, customElement, html, Observable } from "@microsoft/fast-element";
import { state } from "@microsoft/fast-element/state.js";
// Global application state
const appState = state({
user: null as User | null,
theme: 'light' as 'light' | 'dark',
isLoading: false,
notifications: [] as Notification[]
}, { name: 'AppState', deep: true });
// Counter state with simple value
const counterState = state(0, 'Counter');
// User preferences state
const userPreferences = state({
language: 'en',
timezone: 'UTC',
emailNotifications: true,
darkMode: false
}, { deep: true, name: 'UserPreferences' });
// Shopping cart state
const cartState = state({
items: [] as CartItem[],
total: 0,
coupon: null as string | null
}, { deep: true });
@customElement("state-example")
export class StateExample extends FASTElement {
// Local component state
private localState = state({
selectedTab: 'profile',
formData: {
name: '',
email: '',
bio: ''
}
}, { deep: true });
connectedCallback() {
super.connectedCallback();
// Subscribe to global state changes
this.subscribeToStateChanges();
}
private subscribeToStateChanges() {
// Watch for user changes
const userNotifier = Observable.getNotifier(appState.current);
userNotifier.subscribe({
handleChange: (source, args) => {
console.log('App state changed:', args);
this.$fastController.update();
}
}, 'user');
// Watch for theme changes
userNotifier.subscribe({
handleChange: (source, args) => {
this.updateTheme(appState.current.theme);
this.$fastController.update();
}
}, 'theme');
}
// Actions that modify global state
login(user: User) {
appState.set({
...appState.current,
user,
isLoading: false
});
}
logout() {
appState.set({
...appState.current,
user: null
});
}
toggleTheme() {
const newTheme = appState.current.theme === 'light' ? 'dark' : 'light';
appState.set({
...appState.current,
theme: newTheme
});
}
addNotification(notification: Notification) {
const current = appState.current;
current.notifications.push(notification);
appState.set(current); // Trigger update
}
// Local state actions
selectTab(tab: string) {
this.localState.set({
...this.localState.current,
selectedTab: tab
});
}
updateFormData(field: string, value: string) {
const current = this.localState.current;
current.formData[field as keyof typeof current.formData] = value;
this.localState.set(current);
}
private updateTheme(theme: string) {
document.documentElement.setAttribute('data-theme', theme);
}
static template = html<StateExample>`
<div class="state-demo">
<header>
<h1>State Management Demo</h1>
<div class="user-info">
${() => appState().user
? html`Welcome, ${() => appState().user?.name}!`
: html`<button @click="${x => x.showLogin()}">Login</button>`
}
<button @click="${x => x.toggleTheme()}">
Theme: ${() => appState().theme}
</button>
</div>
</header>
<main>
<div class="tabs">
<button
class="${x => x.localState().selectedTab === 'profile' ? 'active' : ''}"
@click="${x => x.selectTab('profile')}">
Profile
</button>
<button
class="${x => x.localState().selectedTab === 'settings' ? 'active' : ''}"
@click="${x => x.selectTab('settings')}">
Settings
</button>
</div>
<div class="content">
${x => x.localState().selectedTab === 'profile'
? x.renderProfile()
: x.renderSettings()
}
</div>
</main>
<footer>
<div class="notifications">
${() => appState().notifications.map(n =>
`<div class="notification">${n.message}</div>`
).join('')}
</div>
</footer>
</div>
`;
private renderProfile() {
return html`
<form>
<input
type="text"
placeholder="Name"
.value="${x => x.localState().formData.name}"
@input="${(x, e) => x.updateFormData('name', (e.target as HTMLInputElement).value)}">
<input
type="email"
placeholder="Email"
.value="${x => x.localState().formData.email}"
@input="${(x, e) => x.updateFormData('email', (e.target as HTMLInputElement).value)}">
<textarea
placeholder="Bio"
.value="${x => x.localState().formData.bio}"
@input="${(x, e) => x.updateFormData('bio', (e.target as HTMLTextAreaElement).value)}">
</textarea>
</form>
`;
}
private renderSettings() {
return html`
<div class="settings">
<p>User preferences will go here</p>
<p>Current language: ${() => userPreferences().language}</p>
<p>Notifications: ${() => userPreferences().emailNotifications ? 'On' : 'Off'}</p>
</div>
`;
}
private showLogin() {
// Simulate login
this.login({
id: '1',
name: 'John Doe',
email: 'john@example.com'
});
}
}
// State management utilities
class StateManager {
private static states = new Map<string, State<any>>();
static createNamedState<T>(name: string, initialValue: T, options?: StateOptions): State<T> {
if (this.states.has(name)) {
return this.states.get(name)!;
}
const newState = state(initialValue, { ...options, name });
this.states.set(name, newState);
return newState;
}
static getState<T>(name: string): State<T> | undefined {
return this.states.get(name);
}
static destroyState(name: string): boolean {
return this.states.delete(name);
}
static getAllStates(): Map<string, State<any>> {
return new Map(this.states);
}
}
// Usage of state manager
const globalSettings = StateManager.createNamedState('globalSettings', {
apiUrl: 'https://api.example.com',
retryAttempts: 3,
timeout: 5000
});
interface User {
id: string;
name: string;
email: string;
}
interface Notification {
id: string;
message: string;
type: 'info' | 'warning' | 'error';
timestamp: Date;
}
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}Scoped state management for component-specific state that's tied to component lifecycle.
/**
* Creates owner-scoped reactive state
* @param owner - The owner object that controls the state lifecycle
* @param value - The initial state value
* @param options - Options to customize the state or a friendly name
* @returns An OwnedState instance
*/
function ownedState<T>(
value: T | (() => T),
options?: string | StateOptions
): OwnedState<T>;
/**
* A read/write owned state value tied to an owner's lifecycle
*/
interface OwnedState<T> extends State<T> {
/** The owner of this state */
readonly owner: any;
}
/**
* A readonly owned state value
*/
interface ReadonlyOwnedState<T> extends ReadonlyState<T> {
/** The owner of this state */
readonly owner: any;
}Usage Examples:
import { FASTElement, customElement, html } from "@microsoft/fast-element";
import { ownedState } from "@microsoft/fast-element/state.js";
@customElement("owned-state-example")
export class OwnedStateExample extends FASTElement {
// State owned by this component instance
private componentState = ownedState(this, {
counter: 0,
items: [] as string[],
isExpanded: false
}, { deep: true, name: `ComponentState-${this.id || 'unknown'}` });
// Separate owned state for form data
private formState = ownedState(this, {
name: '',
email: '',
message: '',
isValid: false
}, 'FormState');
connectedCallback() {
super.connectedCallback();
console.log(`Component ${this.id} connected with state:`, this.componentState());
}
disconnectedCallback() {
super.disconnectedCallback();
console.log(`Component ${this.id} disconnected, state cleanup automatic`);
}
// Component-specific actions
incrementCounter() {
const current = this.componentState.current;
this.componentState.set({
...current,
counter: current.counter + 1
});
}
addItem() {
const current = this.componentState.current;
current.items.push(`Item ${current.items.length + 1}`);
this.componentState.set(current);
}
toggleExpanded() {
const current = this.componentState.current;
this.componentState.set({
...current,
isExpanded: !current.isExpanded
});
}
updateForm(field: string, value: string) {
const current = this.formState.current;
(current as any)[field] = value;
// Validate form
current.isValid = current.name.length > 0 &&
current.email.includes('@') &&
current.message.length > 10;
this.formState.set(current);
}
submitForm() {
if (this.formState().isValid) {
console.log('Submitting form:', this.formState());
// Reset form after submission
this.formState.set({
name: '',
email: '',
message: '',
isValid: false
});
}
}
static template = html<OwnedStateExample>`
<div class="owned-state-demo">
<h2>Owned State Demo</h2>
<section class="counter">
<p>Counter: ${x => x.componentState().counter}</p>
<button @click="${x => x.incrementCounter()}">Increment</button>
</section>
<section class="items">
<p>Items (${x => x.componentState().items.length}):</p>
<ul>
${x => x.componentState().items.map(item =>
`<li>${item}</li>`
).join('')}
</ul>
<button @click="${x => x.addItem()}">Add Item</button>
</section>
<section class="expandable">
<button @click="${x => x.toggleExpanded()}">
${x => x.componentState().isExpanded ? 'Collapse' : 'Expand'}
</button>
<div ?hidden="${x => !x.componentState().isExpanded}">
<p>This content is toggleable!</p>
</div>
</section>
<section class="form">
<h3>Contact Form</h3>
<form @submit="${x => x.submitForm()}">
<input
type="text"
placeholder="Name"
.value="${x => x.formState().name}"
@input="${(x, e) => x.updateForm('name', (e.target as HTMLInputElement).value)}">
<input
type="email"
placeholder="Email"
.value="${x => x.formState().email}"
@input="${(x, e) => x.updateForm('email', (e.target as HTMLInputElement).value)}">
<textarea
placeholder="Message (min 10 characters)"
.value="${x => x.formState().message}"
@input="${(x, e) => x.updateForm('message', (e.target as HTMLTextAreaElement).value)}">
</textarea>
<button type="submit" ?disabled="${x => !x.formState().isValid}">
Submit
</button>
</form>
</section>
</div>
`;
}
// Multiple instances demonstrate independent owned state
@customElement("state-instance-demo")
export class StateInstanceDemo extends FASTElement {
static template = html`
<div class="instance-demo">
<h1>Multiple Component Instances</h1>
<p>Each component below has its own independent owned state:</p>
<owned-state-example id="instance-1"></owned-state-example>
<owned-state-example id="instance-2"></owned-state-example>
<owned-state-example id="instance-3"></owned-state-example>
</div>
`;
}Derived state that automatically recalculates when dependencies change, providing efficient computed values.
/**
* Creates computed state that derives its value from other reactive sources
* @param compute - Function that computes the derived value
* @returns A ComputedState instance
*/
function computedState<T>(compute: ComputedInitializer<T>): ComputedState<T>;
/**
* Function that computes a derived value
*/
type ComputedInitializer<T> = (builder: ComputedBuilder) => T;
/**
* Builder for setting up computed state dependencies
*/
interface ComputedBuilder {
/**
* Sets up the compute function
* @param callback - The callback that computes the value
*/
setup(callback: ComputedSetupCallback): void;
}
/**
* Callback for computed state setup
*/
type ComputedSetupCallback = () => void;
/**
* A computed state value that derives from other reactive sources
*/
interface ComputedState<T> extends ReadonlyState<T> {
/** Indicates this is a computed state */
readonly isComputed: true;
}Usage Examples:
import { FASTElement, customElement, html } from "@microsoft/fast-element";
import { state, computedState } from "@microsoft/fast-element/state.js";
// Base reactive states
const userState = state({
firstName: 'John',
lastName: 'Doe',
birthDate: new Date('1990-01-01'),
email: 'john.doe@example.com'
}, { deep: true });
const cartState = state({
items: [
{ id: 1, name: 'Widget A', price: 10.99, quantity: 2 },
{ id: 2, name: 'Widget B', price: 5.50, quantity: 1 },
{ id: 3, name: 'Widget C', price: 15.00, quantity: 3 }
] as CartItem[],
taxRate: 0.08,
shippingCost: 5.99,
discountPercent: 0
}, { deep: true });
const appState = state({
currentRoute: '/home',
isAuthenticated: false,
theme: 'light' as 'light' | 'dark',
language: 'en'
});
// Computed states that derive from base states
const userDisplayName = computedState(builder => {
const user = userState();
return `${user.firstName} ${user.lastName}`;
});
const userAge = computedState(builder => {
const user = userState();
const today = new Date();
const birthDate = new Date(user.birthDate);
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
});
const cartSubtotal = computedState(builder => {
const cart = cartState();
return cart.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
});
const cartTax = computedState(builder => {
const cart = cartState();
const subtotal = cartSubtotal();
return subtotal * cart.taxRate;
});
const cartDiscount = computedState(builder => {
const cart = cartState();
const subtotal = cartSubtotal();
return subtotal * (cart.discountPercent / 100);
});
const cartTotal = computedState(builder => {
const cart = cartState();
const subtotal = cartSubtotal();
const tax = cartTax();
const discount = cartDiscount();
return subtotal + tax - discount + cart.shippingCost;
});
const cartItemCount = computedState(builder => {
const cart = cartState();
return cart.items.reduce((sum, item) => sum + item.quantity, 0);
});
const isCartEmpty = computedState(builder => {
return cartItemCount() === 0;
});
const currentPageTitle = computedState(builder => {
const app = appState();
const routes: Record<string, string> = {
'/home': 'Home',
'/products': 'Products',
'/cart': 'Shopping Cart',
'/profile': 'User Profile',
'/settings': 'Settings'
};
return routes[app.currentRoute] || 'Page Not Found';
});
const isUserMinor = computedState(builder => {
return userAge() < 18;
});
@customElement("computed-state-example")
export class ComputedStateExample extends FASTElement {
// Local computed state
private localMessage = computedState(builder => {
const displayName = userDisplayName();
const age = userAge();
const itemCount = cartItemCount();
return `Hello ${displayName} (${age} years old)! You have ${itemCount} items in your cart.`;
});
private cartSummary = computedState(builder => {
const subtotal = cartSubtotal();
const tax = cartTax();
const discount = cartDiscount();
const total = cartTotal();
const isEmpty = isCartEmpty();
if (isEmpty) {
return 'Your cart is empty';
}
return {
subtotal: subtotal.toFixed(2),
tax: tax.toFixed(2),
discount: discount.toFixed(2),
total: total.toFixed(2),
savings: discount > 0 ? `You saved $${discount.toFixed(2)}!` : null
};
});
// Actions that modify base state
updateUser(field: string, value: any) {
const current = userState.current;
(current as any)[field] = value;
userState.set(current);
}
addCartItem() {
const current = cartState.current;
const newItem = {
id: Date.now(),
name: `New Item ${current.items.length + 1}`,
price: Math.random() * 20 + 5,
quantity: 1
};
current.items.push(newItem);
cartState.set(current);
}
updateQuantity(itemId: number, newQuantity: number) {
const current = cartState.current;
const item = current.items.find(i => i.id === itemId);
if (item) {
if (newQuantity <= 0) {
current.items = current.items.filter(i => i.id !== itemId);
} else {
item.quantity = newQuantity;
}
cartState.set(current);
}
}
applyDiscount(percent: number) {
const current = cartState.current;
current.discountPercent = percent;
cartState.set(current);
}
navigateTo(route: string) {
appState.set({
...appState.current,
currentRoute: route
});
}
static template = html<ComputedStateExample>`
<div class="computed-state-demo">
<header>
<h1>${() => currentPageTitle()}</h1>
<nav>
<button @click="${x => x.navigateTo('/home')}">Home</button>
<button @click="${x => x.navigateTo('/products')}">Products</button>
<button @click="${x => x.navigateTo('/cart')}">Cart (${() => cartItemCount()})</button>
<button @click="${x => x.navigateTo('/profile')}">Profile</button>
</nav>
</header>
<main>
<section class="user-info">
<h2>User Information</h2>
<p>${x => x.localMessage()}</p>
<div class="user-form">
<input
type="text"
placeholder="First Name"
.value="${() => userState().firstName}"
@input="${(x, e) => x.updateUser('firstName', (e.target as HTMLInputElement).value)}">
<input
type="text"
placeholder="Last Name"
.value="${() => userState().lastName}"
@input="${(x, e) => x.updateUser('lastName', (e.target as HTMLInputElement).value)}">
<input
type="date"
.value="${() => userState().birthDate.toISOString().split('T')[0]}"
@input="${(x, e) => x.updateUser('birthDate', new Date((e.target as HTMLInputElement).value))}">
</div>
<div class="computed-values">
<p><strong>Display Name:</strong> ${() => userDisplayName()}</p>
<p><strong>Age:</strong> ${() => userAge()} years old</p>
<p><strong>Status:</strong> ${() => isUserMinor() ? 'Minor' : 'Adult'}</p>
</div>
</section>
<section class="cart-info">
<h2>Shopping Cart</h2>
<div class="cart-items">
${() => cartState().items.map(item =>
`<div class="cart-item">
<span>${item.name}</span>
<span>$${item.price.toFixed(2)}</span>
<input type="number"
value="${item.quantity}"
min="0"
onchange="this.getRootNode().host.updateQuantity(${item.id}, parseInt(this.value))">
<span>$${(item.price * item.quantity).toFixed(2)}</span>
</div>`
).join('')}
</div>
<div class="cart-actions">
<button @click="${x => x.addCartItem()}">Add Random Item</button>
<button @click="${x => x.applyDiscount(10)}">Apply 10% Discount</button>
<button @click="${x => x.applyDiscount(0)}">Remove Discount</button>
</div>
<div class="cart-summary">
${x => {
const summary = x.cartSummary();
if (typeof summary === 'string') {
return `<p>${summary}</p>`;
}
return `
<div class="summary-line">
<span>Subtotal:</span>
<span>$${summary.subtotal}</span>
</div>
<div class="summary-line">
<span>Tax:</span>
<span>$${summary.tax}</span>
</div>
<div class="summary-line">
<span>Discount:</span>
<span>-$${summary.discount}</span>
</div>
<div class="summary-line">
<span>Shipping:</span>
<span>$${cartState().shippingCost.toFixed(2)}</span>
</div>
<div class="summary-line total">
<span><strong>Total:</strong></span>
<span><strong>$${summary.total}</strong></span>
</div>
${summary.savings ? `<p class="savings">${summary.savings}</p>` : ''}
`;
}}
</div>
</section>
</main>
</div>
`;
}
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}Function for making existing objects reactive, enabling automatic change detection on object properties.
/**
* Makes an object reactive by adding observable properties
* @param target - The object to make reactive
* @param deep - Whether to deeply observe nested objects
* @returns The reactive version of the object
*/
function reactive<T extends object>(target: T, deep?: boolean): T;Usage Examples:
import { FASTElement, customElement, html, Observable } from "@microsoft/fast-element";
import { reactive } from "@microsoft/fast-element/state.js";
// Make existing objects reactive
const userModel = reactive({
profile: {
name: 'John Doe',
email: 'john@example.com',
avatar: '/avatars/john.jpg'
},
settings: {
theme: 'light',
notifications: true,
language: 'en'
},
preferences: {
layout: 'grid',
itemsPerPage: 20,
sortBy: 'name'
}
}, true); // deep reactive
const todoModel = reactive({
items: [
{ id: 1, text: 'Learn FAST Element', completed: false, priority: 'high' },
{ id: 2, text: 'Build awesome components', completed: false, priority: 'medium' },
{ id: 3, text: 'Ship to production', completed: false, priority: 'high' }
],
filter: 'all' as 'all' | 'active' | 'completed',
stats: {
total: 3,
completed: 0,
remaining: 3
}
}, true);
@customElement("reactive-example")
export class ReactiveExample extends FASTElement {
// Local reactive models
private formModel = reactive({
personalInfo: {
firstName: '',
lastName: '',
birthDate: '',
phone: ''
},
address: {
street: '',
city: '',
state: '',
zipCode: ''
},
validation: {
isValid: false,
errors: [] as string[]
}
}, true);
private dashboardModel = reactive({
widgets: [
{ id: 'weather', title: 'Weather', visible: true, position: { x: 0, y: 0 } },
{ id: 'calendar', title: 'Calendar', visible: true, position: { x: 1, y: 0 } },
{ id: 'tasks', title: 'Tasks', visible: false, position: { x: 0, y: 1 } },
{ id: 'news', title: 'News', visible: true, position: { x: 1, y: 1 } }
],
layout: 'grid' as 'grid' | 'list',
theme: 'light' as 'light' | 'dark'
});
connectedCallback() {
super.connectedCallback();
this.setupReactiveListeners();
}
private setupReactiveListeners() {
// Listen to user model changes
const userNotifier = Observable.getNotifier(userModel);
userNotifier.subscribe({
handleChange: (source, args) => {
console.log('User model changed:', args);
this.$fastController.update();
}
});
// Listen to todo model changes
const todoNotifier = Observable.getNotifier(todoModel);
todoNotifier.subscribe({
handleChange: (source, args) => {
this.updateTodoStats();
this.$fastController.update();
}
});
// Listen to form validation changes
const formNotifier = Observable.getNotifier(this.formModel);
formNotifier.subscribe({
handleChange: (source, args) => {
this.validateForm();
this.$fastController.update();
}
});
}
// User actions
updateUserProfile(field: string, value: any) {
const keys = field.split('.');
let target = userModel as any;
for (let i = 0; i < keys.length - 1; i++) {
target = target[keys[i]];
}
target[keys[keys.length - 1]] = value;
}
// Todo actions
addTodo(text: string) {
const newTodo = {
id: Date.now(),
text,
completed: false,
priority: 'medium' as 'low' | 'medium' | 'high'
};
todoModel.items.push(newTodo);
}
toggleTodo(id: number) {
const todo = todoModel.items.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
}
removeTodo(id: number) {
const index = todoModel.items.findIndex(t => t.id === id);
if (index !== -1) {
todoModel.items.splice(index, 1);
}
}
setFilter(filter: typeof todoModel.filter) {
todoModel.filter = filter;
}
private updateTodoStats() {
const completed = todoModel.items.filter(t => t.completed).length;
todoModel.stats.total = todoModel.items.length;
todoModel.stats.completed = completed;
todoModel.stats.remaining = todoModel.items.length - completed;
}
// Form actions
updateForm(field: string, value: any) {
const keys = field.split('.');
let target = this.formModel as any;
for (let i = 0; i < keys.length - 1; i++) {
target = target[keys[i]];
}
target[keys[keys.length - 1]] = value;
}
private validateForm() {
const { personalInfo, address } = this.formModel;
const errors: string[] = [];
if (!personalInfo.firstName) errors.push('First name is required');
if (!personalInfo.lastName) errors.push('Last name is required');
if (!personalInfo.birthDate) errors.push('Birth date is required');
if (!address.street) errors.push('Street address is required');
if (!address.city) errors.push('City is required');
if (!address.zipCode) errors.push('Zip code is required');
this.formModel.validation.errors = errors;
this.formModel.validation.isValid = errors.length === 0;
}
// Dashboard actions
toggleWidget(id: string) {
const widget = this.dashboardModel.widgets.find(w => w.id === id);
if (widget) {
widget.visible = !widget.visible;
}
}
moveWidget(id: string, x: number, y: number) {
const widget = this.dashboardModel.widgets.find(w => w.id === id);
if (widget) {
widget.position.x = x;
widget.position.y = y;
}
}
changeLayout(layout: typeof this.dashboardModel.layout) {
this.dashboardModel.layout = layout;
}
static template = html<ReactiveExample>`
<div class="reactive-demo">
<nav class="tabs">
<button>User Profile</button>
<button>Todo List</button>
<button>Form Demo</button>
<button>Dashboard</button>
</nav>
<section class="user-profile">
<h2>User Profile</h2>
<div class="profile-info">
<input
type="text"
placeholder="Name"
.value="${() => userModel.profile.name}"
@input="${(x, e) => x.updateUserProfile('profile.name', (e.target as HTMLInputElement).value)}">
<input
type="email"
placeholder="Email"
.value="${() => userModel.profile.email}"
@input="${(x, e) => x.updateUserProfile('profile.email', (e.target as HTMLInputElement).value)}">
<select
.value="${() => userModel.settings.theme}"
@change="${(x, e) => x.updateUserProfile('settings.theme', (e.target as HTMLSelectElement).value)}">
<option value="light">Light Theme</option>
<option value="dark">Dark Theme</option>
</select>
<label>
<input
type="checkbox"
.checked="${() => userModel.settings.notifications}"
@change="${(x, e) => x.updateUserProfile('settings.notifications', (e.target as HTMLInputElement).checked)}">
Enable Notifications
</label>
</div>
</section>
<section class="todo-list">
<h2>Todo List</h2>
<div class="todo-stats">
<p>Total: ${() => todoModel.stats.total}</p>
<p>Completed: ${() => todoModel.stats.completed}</p>
<p>Remaining: ${() => todoModel.stats.remaining}</p>
</div>
<div class="todo-filters">
<button
class="${() => todoModel.filter === 'all' ? 'active' : ''}"
@click="${x => x.setFilter('all')}">All</button>
<button
class="${() => todoModel.filter === 'active' ? 'active' : ''}"
@click="${x => x.setFilter('active')}">Active</button>
<button
class="${() => todoModel.filter === 'completed' ? 'active' : ''}"
@click="${x => x.setFilter('completed')}">Completed</button>
</div>
<div class="todo-items">
${() => todoModel.items
.filter(todo => {
if (todoModel.filter === 'active') return !todo.completed;
if (todoModel.filter === 'completed') return todo.completed;
return true;
})
.map(todo =>
`<div class="todo-item ${todo.completed ? 'completed' : ''}">
<input type="checkbox"
${todo.completed ? 'checked' : ''}
onchange="this.getRootNode().host.toggleTodo(${todo.id})">
<span class="todo-text">${todo.text}</span>
<span class="todo-priority ${todo.priority}">${todo.priority}</span>
<button onclick="this.getRootNode().host.removeTodo(${todo.id})">×</button>
</div>`
).join('')}
</div>
<form @submit="${(x, e) => {
e.preventDefault();
const input = (e.target as HTMLFormElement).querySelector('input') as HTMLInputElement;
if (input.value.trim()) {
x.addTodo(input.value.trim());
input.value = '';
}
}}">
<input type="text" placeholder="Add new todo...">
<button type="submit">Add</button>
</form>
</section>
</div>
`;
}Function for watching changes to reactive objects and properties, providing custom change handlers.
/**
* Watches an object for changes and executes a callback
* @param target - The object to watch
* @param callback - The callback to execute on changes
* @returns A disposable to stop watching
*/
function watch<T>(
target: T,
callback: (source: T, args: any) => void
): Disposable;Usage Examples:
import { Disposable } from "@microsoft/fast-element";
import { watch, reactive, state } from "@microsoft/fast-element/state.js";
// Reactive objects to watch
const appSettings = reactive({
theme: 'light',
language: 'en',
autoSave: true,
debugMode: false
});
const userSession = state({
userId: null as string | null,
loginTime: null as Date | null,
lastActivity: new Date(),
permissions: [] as string[]
}, { deep: true });
// Watch setup with proper cleanup
class WatchManager {
private watchers: Disposable[] = [];
setupWatchers() {
// Watch app settings changes
const settingsWatcher = watch(appSettings, (source, args) => {
console.log('App settings changed:', args);
this.handleSettingsChange(args);
});
// Watch user session changes
const sessionWatcher = watch(userSession.current, (source, args) => {
console.log('User session changed:', args);
this.handleSessionChange(args);
});
// Watch specific property changes
const themeWatcher = watch(appSettings, (source, args) => {
if (args.propertyName === 'theme') {
this.applyTheme(source.theme);
}
});
this.watchers.push(settingsWatcher, sessionWatcher, themeWatcher);
}
private handleSettingsChange(args: any) {
// Save settings to localStorage
localStorage.setItem('appSettings', JSON.stringify(appSettings));
// Apply settings
if (args.propertyName === 'language') {
this.loadLanguage(appSettings.language);
}
if (args.propertyName === 'debugMode') {
this.toggleDebugMode(appSettings.debugMode);
}
}
private handleSessionChange(args: any) {
if (args.propertyName === 'userId') {
if (userSession().userId) {
this.onUserLogin();
} else {
this.onUserLogout();
}
}
if (args.propertyName === 'lastActivity') {
this.resetIdleTimer();
}
}
private applyTheme(theme: string) {
document.documentElement.setAttribute('data-theme', theme);
}
private loadLanguage(language: string) {
// Load language resources
console.log(`Loading language: ${language}`);
}
private toggleDebugMode(enabled: boolean) {
if (enabled) {
console.log('Debug mode enabled');
window.addEventListener('error', this.handleGlobalError);
} else {
console.log('Debug mode disabled');
window.removeEventListener('error', this.handleGlobalError);
}
}
private onUserLogin() {
userSession.set({
...userSession.current,
loginTime: new Date(),
lastActivity: new Date()
});
}
private onUserLogout() {
// Clear sensitive data
userSession.set({
userId: null,
loginTime: null,
lastActivity: new Date(),
permissions: []
});
}
private resetIdleTimer() {
// Reset idle timer logic
}
private handleGlobalError = (event: ErrorEvent) => {
console.error('Global error:', event.error);
};
cleanup() {
// Clean up all watchers
this.watchers.forEach(watcher => watcher.dispose());
this.watchers = [];
}
}
// Component using watchers
@customElement("watch-example")
export class WatchExample extends FASTElement {
private watchManager = new WatchManager();
private localWatchers: Disposable[] = [];
connectedCallback() {
super.connectedCallback();
this.watchManager.setupWatchers();
this.setupLocalWatchers();
}
disconnectedCallback() {
super.disconnectedCallback();
this.watchManager.cleanup();
this.cleanupLocalWatchers();
}
private setupLocalWatchers() {
// Watch for validation changes
const validationWatcher = watch(this.formData, (source, args) => {
if (args.propertyName) {
this.validateField(args.propertyName, args.newValue);
}
});
// Watch for array changes
const itemsWatcher = watch(this.items, (source, args) => {
this.updateItemsDisplay();
this.saveItemsToStorage();
});
this.localWatchers.push(validationWatcher, itemsWatcher);
}
private cleanupLocalWatchers() {
this.localWatchers.forEach(watcher => watcher.dispose());
this.localWatchers = [];
}
// Reactive data
private formData = reactive({
email: '',
password: '',
confirmPassword: '',
agreeToTerms: false,
errors: {} as Record<string, string>
});
private items = reactive({
list: [] as Array<{ id: number; name: string; status: string }>,
filter: 'all'
});
private validateField(fieldName: string, value: any) {
const errors = { ...this.formData.errors };
switch (fieldName) {
case 'email':
if (!value || !value.includes('@')) {
errors.email = 'Please enter a valid email address';
} else {
delete errors.email;
}
break;
case 'password':
if (!value || value.length < 8) {
errors.password = 'Password must be at least 8 characters';
} else {
delete errors.password;
}
break;
case 'confirmPassword':
if (value !== this.formData.password) {
errors.confirmPassword = 'Passwords do not match';
} else {
delete errors.confirmPassword;
}
break;
}
this.formData.errors = errors;
}
private updateItemsDisplay() {
console.log('Items updated:', this.items.list.length);
}
private saveItemsToStorage() {
localStorage.setItem('items', JSON.stringify(this.items.list));
}
static template = html<WatchExample>`
<div class="watch-demo">
<h2>Watch Example</h2>
<p>Open console to see watch notifications</p>
<section class="settings">
<h3>App Settings</h3>
<label>
Theme:
<select .value="${() => appSettings.theme}"
@change="${(x, e) => appSettings.theme = (e.target as HTMLSelectElement).value}">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</label>
<label>
<input type="checkbox"
.checked="${() => appSettings.autoSave}"
@change="${(x, e) => appSettings.autoSave = (e.target as HTMLInputElement).checked}">
Auto Save
</label>
<label>
<input type="checkbox"
.checked="${() => appSettings.debugMode}"
@change="${(x, e) => appSettings.debugMode = (e.target as HTMLInputElement).checked}">
Debug Mode
</label>
</section>
</div>
`;
}/**
* Disposable interface for cleanup
*/
interface Disposable {
/** Disposes of the resource */
dispose(): void;
}
/**
* Options for state creation
*/
interface StateOptions {
/** Whether to deeply observe nested objects */
deep?: boolean;
/** Friendly name for the state */
name?: string;
}
/**
* Function signature for computed state initialization
*/
type ComputedInitializer<T> = (builder: ComputedBuilder) => T;
/**
* Function signature for computed setup callback
*/
type ComputedSetupCallback = () => void;
/**
* Builder interface for computed state setup
*/
interface ComputedBuilder {
setup(callback: ComputedSetupCallback): void;
}