Repository navigation
hx data Reactive API
Creates a reactive root object. Returns a proxy that fires change events on any nested modification.
function reactive<T extends object>(
target: T,
options?: ReactiveOptions
): ReactiveRoot & Tconst state = reactive({ count: 0, items: ['a', 'b'] });
// state is both ReactiveRoot AND { count, items }interface ReactiveOptions {
onChanged?: (event: ValueChangedEvent) => void;
}const state = reactive(
{ count: 0 },
{
onChanged(event) {
console.log('built-in listener:', event.pathToRoot);
}
}
);The onChanged callback is called for EVERY change. It's a shorthand equivalent to ERO.on(state, '*', listener), but fires before other registered listeners.
ERO is the singleton static class providing the public API. All methods are static.
import { ERO } from '@hx/data';
Subscribe to changes at a path.
ERO.on(
obj: any, // any reactive object (root or nested)
path: string, // "*" | "path.*" | "path"
listener: (event: ValueChangedEvent) => void
): voidconst state = reactive({ user: { name: 'John', age: 30 } });
// Global: catch everything
ERO.on(state, '*', (e) => {
console.log(`[global] ${e.pathToRoot}: ${e.oldValue} → ${e.newValue}`);
});
// Branch: catch user subtree
ERO.on(state, 'user.*', (e) => {
console.log(`[user] ${e.pathToRoot}: ${e.oldValue} → ${e.newValue}`);
});
// Exact: only user.name changes
ERO.on(state, 'user.name', (e) => {
console.log(`[name] ${e.oldValue} → ${e.newValue}`);
});The callback receives the full ValueChangedEvent.
Unsubscribe.
const listener = (e: ValueChangedEvent) => console.log(e.pathToRoot);
ERO.on(state, 'count', listener);
state.count = 1; // fires
ERO.off(state, 'count', listener);
state.count = 2; // does NOT firePath must match exactly what was passed to ERO.on().
Manually fire a change event without modifying the object.
ERO.emit(state, 'count', 0, 99);
// Any listener on 'count' or 'count.*' will fireUseful for custom mutation patterns or external state sync.
Type guard for ReactiveRoot.
const root = reactive({ x: 1 });
const nested = root.x; // not an object
ERO.isReactiveRoot(root); // true
ERO.isReactiveRoot(nested); // falseType guard for any reactive object (root or nested).
const root = reactive({ user: {} });
const user = root.user;
ERO.isReactiveObject(root); // true
ERO.isReactiveObject(user); // true (nested objects are also proxied)
ERO.isReactiveObject(123); // falseGet the root reactive object from any nested descendant.
const root = reactive({ user: { address: { city: 'NY' } } });
const address = root.user.address;
ERO.rootOf(address) === root; // trueGet the parent. Returns undefined if obj is the root.
const root = reactive({ user: { name: 'John' } });
const user = root.user;
ERO.parentOf(user) === root; // true
ERO.parentOf(root) === undefined; // trueAbsolute path from root to this object.
const root = reactive({ user: { address: { city: 'NY' } } });
const city = root.user.address;
ERO.pathToRoot(city); // 'user.address'Path segment from parent to this object.
const root = reactive({ user: { name: 'John' }, items: ['a', 'b'] });
ERO.pathToParent(root.user); // 'user'
ERO.pathToParent(root.items); // 'items'Resolve an absolute path. Supports relative traversal.
const root = reactive({
user: { name: 'John', address: { city: 'NY' } },
age: 30
});
const address = root.user.address;
// Absolute path of this object
ERO.pathOf(address); // 'user.address'
// Relative: ./ goes deeper
ERO.pathOf(address, './city'); // 'user.address.city'
// Relative: ../ goes up
ERO.pathOf(address, '../name');// 'user.name'
// Multiple levels up
ERO.pathOf(address, '../../age'); // 'age'
// Absolute: / resets to root
ERO.pathOf(address, '/age'); // 'age'Get a value. Works on both plain and reactive objects. For reactive objects, supports relative/absolute paths.
// Plain object
ERO.getValue({ a: { b: 1 } }, 'a.b'); // 1
// Reactive with relative path
const state = reactive({ user: { name: 'John', age: 30 } });
const user = state.user;
ERO.getValue(user, './name'); // 'John'
ERO.getValue(user, '/user/age'); // 30Set a value. Triggers events on reactive objects. Creates intermediate paths automatically.
const state = reactive({ user: { name: 'John' } });
ERO.setValue(state, 'user.name', 'Jane'); // fires event
ERO.setValue(state, 'user.address.city', 'NY'); // creates intermediate objectsWith relative paths:
const address = state.user.address; // created above
ERO.setValue(address, './zip', '10001');
ERO.setValue(address, '../name', 'Bob');Set without triggering events. Works directly on the underlying plain object. mode defaults to mute-all:
-
mute-all(default): no change events at all -
mute-leaf: only the leaf is silent; intermediate path creation fires -
loud: emits all change events, same asERO.setValue
const state = reactive({ user: { name: 'John' } });
// No events at all (default mode)
ERO.setValueSilent(state, 'user.name', 'Jane');
ERO.setValueSilent(state, 'user.name', 'Jane', 'mute-all');
// Only the leaf is silent; intermediate creation fires
ERO.setValueSilent(state, 'user.newField', 'value', 'mute-leaf');Unwrap. Returns the plain JavaScript object with no proxies.
const reactiveObj = reactive({ x: 1 });
const plain = ERO.revoke(reactiveObj);
plain.x = 2; // no events fired
reactiveObj.x = 3; // events fire (proxy still active)Create a stub reactive object that logs errors on any access. Useful for testing or placeholder states.
const fake = ERO.fake<{ name: string }>();
fake.name; // console.error: "Always returns undefined, when getter[name] called..."
fake.name = 1; // console.error: "Always returns true, when setter[name] called..."Like pathOf but returns undefined for null/undefined input (no throw).
ERO.loosePathOf(null); // undefined
ERO.loosePathOf(someObj); // same as ERO.pathOf(someObj)- Overview
- Common Concepts
- I18n
- Data Attributes
- Form Components
- Layout Components
- Display Components
- Overlay Components
- Structure Components
- Validation
- Hooks
- Utilities