CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-microsoft--fast-element

A library for constructing Web Components

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

attributes.mddocs/

Attributes

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

Capabilities

Attribute Decorator

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

/**
 * Decorator: Specifies an HTML attribute with configuration
 * @param config - The configuration for the attribute
 * @returns Property decorator function
 */
function attr(
  config?: DecoratorAttributeConfiguration
): (target: {}, property: string) => void;

/**
 * Decorator: Specifies an HTML attribute with default behavior
 * @param target - The class to define the attribute on
 * @param prop - The property name to be associated with the attribute
 */
function attr(target: {}, prop: string): void;

/**
 * Configuration for attribute decorator
 */
interface DecoratorAttributeConfiguration {
  /** Custom attribute name (defaults to lowercase property name) */
  attribute?: string;
  
  /** Attribute behavior mode */
  mode?: AttributeMode;
  
  /** Value converter for type conversion */
  converter?: ValueConverter;
}

/**
 * Attribute behavior modes
 */
type AttributeMode = 
  | "reflect"    // Two-way sync between property and attribute (default)
  | "boolean"    // Boolean attribute behavior (presence = true, absence = false)
  | "fromView";  // One-way sync from attribute to property only

Usage Examples:

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

const template = html<AttributeExample>`
  <div class="attribute-demo">
    <!-- Display current attribute values -->
    <div class="values">
      <p>Name: ${x => x.name}</p>
      <p>Age: ${x => x.age}</p>
      <p>Disabled: ${x => x.disabled ? 'Yes' : 'No'}</p>
      <p>Theme: ${x => x.theme}</p>
      <p>Count: ${x => x.count}</p>
      <p>Status: ${x => x.status}</p>
    </div>
    
    <!-- Controls to modify attributes -->
    <div class="controls">
      <input type="text" 
             placeholder="Name" 
             @input="${(x, e) => x.name = (e.target as HTMLInputElement).value}">
      <input type="number" 
             placeholder="Age" 
             @input="${(x, e) => x.age = parseInt((e.target as HTMLInputElement).value)}">
      <button @click="${x => x.disabled = !x.disabled}">
        Toggle Disabled
      </button>
      <select @change="${(x, e) => x.theme = (e.target as HTMLSelectElement).value}">
        <option value="light">Light</option>
        <option value="dark">Dark</option>
        <option value="auto">Auto</option>
      </select>
    </div>
  </div>
`;

@customElement({
  name: "attribute-example",
  template
})
export class AttributeExample extends FASTElement {
  // Basic attribute (reflect mode by default)
  @attr name: string = "";
  
  // Attribute with custom name
  @attr({ attribute: "user-age" })
  age: number = 0;
  
  // Boolean attribute
  @attr({ mode: "boolean" })
  disabled: boolean = false;
  
  // Attribute with custom converter
  @attr({ converter: customThemeConverter })
  theme: Theme = Theme.Light;
  
  // Nullable number attribute
  @attr({ converter: nullableNumberConverter })
  count: number | null = null;
  
  // From-view only attribute (no reflection)
  @attr({ mode: "fromView" })
  status: string = "ready";
  
  // Attribute change callbacks
  nameChanged(oldValue: string, newValue: string) {
    console.log(`Name changed from "${oldValue}" to "${newValue}"`);
  }
  
  ageChanged(oldValue: number, newValue: number) {
    console.log(`Age changed from ${oldValue} to ${newValue}`);
    if (newValue < 0) {
      this.age = 0; // Validate and correct
    }
  }
  
  disabledChanged(oldValue: boolean, newValue: boolean) {
    console.log(`Disabled changed from ${oldValue} to ${newValue}`);
    this.classList.toggle('disabled', newValue);
  }
  
  themeChanged(oldValue: Theme, newValue: Theme) {
    console.log(`Theme changed from ${oldValue} to ${newValue}`);
    document.documentElement.setAttribute('data-theme', newValue);
  }
}

// Custom theme enum and converter
enum Theme {
  Light = "light",
  Dark = "dark",
  Auto = "auto"
}

