Overview

onParent(targetRef, events) is an element directive placed on a child (or descendant). It attaches listeners to the element held by targetRef (typically a parent/ancestor set with setRef).

Listeners bind when the ref’s element becomes available, re-sync when the event map updates, and unbind when the host element disconnects.

onEvent(map) binds listeners on the host itself. Keys are DOM event names plus enterViewport, exitViewport, and clickOutside. Import it as onEvent — the name on is the reactive hook. removeOn(el) tears those listeners down early.

onEleEvent(name, handler) listens on a parent for a bubbling CustomEvent. useEleEvent() is called in a component’s outer function and returns dispatch(eventName, detail?), which fires from that component’s host.

Which event API

API
Listener target
Notes
@click=${fn} Same element Native lit-html listener on the host node.
onParent(ref, map) Ref’d ancestor Child hosts the directive; events fire on the parent/ancestor element.
Hook on(fn) Reactive subscription Outer-function hook. Not an element listener.
onEvent(map) Host element DOM events plus enterViewport, exitViewport, and clickOutside.
onEleEvent / useEleEvent Bubbling CustomEvent Parent listens. dispatch() fires from the child component host.

Child listening to parent scroll

The scroll container owns a ref. Each row applies onParent(scrollRef, { scroll, click }) so listeners attach to the parent and clean up with the row.

import { html, atom, ref, setRef, onParent, renderApp } from 'mates';

const App = () => {
const scrollRef = ref();
const scrollTop = atom(0);
const clicks = atom(0);

return () => html`
<div class="m-col m-gap">
<p class="label m-0">
scrollTop: ${scrollTop()}px · parent clicks: ${clicks()}
</p>
<div
${setRef(scrollRef)}
class="p-20"
>
<div
${onParent(scrollRef, {
scroll: (e) => scrollTop.set(Math.round(e.target.scrollTop)),
click: () => clicks.set(n => n + 1),
})}
>
${Array.from({ length: 8 }, (_, i) => html`
<div class="card card-body m-b-10">
Row ${i + 1} — scroll/click listeners are on the parent via onParent
</div>
`)}
</div>
</div>
</div>
`;
};
renderApp(App, document.getElementById('app'));

Parent click counter

Listen for click on an ancestor ref from a nested child.

import { html, atom, ref, setRef, onParent, renderApp } from 'mates';

const App = () => {
const wrap = ref();
const n = atom(0);
return () => html`
<div class="m-col m-gap">
<div ${setRef(wrap)} class="card card-body">
<p class="label m-0">Parent clicks: ${n()}</p>
<div ${onParent(wrap, { click: () => n.set(c => c + 1) })}>
<button class="btn-ghost">Click inside (bubbles to parent)</button>
</div>
</div>
</div>
`;
};
renderApp(App, document.getElementById('app'));

onEvent and onEleEvent

onEvent binds host listeners, including clickOutside. useEleEvent().dispatch fires a bubbling CustomEvent from the child component host. The parent hears it with onEleEvent.

import { html, atom, onEvent, onEleEvent, useEleEvent, x, renderApp } from 'mates';

const Row = (item) => {
const { dispatch } = useEleEvent();
return () => html`
<button class="btn-ghost" @click=${() => dispatch('pick', item().id)}>
${item().label}
</button>
`;
};

const App = () => {
const picked = atom('none');
const outside = atom(0);
const rows = [{ id: 'a', label: 'Ada' }, { id: 'b', label: 'Grace' }];
return () => html`
<div class="m-col m-gap">
<p class="label m-0">Picked: ${picked()} · outside: ${outside()}</p>
<div
class="card card-body m-col m-gap"
${onEleEvent('pick', (id) => picked.set(id))}
${onEvent({ clickOutside: () => outside.set(n => n + 1) })}
>
${rows.map(row => x(Row, row))}
</div>
</div>
`;
};
renderApp(App, document.getElementById('app'));