Events
Ripl provides a full event system modeled after the browser DOM. Elements can listen for and emit events, events bubble up through the element hierarchy, and propagation can be stopped, all familiar patterns for web developers.
Demo
Hover over, click, and drag the elements to see events in action.
NOTE
For the full API, see the Core API Reference.
EventBus
Every element in Ripl extends EventBus, which provides the core event subscription and emission API.
on(event, handler, options?)
Subscribe to an event. Returns a disposable subscription:
const subscription = circle.on('click', (event) => {
console.log('Clicked!', event.data);
});
// Later, unsubscribe
subscription.dispose();on('*', handler, options?)
Subscribe to the wildcard event type to receive every event emitted on a bus, whatever its type — including custom types that never appear in $events. Because events bubble, a wildcard subscription on a group or scene observes its whole subtree, and each event's target still identifies the element it was emitted on:
import {
EVENT_WILDCARD,
} from '@ripl/core';
scene.on(EVENT_WILDCARD, (event) => {
console.log(event.type, 'from', event.target.type);
});Handlers for the event's own type run first, then wildcard handlers. stopPropagation() and the self option apply exactly as they do to a typed subscription.
A wildcard subscription is deliberately invisible to has(): it reports only listeners registered for a concrete type. Since pointer events are dispatched to elements that has them, observing a bus never turns it into a hit-test target — which is what lets the devtools record a scene's events without changing how it behaves.
once(event, handler)
Subscribe to an event that fires only once:
circle.once('click', (event) => {
console.log('First click only');
});off(event, handler)
Remove a previously registered handler:
circle.off('click', myHandler);To drop every listener on an element, use destroy().
emit(event, data?)
Emit an event. The event bubbles up to parent elements by default:
circle.emit('custom-event', { value: 42 });Event Object
Event handlers receive an Event object containing type, data (the payload), target (the bus the event was originally emitted on, preserved as it bubbles), and timestamp (a high-resolution time reading taken when the event was created).
stopPropagation()
Prevent the event from bubbling further up the tree:
circle.on('click', (event) => {
event.stopPropagation();
// Parent group's click handler will NOT fire
});Pointer Events
When elements are rendered to a Context, the context automatically delegates DOM pointer events to the correct elements based on hit testing. A Scene manages the render lifecycle, but the context itself owns interaction.
Following browser DOM behavior, pointer events target the topmost element (highest zIndex) at the cursor position. If overlapping elements exist, only the frontmost one receives the event; lower elements are occluded. The event then bubbles up through the parent hierarchy as usual.
Elements with pointerEvents set to 'none' are transparent to hit testing, allowing events to pass through to the next element below.
Tracked Events
The context tracks click, mousedown, mouseup, mouseenter, mouseleave, mousemove, dragstart, drag, and dragend events automatically.
const scene = createScene('.container', {
children: [circle],
});
scene.render();
circle.on('mouseenter', () => {
circle.fill = '#ff006e';
scene.render();
});
circle.on('mouseleave', () => {
circle.fill = '#3a86ff';
scene.render();
});IMPORTANT
Pointer events only work when elements have been rendered to a context. The context handles DOM event listening and hit testing; see Context: Interaction.
Drag Events
Ripl supports drag interactions on elements via dragstart, drag, and dragend events. A drag begins when the pointer is pressed on an element and moved beyond a configurable threshold (default 3px). Once the threshold is exceeded, dragstart fires, followed by drag on each subsequent move, and dragend on pointer release.
let originX = 0;
let originY = 0;
circle.on('dragstart', (event) => {
originX = circle.cx;
originY = circle.cy;
console.log('Drag started at', event.data.x, event.data.y);
});
circle.on('drag', (event) => {
circle.cx = originX + event.data.deltaX;
circle.cy = originY + event.data.deltaY;
scene.render();
});
circle.on('dragend', (event) => {
console.log('Drag ended at', event.data.x, event.data.y);
});The drag and dragend events include startX/startY (where the drag originated) and deltaX/deltaY (the total movement since the drag started, not the step since the previous event). Record the element's position on dragstart and add the delta to it, as above: that preserves the offset between the cursor and the element's origin, and — because each payload is a total rather than a running sum — the element stays put under the cursor even if a move event is coalesced or dropped.
Every pointer payload — x/y, startX/startY, and the deltas — is in logical space: CSS pixels relative to the context's top-left, the space elements themselves are authored in. They are not device pixels, they are not element-local, and they do not move with the page scroll.
The drag threshold can be configured via context options:
const context = createContext('.container', {
dragThreshold: 5, // pixels before dragstart fires
});NOTE
Drag events continue to fire even when the pointer moves outside the element, until the pointer is released.
Event Bubbling
Events bubble up through the element hierarchy, just like the DOM. If a circle inside a group emits a click event, the group will also receive it:
const circle = createCircle({
cx: 100,
cy: 100,
radius: 50,
});
const group = createGroup({ children: [circle] });
// This fires when the circle (or any child) is clicked
group.on('click', (event) => {
console.log('Group received click from:', event.target.type);
});Self Option
Use the self option to only handle events that originated from the element itself (not from children):
group.on('click', (event) => {
console.log('Only fires for direct group clicks');
}, { self: true });Custom Events
You can emit and listen for any custom event name:
circle.on('highlight', (event) => {
circle.fill = event.data.color;
});
circle.emit('highlight', { color: '#ff006e' });The pointerEvents Property
The pointerEvents property on elements controls hit testing behavior. Set it to 'all' (default, responds to fill and stroke), 'fill', 'stroke', or 'none' (click-through).
const overlay = createRect({
pointerEvents: 'none', // Click passes through to elements below
fill: 'rgba(0, 0, 0, 0.3)',
x: 0,
y: 0,
width: 400,
height: 300,
});