const customThemeConverter: ValueConverter = {
  toView(value: Theme): string {
    return value;
  },
  
  fromView(value: string): Theme {
    return Object.values(Theme).includes(value as Theme) 
      ? value as Theme 
      : Theme.Light;
  }
};

// Advanced attribute patterns
@customElement("advanced-attributes")
export class AdvancedAttributeExample extends FASTElement {
  // Multiple attributes with different behaviors
  @attr({ mode: "reflect" })
  title: string = "Default Title";
  
  @attr({ mode: "boolean" })
  expanded: boolean = false;
  
  @attr({ mode: "boolean" })
  loading: boolean = false;
  
  @attr({ mode: "fromView", attribute: "data-id" })
  dataId: string = "";
  
  // Complex object attribute with JSON converter
  @attr({ converter: jsonConverter })
  config: Config = { width: 300, height: 200 };
  
  // Array attribute with custom converter
  @attr({ converter: arrayConverter })
  tags: string[] = [];
  
  // Validation in change callbacks
  titleChanged(oldValue: string, newValue: string) {
    if (newValue.length > 100) {
      this.title = newValue.substring(0, 100);
      console.warn("Title truncated to 100 characters");
    }
  }
  
  configChanged(oldValue: Config, newValue: Config) {
    // Validate config object
    if (!newValue || typeof newValue !== 'object') {
      this.config = { width: 300, height: 200 };
      return;
    }
    
    // Ensure minimum values
    if (newValue.width < 100) newValue.width = 100;
    if (newValue.height < 100) newValue.height = 100;
  }
  
  static template = html<AdvancedAttributeExample>`
    <div class="advanced-demo">
      <h2>${x => x.title}</h2>
      <div class="config">
        Size: ${x => x.config.width} x ${x => x.config.height}
      </div>
      <div class="tags">
        Tags: ${x => x.tags.join(', ')}
      </div>
      <div class="state">
        <span ?hidden="${x => !x.loading}">Loading...</span>
        <span ?hidden="${x => !x.expanded}">Expanded content</span>
      </div>
    </div>
  `;
}

// Custom converters
interface Config {
  width: number;
  height: number;
}

const jsonConverter: ValueConverter = {
  toView(value: any): string {
    return JSON.stringify(value);
  },
  
  fromView(value: string): any {
    try {
      return JSON.parse(value);
    } catch {
      return null;
    }
  }
};

const arrayConverter: ValueConverter = {
  toView(value: string[]): string {
    return value.join(',');
  },
  
  fromView(value: string): string[] {
    return value ? value.split(',').map(s => s.trim()).filter(Boolean) : [];
  }
};

Attribute Definition

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

/**
 * An implementation of Accessor that supports reactivity, change callbacks, 
 * attribute reflection, and type conversion for custom elements
 */
class AttributeDefinition implements Accessor {
  /** The class constructor that owns this attribute */
  readonly Owner: Function;
  
  /** The name of the property associated with the attribute */
  readonly name: string;
  
  /** The name of the attribute in HTML */
  readonly attribute: string;
  
  /** The AttributeMode that describes the behavior of this attribute */
  readonly mode: AttributeMode;
  
  /** A ValueConverter that integrates with the property getter/setter */
  readonly converter?: ValueConverter;
  
  /**
   * Creates an instance of AttributeDefinition
   * @param Owner - The class constructor that owns this attribute
   * @param name - The name of the property associated with the attribute
   * @param attribute - The name of the attribute in HTML
   * @param mode - The AttributeMode that describes the behavior
   * @param converter - A ValueConverter for type conversion
   */
  constructor(
    Owner: Function,
    name: string,
    attribute?: string,
    mode?: AttributeMode,
    converter?: ValueConverter
  );
  
  /**
   * 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;
  
  /**
   * Sets the value based on a string from an HTML attribute
   * @param source - The source object
   * @param value - The string value from the attribute
   */
  onAttributeChangedCallback(source: any, value: string): void;
}

/**
 * Metadata used to configure a custom attribute's behavior
 */
interface AttributeConfiguration {
  /** The property name */
  property: string;
  
  /** The attribute name in HTML */
  attribute?: string;
  
