@qery/reactive-component
v0.8.0
Published
A lightweight, powerful Web Component library for building reactive user interfaces with minimal boilerplate and maximum performance.
Maintainers
Readme
Reactive Component
Reactive Component is a lightweight library for building modern web components with native standards. It extends the Custom Elements API by adding a signal-based reactive system for declarative DOM manipulation and precise state management. Using simple directives like $state and $bind-*, you can easily bind the component state to the DOM, keeping your UI in sync with changes.
This HTML-first approach lets you work directly with your existing markup—there is no need for virtual DOM diffing, heavy templating, or build steps. Computed properties and automatic dependency tracking handle updates, so you can focus on your app’s logic.
In summary, Reactive Component offers reactive state management and declarative data binding in a simple, standards-compliant way—without the complexity of larger frameworks.
Important — Authoritative Prompt: See prompt.txt for the HTML-first workflow, strict binding validation, security guidance, and AI usage rules.
Features
- Reactive: Automatically update the DOM when the state changes.
- Computed: Instantly refresh derived values.
- Declarative Binding: Keep UI and data in sync with minimal code.
- Zero Build: Use plain HTML and Custom Elements, no bundlers needed.
- Progressive Enhancement: Boost SEO, speed up initial loads, and simplify maintenance.
- High Performance: Direct DOM updates in a ~4.8KB gzipped package.
- TypeScript: Benefit from type safety and smarter tooling.
- Framework Agnostic: Easily integrate with other libraries or legacy systems.
- Context API: Share state between components in a clean, React-like way.
Credits
Hawk Ticehurst's work on Stellar and his article 'Declarative Signals' inspired this library.
Ginger for the article 'Functional custom elements the easy way', which inspired the
definefunctionality.
Installation
# Using pnpm (recommended)
pnpm install @qery/reactive-componentOr
# Using npm
npm install @qery/reactive-componentQuick Start
Let's start with a simple counter component that demonstrates several core reactive features:
Key Features Demonstrated:
Function-based Component Definition
- Use
define()to create components with a concise API - All state and behavior managed through the context object
- Clean, minimal syntax for small to medium components
- Use
Automatic State Management
- The
$stateproxy automatically tracks and manages state - Changes trigger efficient DOM updates
- Property-based API for reading and writing state
- The
Method Binding
$bindmaps methods for use in HTML event handlers- Methods are automatically bound to the component instance
- Clean event handler syntax
State Initialization + Reactive Display
$stateinitializes state keys from the element's initial text content- After initialization it is one-way: state updates the DOM
- User edits do not propagate back (not two-way)
Here's the complete example:
define("basic-counter", ({ $state, $on }) => {
// Initialize state
$state.count = 0;
// Bind methods for $on* event handlers
$on.increment = () => {
$state.count++;
};
$on.decrement = () => {
$state.count--;
};
});<basic-counter>
<!-- $state initializes `count` from HTML (0) then one-way reflects state updates -->
<p>Count: <span $state="count">0</span></p>
<!-- $onclick attribute references bound methods by name -->
<button $onclick="decrement" class="bg-blue-500 text-white px-4 py-2 rounded">Decrement</button>
<button $onclick="increment" class="bg-blue-500 text-white px-4 py-2 rounded">Increment</button>
</basic-counter>Implementation Details:
Component Definition
define()registers the custom element- Context object provides
$state,$bind,$compute,$effect,$ref - Definition runs once per element instance
State Management
$state.countprovides direct property access- Changes are automatically tracked and trigger DOM updates
- Binding attribute values must be alphanumeric
Template Structure
$state="count"initializes the counter (0) then one-way reflects stateonclickhandlers reference method names from$bind- Tailwind classes provide styling without extra CSS
Core Concepts
- State Management - Reactive state that automatically updates the UI
- Computed Properties - Derived values that update when dependencies change
- Context API - Share state between components without prop drilling
State Management
The component uses a sophisticated signal-based reactive system for efficient state management that provides several powerful features:
Declarative State Initialization
- States are initialized using
setState()with automatic type inference - Values can be primitives, objects, or complex data structures
- State changes trigger efficient, granular re-renders
- States are initialized using
Computed Properties with Auto-Tracking
- Derived values update automatically when dependencies change
- Smart caching prevents unnecessary recalculations
- Dependencies are tracked without explicit declarations
DOM Binding Modes
$state: initialization + one-way (state → DOM)$bind-text,$bind-class,$bind-attr: one-way (state → DOM)$bind-value,$bind-checked: two-way (form control ↔ state)
Here's a practical example showing these features in action:
define("input-echo", ({ $state, $compute }) => {
// Initialize reactive state with empty string
$state.text = "";
// Create a computed property that transforms text to uppercase
// Updates automatically when text changes
$compute("uppercase", ["text"], (c) => String(c).toUpperCase());
});<input-echo>
<!-- Two-way binding syncs input value with text state -->
<input type="text" $bind-value="text" class="border p-2 w-full mb-2" placeholder="Type something..." />
<!-- Displays computed uppercase value -->
<!-- Updates automatically when text changes -->
<p>You typed: <span $bind-text="uppercase" /></p>
</input-echo>Key Features Demonstrated:
- Automatic state synchronization between input and component
- Computed properties that transform state (
uppercase) - Efficient updates that only re-render affected parts
- Clean, declarative syntax for complex reactivity
Computed Properties
Computed properties are a powerful feature that enables you to create derived state values that automatically update based on changes to their dependencies. This reactive computation system provides several key benefits:
Note: Calling this.compute() more than once with the same property name will log a warning in the console, as it indicates a potential logic error.
Automatic Dependency Tracking
- The system intelligently tracks dependencies between state values
- Only recomputes when dependent values change
- Eliminates unnecessary calculations and improves performance
Smart Caching
- Computed values are cached until dependencies change
- Prevents recalculating the same value multiple times
- Optimizes memory usage and computation time
Declarative Data Flow
- Define transformations as pure functions
- Dependencies are automatically managed
- Results update seamlessly when source data changes
Here's a practical example of computed properties in action with a temperature converter:
define("temperature-converter", ({ $state, $compute }) => {
// Initialize base temperature in Celsius
$state.celsius = 20;
// Compute Fahrenheit from Celsius
// Updates automatically when celsius changes
$compute("fahrenheit", ["celsius"], (c) => {
// Standard C to F conversion formula
return ((c as number) * 9) / 5 + 32;
});
});<temperature-converter>
<div class="space-y-2">
<!-- Base temperature initialized via $state (one-way thereafter) -->
<p>Celsius: <span $state="celsius">20</span>°C</p>
<!-- Computed temperatures update automatically -->
<p>Fahrenheit: <span $bind-text="fahrenheit" />°F</p>
<!-- Interactive slider updates celsius state (two-way) -->
<input type="range" min="0" max="40" $bind-value="celsius" class="w-full" />
</div>
</temperature-converter>Key Features Demonstrated:
- Automatic updates when source data changes
- Multiple computed properties from a single source
- Built-in caching and performance optimization
- Clean separation of computation logic
- Declarative template bindings
Advanced Features
Element References
Element references provide direct, type-safe access to DOM elements in your components. This feature enables efficient manipulation of DOM elements while maintaining reactivity and encapsulation.
Direct Element Access
- Direct access to DOM elements through the
$reffunction - Refs are populated at runtime after the component connects to the DOM
- Always check for a ref's existence before using it
- Direct access to DOM elements through the
Ref Registration
- Elements marked with
$refattribute are automatically registered - References are available immediately after component mounting
- Clean separation between template and logic
- Dynamic Refs: Refs on elements dynamically added after initial connection are recognized and available via
$ref. However, it's recommended to define refs in your initial HTML for better performance and predictability.
- Elements marked with
Reactive Integration
- Refs work seamlessly with reactive state
- Changes through refs trigger appropriate updates
- Maintains the component's reactive nature
Here's a practical example demonstrating element references:
define("ref-demo", ({ $state, $on, $ref }) => {
// Initialize state
$state.dimensions = "Not measured yet";
// Use Case 1: Focus Management
$on.focusUsername = () => {
const usernameInput = $ref.usernameInput as HTMLInputElement;
usernameInput?.focus();
};
// Use Case 2: DOM Measurements
$on.measureElement = () => {
const measureBox = $ref.measureBox as HTMLDivElement;
const rect = measureBox?.getBoundingClientRect();
if (rect) {
$state.dimensions = `${Math.round(rect.width)}px × ${Math.round(rect.height)}px`;
}
};
});<ref-demo>
<div class="space-y-4">
<p>Click buttons to interact with referenced elements:</p>
<!-- Element referenced using $ref attribute -->
<input $ref="usernameInput" type="text" placeholder="Enter username" class="border p-2" />
<div $ref="measureBox" class="p-4 border rounded bg-gray-100">
<p>This box can be measured</p>
</div>
<p>Dimensions: <span $bind-text="dimensions"></span></p>
<div class="flex justify-center space-x-4">
<!-- Event handlers trigger ref-based operations -->
<button $onclick="focusUsername" class="bg-purple-500 text-white px-4 py-2 rounded">Focus Input</button>
<button $onclick="measureElement" class="bg-pink-500 text-white px-4 py-2 rounded">Measure Box</button>
</div>
</div>
</ref-demo>Key Features Demonstrated:
- Simple element referencing with
$refattribute - Direct access to DOM methods and properties
- Integration with component state management
- Clean separation of concerns between template and logic
- Type-safe element manipulation
- Automatic reference management
- Event handling with referenced elements
JSON State Management
The JSON State Management feature provides sophisticated handling of complex data structures with automatic serialization and reactive updates. Here's a detailed breakdown of its capabilities:
Automated JSON Serialization
- Automatic conversion between JS objects and JSON
- Pretty-printing with proper indentation
- Type-safe serialization of complex nested structures
Deep Reactivity
- Changes to nested properties trigger updates
- Computed properties track deep dependencies
- Metadata automatically updates on state changes
Form Integration
- Two-way binding with form inputs
- Real-time validation and feedback
- Automatic type coercion for form fields
Here's a practical example demonstrating JSON state management:
define("json-state-management", ({ $state, $compute }) => {
// Initialize form field states
$state.name = "Paco Doe";
$state.age = 30;
$state.bio = "";
// Create computed JSON representation
// Updates automatically when any dependency changes
$compute("json", ["name", "age", "bio"], (name, age, bio) =>
JSON.stringify(
{
name,
age,
bio,
},
null,
2,
),
);
});<json-state-management>
<div class="space-y-4">
<div>
<!-- Two-way binding for name field -->
<label for="name" class="block mb-1">Name:</label>
<input id="name" type="text" $bind-value="name" class="border p-2 w-full" />
</div>
<div>
<!-- Numeric input with automatic type coercion -->
<label for="age" class="block mb-1">Age:</label>
<input id="age" type="number" $bind-value="age" class="border p-2 w-full" />
</div>
<div>
<!-- Multi-line text input with two-way binding -->
<label for="bio" class="block mb-1">Bio:</label>
<textarea id="bio" $bind-value="bio" class="border p-2 w-full h-24"></textarea>
</div>
<div>
<!-- Live JSON preview with automatic updates -->
<p>JSON Output:</p>
<pre class="bg-gray-100 p-2 rounded mt-1">
<code $bind-text="json"></code>
</pre>
</div>
</div>
</json-state-management>Key Features Demonstrated:
- Deep reactive state management with nested objects
- Automatic JSON serialization and formatting
- Real-time form input synchronization
- Computed metadata updates
- Type-safe state handling
- Clean separation of data and presentation
- Automatic dependency tracking
Context API
Context provides a way to share values between components without having to explicitly pass a prop through every level of the component tree. This feature is particularly useful for sharing global state such as themes, user data, or application configuration.
Creating Context
- Define shared state using the
createContextfunction - Provide a state key for storage and an optional debug name
- Context is identified by a unique symbol to prevent collisions
- Define shared state using the
Exposing Context
- Provider components expose context using
exposeContext(context) - State changes in provider components automatically update consumers
- Multiple contexts can be exposed from a single component
- Provider components expose context using
Consuming Context
- Child components consume context using
consumeContext(context) - Consumed context is automatically synchronized with provider updates
- Components can consume multiple contexts from different providers
- Child components consume context using
Here's an example of a theme context system:
// Define context and provider component
const themeContext = createContext("theme");
class ThemeProvider extends ReactiveComponent {
constructor() {
super();
// Initialize state
this.setState("theme", {
mode: "light",
background: "bg-slate-200",
text: "text-slate-900",
});
// Expose the theme context
this.exposeContext(themeContext);
}
toggleTheme() {
const currentTheme = this.getState("theme");
// Toggle between light and dark mode
this.setState(
"theme",
currentTheme.mode === "light"
? { mode: "dark", background: "bg-slate-900", text: "text-slate-50" }
: { mode: "light", background: "bg-slate-200", text: "text-slate-900" },
);
}
}
customElements.define("theme-provider", ThemeProvider);
// Consumer component
class ThemeConsumer extends ReactiveComponent {
constructor() {
super();
// Consume the theme context
this.consumeContext(themeContext);
// Create computed properties based on the theme
this.compute("themeMode", [themeContext.state], (theme) => `ThemeMode: ${theme.mode}`);
}
connectedCallback() {
super.connectedCallback();
// React to theme changes
this.effect(() => {
const theme = this.getState("theme");
this.classList.add(theme.background, theme.text);
this.refs.themeInfo.textContent = `Current Theme: ${theme.mode}`;
});
}
}
customElements.define("theme-consumer", ThemeConsumer);<theme-provider>
<button type="button" $onclick="toggleTheme">Toggle Theme</button>
<theme-consumer>
<p $bind-text="themeMode"></p>
<p $ref="themeInfo"></p>
</theme-consumer>
</theme-provider>Key Features Demonstrated:
- Clean provider/consumer pattern for shared state
- Automatic propagation of state changes
- Type-safe context consumption
- Computed properties based on context values
- Nested component communication without prop drilling
- Reactive UI updates when context changes
Form Handling
Form handling in ReactiveComponent provides sophisticated validation, state management, and real-time feedback capabilities. Here's a detailed breakdown of its features:
Reactive Form State Management
- Automatic two-way data binding for form inputs
- Real-time validation and error handling
- Dynamic enable/disable functionality
- Status tracking and feedback
Smart Validation System
- Built-in validation rules and custom validators
- Real-time validation feedback
- Computed validation states
- Error message management
Accessibility Integration
- Keyboard navigation support
- Screen reader-friendly status messages
- Focus management
Here's a practical example demonstrating form-handling capabilities:
class FormDemo extends ReactiveComponent {
constructor() {
super();
// Initialize form state with defaults
this.setState("isEnabled", false);
this.setState("inputText", "");
// Compute disabled state from isEnabled
// Updates automatically when enabled state changes
this.compute("isDisabled", ["isEnabled"], (enabled) => !enabled);
// Compute status message with validation
// Re-computes when either input text or enabled state changes
this.compute("status", ["isEnabled", "inputText"], (enabled, text) => {
if (!enabled) return "Input disabled";
if (text.length < 3) return "Input too short (min 3 characters)";
return `Input active: ${text.length} characters`;
});
// Track validation state
// Updates when is enabled and the input text changes
this.compute("isStatusValid", ["isEnabled", "inputText"], (enabled, text) => {
return { [text.length >= 3 || !enabled ? "remove" : "add"]: "text-red-500" };
});
}
}
customElements.define("form-demo", FormDemo);<form-demo>
<div class="space-y-4">
<!-- Toggle input enabled/disabled state -->
<div>
<input type="checkbox" id="enabled" $bind-checked="isEnabled" class="mr-2" />
<label for="enabled">Enable input</label>
</div>
<!-- Input field with validation -->
<div>
<input
type="text"
$bind-value="inputText"
$bind-disabled="isDisabled"
class="border p-2 w-full disabled:opacity-50 disabled:cursor-not-allowed"
placeholder="Type when enabled..."
aria-describedby="status"
/>
</div>
<!-- Status message with validation styling -->
<p id="status" role="status">Status: <span $bind-text="status" $bind-class="className"> </span></p>
</div>
</form-demo>Key Features Demonstrated:
- Two-way data binding for form inputs
- Real-time validation with computed properties
- Dynamic enable/disable functionality
- Status messages with validation styling
- Clean separation of form logic and presentation
- Reactive updates without explicit event handling
Attribute Binding ($bind-attr)
The $bind-attr binding manages multiple element attributes dynamically from a single state object.
State Shape:
this.setState("buttonAttrs", {
"data-id": "user-123", // Sets attribute
"aria-label": "Submit Form",
hidden: false, // Removes 'hidden' attribute
disabled: true, // Adds 'disabled' attribute
title: null, // Removes 'title' attribute
});HTML:
<button $bind-attr="buttonAttrs">Submit</button>Rules:
- A truthy value sets the attribute.
trueresults in an empty attribute (e.g.,disabled). - A falsy value (
false,null,undefined,"") removes the attribute.
Class Binding ($bind-class)
$bind-class operates on a state object that specifies add, remove, toggle, or replace operations. It does not work with simple strings or booleans.
State Shape:
this.setState("panelClasses", {
add: ["panel", "active"], // Add one or more classes
remove: "loading", // Remove a class
toggle: "highlighted", // Toggle a class
replace: ["old-style", "new-style"], // Replace a class
});HTML:
<div $bind-class="panelClasses">...</div>Note: Operations are cumulative. Classes added in a previous state update will persist unless explicitly removed.
Custom Binding Handlers
Custom binding handlers allow you to extend the component's binding capabilities with your own custom logic. This powerful feature enables specialized DOM updates based on state changes.
Handler Definition
- Register custom handlers using
$customBindingHandlers - Assign handler functions for each binding type
- Access element and value details for fine-grained control
- Register custom handlers using
Handler Execution
- Automatically called when bound state changes
- Receives element and raw state values
- Full access to elements for direct DOM manipulation
Integration with State System
- Works seamlessly with reactive state management
- Handlers are reactive and update automatically
- Clean integration with the existing binding system
Here's a practical example of custom binding handlers:
define("custom-binding-demo", ({ $state, $on, $customBindingHandlers }) => {
// Initialize state for various custom bindings
$state.counter = 0;
$state.theme = "light";
$state.status = "idle";
// Custom handler for counter animation
$customBindingHandlers["animate-count"] = ({ element, rawValue }) => {
if (element && typeof rawValue === "number") {
element.style.transform = `scale(${1 + rawValue * 0.1})`;
element.textContent = String(rawValue);
}
};
// Custom handler for theme switching
$customBindingHandlers["theme-switch"] = ({ element, rawValue }) => {
if (element && rawValue) {
const theme = String(rawValue);
element.classList.remove("theme-light", "theme-dark");
element.classList.add(`theme-${theme}`);
element.setAttribute("aria-theme", theme);
}
};
// Custom handler for status indicators
$customBindingHandlers["status-indicator"] = ({ element, rawValue }) => {
if (element && rawValue) {
const status = String(rawValue);
element.setAttribute("data-status", status);
element.setAttribute("aria-busy", status === "loading" ? "true" : "false");
element.classList.toggle("pulse", status === "active");
}
};
// Bind methods to update state
$on.increment = () => {
$state.counter = ($state.counter as number) + 1;
};
$on.toggleTheme = () => {
$state.theme = $state.theme === "light" ? "dark" : "light";
};
$on.updateStatus = (e: Event) => {
const target = e.currentTarget as HTMLButtonElement;
$state.status = target.textContent?.toLowerCase() || "idle";
};
});<custom-binding-demo>
<div class="space-y-4">
<!-- Animated counter binding -->
<div>
<span $bind-animate-count="counter" class="text-2xl font-bold transition-transform"> 0 </span>
<button $onclick="increment" class="ml-2 px-4 py-2 bg-blue-500 text-white rounded">Increment</button>
</div>
<!-- Theme switching binding -->
<div $bind-theme-switch="theme" class="p-4 border rounded transition-colors">
<h3>Theme Demo</h3>
<button $onclick="toggleTheme" class="px-4 py-2 bg-gray-200 rounded">Toggle Theme</button>
</div>
<!-- Status indicator binding -->
<div $bind-status-indicator="status" class="p-2 border rounded">
<p>Current Status: <span $bind-text="status"></span></p>
<div class="flex space-x-2 mt-2">
<button $onclick="updateStatus" class="px-3 py-1 bg-gray-500 text-white rounded">Idle</button>
<button $onclick="updateStatus" class="px-3 py-1 bg-green-500 text-white rounded">Active</button>
<button $onclick="updateStatus" class="px-3 py-1 bg-yellow-500 text-white rounded">Loading</button>
</div>
</div>
</div>
</custom-binding-demo>Key Features Demonstrated:
- Custom animation binding with state-based scaling
- Theme switching with dynamic class management
- Multiple custom bindings in a single component
- Clean integration with an existing state system
- Reactive updates without manual event handling
- Accessibility considerations in custom bindings
Function-based Components with define()
In addition to class-based components, Reactive Component supports concise function-based components via define(name, definition). This style keeps logic close to the HTML and offers a minimal API through a context object.
- HTML-first remains the rule: structure and user-visible text live in HTML.
- All state and behavior are managed through the
define()context. - Ideal for small/mid components and rapid prototyping.
Basic Usage
define("rc-counter", function Counter({ $state, $on, $effect, $compute, $ref }) {
// Initialize state (property-only API)
$state.count = 0;
// Bind methods for $on* event handlers (auto-bound to the element)
$on.increment = () => {
$state.count = ($state.count as number) + 1;
};
// Computed values
$compute("doubleCount", ["count"], (count) => (count as number) * 2);
// Effects
$effect(() => {
console.log("Count changed:", $state.count);
});
// Refs
$on.focusInput = () => {
const input = $ref.countInput;
input?.focus();
};
// Optional lifecycle hooks
return {
connected: () => console.log("Counter connected!"),
};
});<rc-counter>
<p>Count: <span $state="count">0</span></p>
<p>Double: <span $bind-text="doubleCount"></span></p>
<input $ref="countInput" type="number" $bind-value="count" />
<button $onclick="increment">Increment</button>
<button $onclick="focusInput">Focus Input</button>
</rc-counter>Notes:
- Event handlers reference bound methods by name via
$onclick="methodName". - Use
$state.someKeyto read/write state. The binding attributes must use alphanumeric keys.
Passing Data to $on* Handlers
Inline expressions are not allowed in $-prefixed attributes. Use data-* attributes and read values in your handler:
HTML:
<button $onclick="setStatus" data-status="idle">Idle</button>
<button $onclick="setStatus" data-status="active">Active</button>TypeScript:
define("status-toggle", ({ $state, $on }) => {
$state.status = "idle";
$on.setStatus = (e: Event) => {
const el = e.currentTarget as HTMLElement;
const status = el?.dataset.status ?? "idle";
$state.status = status;
};
});Context API
Inside the definition function you receive a single context object:
$element: The component instance. Extends ReactiveComponent with public wrappers:setState(key, value),getState(key),compute(key, deps, fn),effect(fn),refs
$state: Property-only state API exposed via Proxy- Read:
const v = $state.key - Write:
$state.key = next - Tracks keys that are actually used in bindings and computed values
- Read:
$compute(key, sources, computation): Define a derived state value$effect(callback): Register an effect; returns a cleanup function$ref: Property-only API for accessing elements registered via$refattributes- Access:
const el = $ref.refName
- Access:
$on: Bind methods for$on*event attributes (alias of$bind)- Assign:
$on.methodName = (...args) => { /* this === element */ } - Use in HTML:
$onclick="methodName" - Prefer
$onfor event handlers to match the$on*convention
- Assign:
$bind: Bind functions onto the component instance- Assign:
$bind.methodName = (...args) => { /* this === element */ } - Use in HTML:
$bind-class="methodName" - Prefer
$bindfor binding functions to match the$bind-*convention
- Assign:
$customBindingHandlers: Define custom binding handlers for extending the binding system- Assign:
$customBindingHandlers["handler-name"] = ({ element, rawValue }) => { /* handler logic */ } - Use in HTML:
$bind-handler-name="stateKey"
- Assign:
All methods are safe to call during definition execution.
Lifecycle
You can return lifecycle hooks from your definition:
define("rc-lifecycle", function WithLifecycle({ $state }) {
$state.label = "Hello";
return {
connected: () => console.log("connected"),
disconnected: () => console.log("disconnected"),
adopted: () => console.log("adopted"),
attributeChanged: (name, oldValue, newValue) => {
if (name === "label" && newValue != null) $state.label = newValue;
},
};
});<rc-lifecycle label="Welcome">
<span $bind-text="label"></span>
</rc-lifecycle>Details:
return { connected, disconnected, adopted, attributeChanged }is optional.attributeChanged(name, oldValue, newValue)fires when an observed attribute changes.
Custom Binding Handlers
You can extend the binding system with custom handlers using $customBindingHandlers:
define("tab-component", function TabComponent({ $state, $customBindingHandlers, $on }) {
$state.activeTab = "tab1";
// Define custom binding handler for tab triggers
$customBindingHandlers["tab-trigger"] = ({ element, rawValue }) => {
if (!(element instanceof HTMLElement)) return;
const name = element.dataset.name;
if (!name) return;
const isActive = rawValue === name;
element.setAttribute("aria-selected", isActive ? "true" : "false");
element.tabIndex = isActive ? 0 : -1;
};
$on.selectTab = (e: Event) => {
const target = e.currentTarget as HTMLElement;
$state.activeTab = target.dataset.name || "tab1";
};
});<tab-component>
<div role="tablist">
<button role="tab" data-name="tab1" $bind-tab-trigger="activeTab" $onclick="selectTab">Tab 1</button>
<button role="tab" data-name="tab2" $bind-tab-trigger="activeTab" $onclick="selectTab">Tab 2</button>
<button role="tab" data-name="tab3" $bind-tab-trigger="activeTab" $onclick="selectTab">Tab 3</button>
</div>
<div role="tabpanel">
<p>Active Tab: <span $bind-text="activeTab"></span></p>
</div>
</tab-component>Interop and When to Use define()
- Works alongside class-based components; both use the same reactive engine.
- Choose
define()for:- Small to medium components
- Components without inheritance needs
- Co-locating simple setup logic with HTML
- Quick prototyping with custom bindings
- Choose class-based for:
- Advanced inheritance / mixins
- Complex lifecycles or custom element internals
- Components requiring extensive private methods
- Global availability: when running in a browser,
window.defineis set for script-based usage.
Value Coercion
When a component initializes, text content from elements with $state is automatically coerced into a JavaScript type. The runtime applies these deterministic rules (evaluated in the order below):
| Input from HTML | Coerced Type | Notes / Example |
| ----------------------------------------------- | ------------------ | ------------------------------------------------- |
| "true" | boolean | true |
| "false" | boolean | false |
| "42", "-10.5" | number | 42, -10.5 |
| "null" | null | null |
| "undefined" | string | the literal string "undefined" - see note below |
| Strings starting with { or [ and valid JSON | object / array | parsed via JSON.parse |
| Other | string | preserved as-is |
Important details and guidance:
The implementation treats the token
"undefined"as the literal string"undefined"(it does not coerce it to the JavaScriptundefinedvalue). Avoid using"undefined"as a sentinel in markup; omit the attribute or usenull/ an explicit value instead.JSON parsing is attempted only when the string begins with
{or[and valid JSON is present. Invalid JSON will remain a string.Note: Coercion is automatically applied in the following scenarios:
- When extracting initial values from
$stateelements in HTML - On every
setState(key, value)call - On every computed property result from
compute()
This means
setState("count", "42")will store the number42, not the string.- When extracting initial values from
These coercion rules are deterministic and intentionally conservative to avoid surprising application behavior (for example,
"0"becomes the number0, but"undefined"remains a string).
AIDEV-NOTE: Make UI/markup explicit instead of relying on "undefined"; prefer omitting attributes or using null / explicit JSON for complex values.
Binding Validation Rules
To ensure predictable behavior, security, and fast parsing, the runtime enforces strict validation on $-prefixed bindings. Bindings must be simple identifiers (no expressions, interpolation, or punctuation). Violations are considered errors and will be logged; the runtime will keep the last valid state and fall back to safe defaults.
| Aspect / Target | Attribute Pattern | Allowed Pattern (Regex) | Notes |
| --------------------------------------------- | ------------------------------------------------- | ----------------------- | -------------------------------------------------------------------------------- |
| Binding type (e.g. bind-<type>) | $bind-<type>="key" | /^[a-zA-Z0-9-]+$/ | Hyphens allowed in the binding type (e.g. $bind-animate-count, $bind-custom) |
| Binding value (state key / ref / $state name) | $state="key", $ref="name", $bind-text="key" | /^[a-zA-Z0-9]+$/ | Strict alphanumeric state key; no dots, brackets, or operators |
| Ref name | $ref="name" | /^[a-zA-Z0-9]+$/ | Alphanumeric only |
Validation guidance:
- Binding attribute values must be a single alphanumeric token matching
^[a-zA-Z0-9]+$. Do not embed expressions likeuser.name,items[0], or templates like${value}. - Binding types (the
<type>part of$bind-<type>) may include hyphens and therefore use the looser pattern/^[a-zA-Z0-9-]+$/. - On invalid bindings the runtime will:
- Log a descriptive validation error with the element, attribute, and offending value.
- Suggest a corrected form (e.g., replace dots with a single key or rename the key).
- Preserve the component's last-known-good state and fall back to a safe default display.
Form Input Gotchas
Checkboxes
- Use
$bind-checkedfor boolean state on checkboxes. - Avoid
$bind-valuewith checkboxes — the library will warn and may not behave as expected.
Radio Buttons
- Use
$bind-valueto bind to thevalueof the selected radio in a group. - Avoid
$bind-checkedfor radio groups (will trigger a warning). - Requirement: all radios in a group must share the same
nameattribute. - Note: When the state bound to a radio group changes, radio inputs with the same
nameare updated automatically to reflect the new selection.
NOTE: If your binding needs fall outside these patterns (complex paths, computed expressions), expose a simple computed state key (with $compute) instead of embedding expressions in markup.
Security Best Practices
HTML and Custom Bindings
The library prioritizes performance and flexibility, which means it does not include built-in sanitization for HTML content. You are responsible for sanitizing any untrusted data.
$bind-html
Never use $bind-html with user-provided content without sanitizing it first.
Unsafe:
this.userComment = "<img src=x onerror=alert('XSS')>"; // Malicious input<!-- This is vulnerable to XSS -->
<div $bind-html="userComment"></div>Safe (with a library like DOMPurify):
import DOMPurify from "dompurify";
this.userComment = DOMPurify.sanitize("<img src=x onerror=alert('XSS')>");<!-- Sanitized and safe -->
<div $bind-html="userComment"></div>Custom Binding Handlers
Be equally cautious in customBindingHandlers. Manipulating the DOM with innerHTML, or setting href attributes from untrusted state can also lead to XSS. Always validate and sanitize data within your custom handlers.
API Reference
Function-based define API
define(name: string, definition: Definition): typeof HTMLElement- Registers a custom element using the function-based API.
- Returns the custom element constructor.
Definition:
interface Definition- Call signature:
(this: Element, context: Context) => unknown - Can return
Partial<LifecycleMethods>:{ connected?, disconnected?, adopted?, attributeChanged?(name, oldValue, newValue) }
- Call signature:
Context:
$element: Element— The element instance with public wrappers:setState(key, value),getState(key),compute(key, deps, fn),effect(fn),refs
$state: Record<string, any>— Property-only state API (Proxy)$compute(key: string, sources: string[], computation: (...args) => StateValue): void$effect(callback: () => void): () => void$ref: Record<string, HTMLElement | undefined>— Property-only API for accessing registered refs$bind: Record<string, ((...args: unknown[]) => unknown) | undefined>- Assign functions to add methods onto the element instance
Notes:
- Binding constraints apply: binding attribute values must be alphanumeric (no expressions).
- All user-visible text should live in HTML templates.
Component Lifecycle
connectedCallback(): Called when the component is added to the DOMdisconnectedCallback(): Called when the component is removed from the DOMattributeChangedCallback(name, oldValue, newValue): Called when an observed attribute changes
State Methods
setState(key: string, value: unknown): Initialize or update state with automatic type coerciongetState(key: string): Retrieve current state value with type safetycompute(key: string, dependencies: string[], computation: Function): Create computed property with dependency trackingeffect(callback: Function): Create a side effect that runs when dependencies change. The callback may return a cleanup function that will be called when the component is disconnected from the DOMcustomBindingHandlers({ stateKey, element, formattedValue, rawValue }: { stateKey?: string; element?: HTMLElement; formattedValue?: string; rawValue?: StateValue }): Record<string, () => void>: Override to add custom binding handlers for state updates. All parameters are optional.
Context Methods
createContext(stateKey: string): Create a new context object for sharing state between componentsexposeContext(context: Context): Expose state to child components through a context providerconsumeContext(context: Context): Subscribe to a context from a parent component
Element Processing
Processes an element's attributes for special bindings and state declarations. This method is responsible for:
Reference Processing
- Handles
$refattributes to create element references - Populates the component's
refsobject - Automatically removes ref attributes after processing
- Handles
State Declaration Processing
- Process
$stateattributes for direct state declarations - Extracts initial values from element content
- Establishes state-to-element bindings
- Removes state declaration attributes
- Process
Binding Setup
- Handles
$bind-*attributes for two-way data binding - Establishes appropriate event listeners for form elements
- Sets up validation and type coercion
- Removes binding attributes after setup
- Handles
Event Handler Registration
- Process
on*event handler attributes - Binds event handlers to component methods
- Provides event context to handler functions
- Removes event attributes after binding
- Process
Binding Types
| Binding Type | Description | Example |
| ---------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------- |
| $bind-text | Text content with automatic updates | <span $bind-text="name"></span> |
| $bind-html | WARNING: No built-in sanitization. Renders raw HTML. See Security section. | <div $bind-html="content"></div> |
| $bind-value | Form input value with two-way binding | <input $bind-value="username"> |
| $bind-checked | Checkbox/radio state | <input type="checkbox" $bind-checked="isActive"> |
| $bind-disabled | Element disabled state | <button $bind-disabled="isLoading">Submit</button> |
| $bind-class | Dynamic class operations | <div $bind-class="panelClasses"> |
| $bind-attr | Dynamically sets or removes multiple element attributes | <button $bind-attr="buttonAttrs"> |
| $bind-* | Custom state binding type | <div $bind-custom="myState"> |
AI-Assisted Development
ReactiveComponent includes an authoritative prompt specification (prompt.txt) that helps developers create robust, accessible web components using AI assistance. It defines the project’s core rules and workflows.
prompt.txt (Authoritative Prompt)
- Location: project root at
prompt.txt - Purpose: Acts as the single source of truth for HTML-first workflow, binding validation, state management rules, and security guidance
- Key directives:
- HTML-first with approval gate before writing TypeScript/JavaScript
- Strict binding validation (alphanumeric keys only; no expressions in
$bindings) - Separation of concerns: keep user-facing text in HTML; no HTML/CSS in TS/JS
- Security guidance for
$bind-htmland custom binding handlers - TypeScript-first patterns and use of Lucide Icons for icons
- How to use:
- Read
prompt.txtbefore generating or updating components - Draft HTML structure first; request approval before adding TS/JS
- Use computed properties instead of expressions in bindings
- Avoid hardcoding secrets or user-facing text in TS/JS
- Read
This feature provides:
Key Benefits
Structured Development Process
- Standardized component creation workflow
- Built-in validation steps
- Best practices enforcement
Quality Assurance
- Accessibility compliance checks
- Performance optimization guidelines
- Code quality standards
Automated Guidance
- Component architecture recommendations
- State management patterns
- Error handling strategies
Developer Server
Reactive Component uses Query as a developing system that provides bundling, server-side rendering, hot reloading, and state persistence. Here's how they are used together: ReactiveComponent uses Query as a developing system that provides bundling, server-side rendering, hot reloading, and state persistence. Here's how they are used together:
Getting Started
- Start by cloning the ReactiveComponent repository:
git clone https://github.com/gc-victor/reactive-componentThen
cd reactive-componentInstallation
- First, ensure you have all the packages installed:
# Using pnpm (recommended)
pnpm installOr
# Using npm
npm installSetup
Set the local Query settings.
pnpm query settingsInstall assets.
pnpm devThen in a different terminal run:
pnpm query asset publicProject Structure
The project includes:
dist/index.js # Distributed version of the Reactive Component
src
├── index.ts # Reactive Component
├── index.d.ts # Type definitions for Reactive Component
├── public # Public Assets
├── pages # Application pages
│ ├── get.index.tsx # Page server function
│ ├── hot-reload # Hot reload service
│ ├── index.island.js # Examples of Reactive Components
│ ├── layout # Layout components
│ ├── lib # Helper functions
│ └── styles.css # Global styles
└── tests # Test filesIntegration Example
Here's how to integrate the BasicCounter component with Query:
// src/pages/get.index.tsx
import { Page } from "@/pages/layout/page";
export async function handleRequest(req: Request): Promise<Response> {
return response(
<Page>
<div className="container mx-auto p-4">
<basic-counter className="p-4 border rounded block">
<p className="mb-2">
Count: <span $state="count">0</span>
</p>
<button type="button" $onclick="decrement" className="mr-2 bg-blue-500 text-white px-4 py-2 rounded">
Decrement
</button>
<button type="button" $onclick="increment" className="bg-blue-500 text-white px-4 py-2 rounded">
Increment
</button>
</basic-counter>
</div>
</Page>,
);
}Key Integration Points
- Initial Render
- Server generates complete HTML document
- Web Components are defined in included scripts
- No hydration needed
- State Management
- Initial state can be embedded in HTML attributes
- Web Components handle their own state after initialization
- No explicit hydration or state reconciliation is required
- APIs and Data Flow
- Query functions handle API requests
- Web Components can fetch data through standard APIs
- Database access is controlled through Query's server functions
- Runtime Behavior
- Server provides initial HTML and required scripts
- Web Components take over client-side functionality
- Clean separation between server and client concerns
References
- Query Website: https://qery.io
- Query - GitHub: https://github.com/gc-victor/query
Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you want to change.
License
This project is licensed under the MIT License - see the LICENSE file for details.
