-
Notifications
You must be signed in to change notification settings - Fork 5
Description
Vision
v3: a micro-framework for writing JS components.
v4: a toolkit of HTML-first meta-components for adding interactivity.
HTML becomes the primary interface. Custom JS is only needed for complex cases (Slider, Drag, etc.). Meta-components (Action, Fetch, DataBind, Transition) cover 80% of use cases directly from markup.
Principles:
- 0 runtime dependencies
- HTML-first, progressive enhancement, Light DOM
- What the browser does natively, we do not reimplement
- The registry is the catalog of components available to HTML
Architectural changes
$emit — bubbling events (#630)
$emit dispatches a standard CustomEvent with bubbles: true and an __source property referencing the emitting instance:
$emit(event, ...args) {
const e = new CustomEvent(event, { bubbles: true, detail: args });
e.__source = this;
this.$el.dispatchEvent(e);
}- Events bubble up the DOM natively — enables event delegation
__sourceidentifies the emitting instance foron{Child}{Event}resolution- Action keeps working as-is —
data-on:fetch-beforelistens tofetch-beforewhich bubbles normally $on/$offkeep working as-is — direct listener on$el
on{Child}{Event} — event delegation
The parent listens on this.$el instead of binding on each child. The handler walks up from event.target to this.$el, looking for mounted instances.
handler(event) {
let el = event.target;
while (el && el !== this.$el) {
if (el.__base__) {
for (const [name, instance] of el.__base__) {
if (instance === "terminated") continue;
const method = `on${name}${pascalCase(event.type)}`;
if (typeof this[method] === "function") {
this[method]({ event, target: instance });
return;
}
}
}
el = el.parentElement;
}
}el.__base__is aMap<string, Base>keyed byconfig.name(already implemented on develop)- Dynamically added children work automatically (no binding/unbinding needed)
- The parent still needs
config.componentsto know which events to listen to (resolving method name ambiguity:onSliderDragStart→SliderDrag+Start) mouseenter/mouseleavedo not bubble — these keep direct binding on the child element (accepted limitation)
$parent → $closest(name)
$parent is removed. Replaced by an explicit query:
// v3
this.$parent.goNext();
// v4
this.$closest("Slider").goNext();$children → $query(name)
// v3
for (const item of this.$children.SliderItem) { ... }
// v4
for (const item of this.$query("SliderItem")) { ... }Sugar for queryComponentAll(name, { from: this.$el }) (already implemented as helper, #629).
createApp removed
The registry + MutationObserver handle everything. Features are configured separately (#691):
// v4
import { defineFeatures, registerComponent } from "@studiometa/js-toolkit";
defineFeatures({ breakpoints: { ... } });
registerComponent(MyComponent);No root "App" component required. Each component is independent.
SafeAction — CSP-safe declarative commands
New component alongside Action. No new Function(), just declarative method calls:
<!-- Action (CSP-unsafe, kept for backwards compat) -->
<button data-component="Action" data-on:click="Modal->target.open()">
<!-- SafeAction (CSP-safe, recommended) -->
<button data-component="SafeAction" data-on:click="Modal.open">
<button data-component="SafeAction" data-on:click="Modal(#my-modal).open">
<button data-component="SafeAction" data-on:click="Modal(.sidebar).close">Format: Component[(selector)].method — parsed with a simple regex, resolved via the registry.
Breaking changes
- Make custom events bubble up by default with
__sourceannotation ([Feature] Enable bubbling and cancelation for custom events #630) - Refactor
EventsManager— event delegation onthis.$elinstead of binding on each child - Remove
createApp(replaced byregisterComponent+defineFeatures) - Remove
$parentproperty (replaced by$closest(name)) - Remove
$rootproperty - Remove
$childrenproperty (replaced by$query(name)) - Remove
LoadService - Remove
KeyService - Remove ability to configure the blocking feature
- Maybe remove prefix and attributes feature configuration (to reduce size)
- Update default breakpoints to match
@studiometa/tailwind-config - Make
ResponsiveOptionsManagerthe default - Config merge strategy for refs ([Base] Maybe improve
configmerge strategy? #627) — merge by default instead of override
New features
-
$query(name)— scoped descendant instance query on Base -
$closest(name)— ancestor instance lookup on Base -
defineFeatures()helper ([Features] Features should be configurable outside of thecreateApphelper #691) -
SafeActioncomponent — CSP-safe declarative commands - Multiple option types ([Base] Add support for multiple option type #651)
- Sibling configuration
config.use([Feature] Add support for sibling configuration #697) - Component tree inspector / DX improvements (Improve DX #653)
Meta-components integrated into core
Some components from @studiometa/ui are promoted to the core framework:
-
Action— event → JS expression on a target (CSP-unsafe) -
SafeAction— event → declarative command on a target (CSP-safe) -
Fetch— HTTP request → DOM swap -
Transition— enter/leave animations -
DataBind,DataModel,DataEffect,DataComputed— reactive data binding
UI components removed (replaced by web platform)
These components no longer need to exist in @studiometa/ui:
| Component | Native replacement |
|---|---|
Modal |
<dialog> + showModal() + Invoker Commands (commandfor) |
Accordion / AccordionItem |
<details> + <summary> |
AnchorScrollTo |
scroll-behavior: smooth + <a href="#id"> |
Sticky |
position: sticky |
Figure |
loading="lazy" |
Hoverable |
CSS :hover + :has() |
Prefetch |
<link rel="prefetch"> + Speculation Rules API |
Sentinel |
IntersectionObserver (trivial) |
Services
| Service | v4 status |
|---|---|
RafService |
✅ Keep — read/write scheduling has no native equivalent |
DragService |
✅ Keep — custom drag + inertia has no native equivalent |
ScrollService |
✅ Keep — aggregation + throttle still useful |
ResizeService |
ResizeObserver |
PointerService |
|
LoadService |
🗑 Remove |
KeyService |
🗑 Remove |
MutationService |
Already done on develop (v3.4.x)
- ✅
registerComponent()helper ([Feature] Add support for a registry #683, [Feature] Add support for custom name or selector to register a component #676) - ✅ Global registry with MutationObserver auto-mount
- ✅
__base__is aMap<string, Base>keyed byconfig.name - ✅
$parentresolved dynamically viagetInstances()+findClosestInstance()([Bugfix] Refactor$parentto work with the new registry #700, [Bug] The$parentproperty might not be defined with the new registry #699) - ✅
addToRegistry()recursively registersconfig.componentschildren ([Base] Children component should be registered withregisterComponent#690, [Bugfix] Add child components to the registry #692) - ✅
createAppsimplified (no internal MutationObserver — registry handles it) - ✅
queryComponent/queryComponentAllhelpers ([Feature] Add aqueryComponenthelper function #629) - ✅ Auto-update when DOM changes via MutationObserver ([Base] Add support for auto-updating a component when its DOM changes #562)
Migration path
Phase 1 — v3.x (non-breaking)
- Add
$query()and$closest()as methods onBase - Add
defineFeatures()helper ([Features] Features should be configurable outside of thecreateApphelper #691) - Deprecation warnings on
$parent,$children,$root
Phase 2 — v4 (breaking)
-
$emitwithbubbles: true+__source([Feature] Enable bubbling and cancelation for custom events #630) - Event delegation in
EventsManager - Remove
$parent,$root,$children - Remove
createApp - Remove
LoadService,KeyService - Integrate Action, SafeAction, Fetch, Transition, Data* into core
- Config merge strategy for refs ([Base] Maybe improve
configmerge strategy? #627) - Multiple option types ([Base] Add support for multiple option type #651)
- Sibling configuration
config.use([Feature] Add support for sibling configuration #697)