  /** The behavior mode */
  mode?: AttributeMode;
  
  /** The value converter */
  converter?: ValueConverter;
}

/**
 * Utilities for managing attribute configurations
 */
const AttributeConfiguration: {
  /**
   * Locates all attribute configurations associated with a type
   */
  locate(target: any): AttributeConfiguration[];
};

Usage Examples:

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

// Manual attribute definition
class ManualAttributeExample {
  private _value: string = "";
  
  static attributes: AttributeDefinition[] = [];
  
  constructor() {
    // Create attribute definition manually
    const valueAttr = new AttributeDefinition(
      ManualAttributeExample,
      "value",
      "data-value",
      "reflect"
    );
    
    ManualAttributeExample.attributes.push(valueAttr);
    
    // Define property with attribute behavior
    Object.defineProperty(this, "value", {
      get: () => valueAttr.getValue(this),
      set: (newValue) => valueAttr.setValue(this, newValue)
    });
  }
  
  get value(): string {
    return this._value;
  }
  
  set value(newValue: string) {
    const oldValue = this._value;
    this._value = newValue;
    
    // Trigger change callback if exists
    if ((this as any).valueChanged) {
      (this as any).valueChanged(oldValue, newValue);
    }
  }
}

// Runtime attribute inspection
class AttributeInspector {
  static inspectElement(elementClass: any): AttributeDefinition[] {
    const configs = AttributeConfiguration.locate(elementClass);
    return configs.map(config => 
      new AttributeDefinition(
        elementClass,
        config.property,
        config.attribute,
        config.mode,
        config.converter
      )
    );
  }
  
  static getAttributeNames(elementClass: any): string[] {
    return this.inspectElement(elementClass)
      .map(attr => attr.attribute);
  }
  
  static getPropertyNames(elementClass: any): string[] {
    return this.inspectElement(elementClass)
      .map(attr => attr.name);
  }
}

// Usage
const myElementAttrs = AttributeInspector.inspectElement(AttributeExample);
console.log("Attributes:", myElementAttrs.map(a => a.attribute));
console.log("Properties:", myElementAttrs.map(a => a.name));

Built-in Value Converters

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

/**
 * Represents objects that can convert values between view and model representations
 */
interface ValueConverter {
  /**
   * Converts a value from model representation to view representation
   * @param value - The value to convert to a view representation
   */
  toView(value: any): any;

  /**
   * Converts a value from view representation to model representation
   * @param value - The value to convert to a model representation
   */
  fromView(value: any): any;
}

/**
 * A ValueConverter that converts to and from boolean values
 * Used automatically when the "boolean" AttributeMode is selected
 */
const booleanConverter: ValueConverter;

/**
 * A ValueConverter that converts to and from boolean values
 * null, undefined, "", and void values are converted to null
 */
const nullableBooleanConverter: ValueConverter;

/**
 * A ValueConverter that converts to and from number values
 * This converter allows for nullable numbers, returning null if the
 * input was null, undefined, or NaN
 */
const nullableNumberConverter: ValueConverter;

Usage Examples:

import { 
  ValueConverter,
  booleanConverter, 
  nullableBooleanConverter, 
  nullableNumberConverter,
  attr,
  FASTElement,
  customElement
} from "@microsoft/fast-element";

@customElement("converter-example")
export class ConverterExample extends FASTElement {
  // Boolean converter (automatic with boolean mode)
  @attr({ mode: "boolean" })
  visible: boolean = false;
  
  // Explicit boolean converter
  @attr({ converter: booleanConverter })
  enabled: boolean = true;
  
  // Nullable boolean converter
  @attr({ converter: nullableBooleanConverter })
  optional: boolean | null = null;
  
  // Nullable number converter
  @attr({ converter: nullableNumberConverter })
  score: number | null = null;
  
  // Custom date converter
  @attr({ converter: dateConverter })
  created: Date = new Date();
  
  // Custom enum converter
  @attr({ converter: priorityConverter })
  priority: Priority = Priority.Medium;
  
  // Custom URL converter
  @attr({ converter: urlConverter })
  homepage: URL | null = null;
}

