@rupertsworld/observable
v0.1.2
Published
Observable properties mixin and ObservableElement with attribute reflection
Downloads
923
Readme
@rupertsworld/observable
Observable properties for classes and custom elements.
Why?
Reacting to property changes typically means writing boilerplate getters/setters or remembering to call update functions. This library provides a declarative observedProperties pattern that works on any class — and extends naturally to custom elements with attribute reflection.
Install
npm install @rupertsworld/observableQuick Examples
Any class with observable(Base)
import { observable } from "@rupertsworld/observable";
class Model extends observable(Object) {
static observedProperties = ["data"];
data = null;
propertyChangedCallback(name: string, oldValue: unknown, newValue: unknown) {
console.log(`${name} changed from`, oldValue, "to", newValue);
}
}
const model = new Model();
model.data = { foo: 1 }; // logs: "data changed from null to { foo: 1 }"Observable with typed events
Observable is observable(EventTarget) — use it when you want to dispatch events:
import { Observable } from "@rupertsworld/observable";
class ChangeEvent extends Event {
type = "change" as const;
constructor(public property: string, public value: unknown) {
super("change");
}
}
class Counter extends Observable<ChangeEvent> {
static observedProperties = ["count"];
count = 0;
propertyChangedCallback(name: string, _oldValue: unknown, newValue: unknown) {
this.dispatchEvent(new ChangeEvent(name, newValue));
}
}
const counter = new Counter();
counter.addEventListener("change", (e) => {
console.log(e.property, e.value); // typed
});
counter.count = 5; // logs: "count" 5Custom elements with ObservableElement
import { ObservableElement } from "@rupertsworld/observable";
class MyCounter extends ObservableElement {
static observedProperties = {
count: { type: Number, attribute: "count" },
};
count = 0;
connectedCallback() {
this.render();
this.addEventListener("click", () => this.count++);
}
propertyChangedCallback() {
this.render();
}
render() {
this.innerHTML = `<button>Count: ${this.count}</button>`;
}
}
customElements.define("my-counter", MyCounter);<my-counter count="5"></my-counter>The count property:
- Syncs with the
countattribute automatically - Coerces strings to numbers
- Triggers
propertyChangedCallbackwhen changed
Typed Events
You can add type safety for custom events by passing a generic parameter:
class CountChangeEvent extends Event {
type = "count-change";
}
class MyCounter extends ObservableElement<CountChangeEvent> {
// ...
propertyChangedCallback(name: string) {
if (name === "count") {
this.dispatchEvent(new CountChangeEvent('count-change'));
}
this.render();
}
}const counter = document.querySelector("my-counter") as MyCounter;
counter.addEventListener("countchange", (e) => {
console.log(e.count); // typed as number
});Features
- Works on any class —
observable(Base)mixin works withObject,EventTarget, or any base - Reactive properties — declare once, get automatic getters/setters
- Attribute sync —
ObservableElementkeeps properties and attributes in sync - Type coercion —
Number,Boolean,String,Objectbuilt-in - Typed events — optional strong typing for
addEventListener/dispatchEvent - Drop-in replacement —
ObservableElementbehaves exactly like nativeHTMLElement
Documentation
See docs/SPEC.md for the full API reference.
License
MIT
