@io-gui/core
v2.0.0-alpha.3
Published
A lightweight library that provides core features for Io-Gui components (nodes and elements).
Readme
@io-gui/core
A lightweight (~25KB gzipped) core reactive library for Io-Gui framework.
Core Classes
ReactiveNode
Reactive Object with all Io-Gui features. Use for data models, state containers, and business logic.
IoElement
Reactive HTMLElement with ReactiveNode features plus virtual DOM rendering and CSS style management.
Both share identical APIs: reactive properties, bindings, event dispatch, change handlers, and lifecycle methods.
Registration
Every subclass must be registered before instantiation using the @Register decorator:
@Register
class MyNode extends ReactiveNode {
@ReactiveProperty({type: String, value: ''})
declare label: string
}Registration triggers ProtoChain initialization which aggregates property definitions, listeners, handlers, and styles from the entire prototype chain.
Reactive Properties
Declare with @ReactiveProperty decorator or static get ReactiveProperties():
static get ReactiveProperties() {
return {
count: Number, // Type only → defaults to 0
label: '', // Value only → infers type
enabled: {type: Boolean, value: true}, // Full definition
data: {type: Object, init: null}, // Object type with empty init
size: {type: Array, init: [0, 0]}, // Array with initial values
items: {type: NodeArray, init: 'this'} // NodeArray requires 'this' init
}
}Property Definition Options
| Option | Description |
|--------|-------------|
| value | Initial value |
| type | Constructor (String, Number, Boolean, Array, Object, ReactiveNode, etc.) |
| binding | Binding object for two-way sync |
| reflect | If true, syncs to HTML attribute (IoElement only) |
| init | Constructor arguments. Use null for empty init, 'this' for NodeArray |
Type Defaults
String→''Number→0Boolean→falseObject→{}(wheninit: null)Array→[](wheninit: null)
Non-Reactive Properties
Use @Property decorator or static get Properties() for properties that don't need change tracking:
@Property(Object)
declare $: Record<string, HTMLElement> // Common pattern for element refsChange Detection & Handlers
When a reactive property changes:
[propName]Changed(change)handler is invoked (if defined)[propName]-changedevent is dispatched with{property, value, oldValue}changed()handler is invokedio-object-mutationevent is dispatched for the node
labelChanged(change: Change) {
console.log(change.oldValue, '→', change.value)
}
changed() {
// Called after any property change
}Reactivity Modes
Control dispatch timing via reactivity property:
| Mode | Behavior |
|------|----------|
| 'immediate' | Synchronous dispatch (default) |
| 'throttled' | Once per animation frame, first value wins |
| 'debounced' | Once per animation frame, last value wins |
Two-Way Binding
Create bindings with this.bind('propertyName'):
const binding = sourceNode.bind('value')
targetNode.prop = binding // Target syncs to source
// Changes propagate bidirectionallyBinding Edge Cases
- Multiple targets: One binding can sync to many target properties
- Binding collision: If target already has a binding, the old one is removed
- NaN handling:
NaN === NaNis false; bindings handle this correctly - Type mismatch: Debug mode warns if bound properties have incompatible types
- Re-assignment: Assigning the same binding object is a no-op
Object Mutation Observation
ReactiveNode-typed Properties
Automatically observed. Mutations trigger [propName]Mutated() handlers.
@ReactiveProperty({type: MyReactiveNode, init: null})
declare data: MyReactiveNode
dataMutated(event: CustomEvent) {
// Called when data or its descendants mutate
}Non-ReactiveNode Objects
Observed but require manual mutation dispatch:
this.plainObject.value = 42
this.dispatchMutation(this.plainObject)NodeArray
A Proxy-wrapped Array that auto-dispatches mutations on all mutating operations (push, splice, etc.). Items must be ReactiveNode instances.
@ReactiveProperty({type: NodeArray, init: 'this'})
declare items: NodeArray<MyReactiveNode>
itemsMutated() {
// Called on any array modification
}Edge cases:
fill()andcopyWithin()are unsupported (log warning)- Setting
lengthto extend array logs warning - Item listeners are automatically managed on add/remove
Event System
Listener Definition
Subclass Listeners replace parent handlers for the same event name (last wins). Only one proto listener is registered per event at runtime.
static get Listeners() {
return {
'click': 'onClick', // Method name
'pointermove': ['onMove', {passive: true}], // With options
}
}Inline Event Listeners
const element = new MyElement({
'@click': this.onClick,
'@custom-event': (e) => console.log(e.detail)
})Dispatch Events
this.dispatch('my-event', {data: 42}, true) // type, detail, bubblesEvent Propagation for ReactiveNodes
Non-DOM ReactiveNodes bubble events through _parents array. Add/remove parents with addParent()/removeParent().
Virtual DOM (IoElement)
Render children with this.render():
changed() {
this.render([
div({class: 'container'}, [
span({id: 'label'}, this.label),
MyComponent.vConstructor({value: this.bind('value')}),
])
])
}vDOM Edge Cases
nullchildren are filtered out (useful for conditional rendering)- Element reuse: matching tag names update props; mismatched tags replace element
this.$map stores elements withidprop:this.$.label- Native element events require
EventDispatcherattachment (automatic) - Text content optimized via single TextNode (no layout thrashing)
Styling (IoElement)
static get Style() {
return /* css */`
:host {
display: flex;
}
:host[disabled] {
opacity: 0.5;
}
`
}Rules:
- All selectors must start with
:host - Styles are aggregated up prototype chain
- Injected to document
<head>at registration - Custom mixins via
--mixin-name: { ... }and@apply --mixin-name
ThemeSingleton
Global CSS variables manager. Properties map to --io_propertyName variables:
ThemeSingleton.themeID = 'dark'
ThemeSingleton.spacing = 4
ThemeSingleton.bgColor = new Color(0.2, 0.2, 0.2, 1)Themes persist to localStorage. Register custom themes with registerTheme(id, vars).
Storage
Persistent reactive values:
const myValue = Storage({key: 'my-key', value: 'default', storage: 'local'})
element.prop = myValue // Binding syncs to storage
// Storage types: 'local' (localStorage), 'hash' (URL hash), 'none'Privacy: Storage.permit() / Storage.unpermit() controls localStorage access.
Lifecycle
| Method | When Called |
|--------|-------------|
| init() | Before property initialization |
| ready() | After construction, before first dispatch |
| changed() | After any reactive property change |
| connectedCallback() | IoElement attached to DOM |
| disconnectedCallback() | IoElement removed from DOM |
| dispose() | Manual cleanup (removes listeners, bindings) |
Throttle/Debounce
this.throttle(this.expensiveOperation) // Once per frame, first call wins
this.debounce(this.expensiveOperation) // Once per frame, last call winsFrame-based queue with automatic cleanup on disposal.
IoGl
WebGL-rendered element base class. Properties auto-map to shader uniforms:
@Register
class MyShader extends IoGl {
@ReactiveProperty({type: Number, value: 0.5})
declare intensity: number // → uniform float uIntensity
static get Frag() {
return `void main() { gl_FragColor = vec4(uIntensity); }`
}
}Theme variables available as io_* uniforms. Shared WebGL context across all instances.
IoOverlaySingleton
Document-level container for floating UI (menus, modals, tooltips). Manages focus restoration and pointer blocking.
IoOverlaySingleton.appendChild(myMenu)
IoOverlaySingleton.expanded = trueUtilities
Focus Navigation
io-focus-to event system for keyboard navigation between focusable elements.
Nudge
Viewport-aware element positioning:
nudge(floatingElement, anchorElement, 'down') // 'up'|'down'|'left'|'right'|'over'Debug Mode
Runtime type checking, warning logs, and validation in development. Debug blocks stripped in production builds:
debug: {
if (typeof value !== 'string') {
console.warn('Expected string')
}
}Common Pitfalls
- Forgetting
@Register: Classes must be registered before use - Arrow function handlers: Won't auto-bind; use regular methods for
on*handlers - Direct object mutation: Must call
dispatchMutation()for non-ReactiveNode objects - NodeArray init: Must use
init: 'this'to get owner reference - Disposed nodes: Operations on disposed nodes are no-ops
- Binding loops: Circular bindings are prevented; same-binding assignment exits early
- Property type mismatch: Debug warnings for assigning wrong types
- CSS selector leakage: Non
:hostprefixed selectors leak to global scope