// Custom converters
const dateConverter: ValueConverter = {
  toView(value: Date): string {
    return value ? value.toISOString() : "";
  },
  
  fromView(value: string): Date {
    return value ? new Date(value) : new Date();
  }
};

enum Priority {
  Low = "low",
  Medium = "medium", 
  High = "high"
}

const priorityConverter: ValueConverter = {
  toView(value: Priority): string {
    return value;
  },
  
  fromView(value: string): Priority {
    return Object.values(Priority).includes(value as Priority)
      ? value as Priority
      : Priority.Medium;
  }
};

const urlConverter: ValueConverter = {
  toView(value: URL | null): string {
    return value ? value.toString() : "";
  },
  
  fromView(value: string): URL | null {
    try {
      return value ? new URL(value) : null;
    } catch {
      return null;
    }
  }
};

// Advanced converter with validation
const emailConverter: ValueConverter = {
  toView(value: string): string {
    return value || "";
  },
  
  fromView(value: string): string {
    // Basic email validation
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(value) ? value : "";
  }
};

const currencyConverter: ValueConverter = {
  toView(value: number): string {
    return value ? value.toFixed(2) : "0.00";
  },
  
  fromView(value: string): number {
    const num = parseFloat(value.replace(/[^0-9.-]+/g, ""));
    return isNaN(num) ? 0 : num;
  }
};

// Converter with options
function createRangeConverter(min: number, max: number): ValueConverter {
  return {
    toView(value: number): string {
      return value.toString();
    },
    
    fromView(value: string): number {
      const num = parseInt(value, 10);
      if (isNaN(num)) return min;
      return Math.max(min, Math.min(max, num));
    }
  };
}

// Usage with range converter
@customElement("range-example")
export class RangeExample extends FASTElement {
  @attr({ converter: createRangeConverter(0, 100) })
  percentage: number = 50;
  
  @attr({ converter: createRangeConverter(1, 5) })
  rating: number = 3;
  
  static template = html<RangeExample>`
    <div>
      <p>Percentage: ${x => x.percentage}%</p>
      <p>Rating: ${x => x.rating}/5 stars</p>
    </div>
  `;
}

Attribute Modes

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

/**
 * The mode that specifies the runtime behavior of the attribute
 */
type AttributeMode = "reflect" | "boolean" | "fromView";

/**
 * Attribute mode behaviors:
 * 
 * "reflect" - Two-way synchronization (default)
 * - Property changes reflect to DOM attribute
 * - DOM attribute changes update property
 * 
 * "boolean" - Boolean attribute behavior
 * - Presence of attribute = true
 * - Absence of attribute = false
 * - Property changes add/remove attribute
 * 
 * "fromView" - One-way from DOM to property
 * - DOM attribute changes update property
 * - Property changes do NOT reflect to DOM
 */

Usage Examples:

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

@customElement("mode-example")
export class AttributeModeExample extends FASTElement {
  // Reflect mode (default) - two-way sync
  @attr({ mode: "reflect" })
  title: string = "Default Title";
  
  // Boolean mode - presence/absence behavior
  @attr({ mode: "boolean" })
  disabled: boolean = false;
  
  @attr({ mode: "boolean" })
  hidden: boolean = false;
  
  @attr({ mode: "boolean" })
  readonly: boolean = false;
  
  // FromView mode - one-way from attribute to property
  @attr({ mode: "fromView" })
  dataId: string = "";
  
  @attr({ mode: "fromView", attribute: "aria-label" })
  ariaLabel: string = "";
  
  // Demonstrate mode behaviors
  testReflectMode() {
    // This will update both property and attribute
    this.title = "New Title";
    console.log("Title attribute:", this.getAttribute("title"));
  }
  
  testBooleanMode() {
    // This will add/remove the 'disabled' attribute
    this.disabled = !this.disabled;
    console.log("Has disabled attribute:", this.hasAttribute("disabled"));
  }
  
  testFromViewMode() {
    // This will NOT update the DOM attribute
    this.dataId = "new-id";
    console.log("data-id attribute:", this.getAttribute("data-id"));
    
    // But setting the attribute will update the property
    this.setAttribute("data-id", "from-dom");
    console.log("dataId property:", this.dataId);
  }
  
  static template = html<AttributeModeExample>`
    <div class="mode-demo">
      <h3>${x => x.title}</h3>
      
      <div class="controls">
        <button @click="${x => x.testReflectMode()}">
          Test Reflect Mode
        </button>
        
        <button @click="${x => x.testBooleanMode()}">
          Test Boolean Mode (Disabled: ${x => x.disabled})
        </button>
        
        <button @click="${x => x.testFromViewMode()}">
          Test FromView Mode
        </button>
      </div>
      
      <div class="state">
        <p>Title: "${x => x.title}"</p>
        <p>Disabled: ${x => x.disabled}</p>
        <p>Data ID: "${x => x.dataId}"</p>
        <p>ARIA Label: "${x => x.ariaLabel}"</p>
      </div>
    </div>
  `;
}

// Practical mode usage patterns
@customElement("practical-modes")
export class PracticalModesExample extends FASTElement {
  // Reflect mode for user-configurable properties
  @attr({ mode: "reflect" })
  variant: "primary" | "secondary" = "primary";
  
  @attr({ mode: "reflect" })
  size: "small" | "medium" | "large" = "medium";
  
  // Boolean mode for states and flags
  @attr({ mode: "boolean" })
  loading: boolean = false;
  
  @attr({ mode: "boolean" })
  selected: boolean = false;
  
  @attr({ mode: "boolean" })
  expanded: boolean = false;
  
  // FromView mode for read-only external data
  @attr({ mode: "fromView", attribute: "data-testid" })
  testId: string = "";
  
  @attr({ mode: "fromView", attribute: "role" })
  role: string = "";
  
  @attr({ mode: "fromView", attribute: "tabindex" })
  tabIndex: string = "0";
  
  // Change callbacks for different modes
  variantChanged(oldValue: string, newValue: string) {
    console.log(`Variant reflect: ${oldValue} → ${newValue}`);
    this.classList.remove(`variant-${oldValue}`);
    this.classList.add(`variant-${newValue}`);
  }
  
  loadingChanged(oldValue: boolean, newValue: boolean) {
    console.log(`Loading boolean: ${oldValue} → ${newValue}`);
    this.classList.toggle('loading', newValue);
  }
  
  testIdChanged(oldValue: string, newValue: string) {
    console.log(`Test ID fromView: ${oldValue} → ${newValue}`);
    // Only reacts to external attribute changes
  }
  
  static template = html<PracticalModesExample>`
    <div class="practical-modes ${x => x.variant} ${x => x.size}">
      <div class="content" ?hidden="${x => x.loading}">
        Content (variant: ${x => x.variant}, size: ${x => x.size})
      </div>
      
      <div class="loading-spinner" ?hidden="${x => !x.loading}">
        Loading...
      </div>
      
      <div class="metadata">
        Test ID: ${x => x.testId}
        Role: ${x => x.role}
        Tab Index: ${x => x.tabIndex}
      </div>
    </div>
  `;
}

Types

/**
 * Property accessor interface for reactive properties
 */
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;
}

/**
 * Complete attribute configuration interface
 */
interface AttributeConfiguration {
  /** The property name */
  property: string;
  
  /** The attribute name in HTML */
  attribute?: string;
  
  /** The behavior mode */
  mode?: AttributeMode;
  
  /** The value converter */
  converter?: ValueConverter;
}

/**
 * Attribute configuration for decorators (excludes property)
 */
type DecoratorAttributeConfiguration = Omit<AttributeConfiguration, "property">;

/**
 * Available attribute behavior modes
 */
type AttributeMode = 
  | "reflect"    // Two-way synchronization (default)
  | "boolean"    // Boolean attribute behavior
  | "fromView";  // One-way from attribute to property

docs

attributes.md

context-system.md

css-styling.md

data-binding.md

dependency-injection.md

html-templates.md

index.md

observable-system.md

ssr-hydration.md

state-management.md

template-directives.md

testing-utilities.md

utilities.md

web-components.md

tile.json