Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 13 additions & 35 deletions packages/core/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,75 +10,53 @@ import { connected } from './lifecycle';
*
* @param eventName - The name of the event to listen for (e.g., 'click', 'focus', 'custom-event')
* @param selector - CSS selector to match elements against
* @param callback - Function to invoke when the event occurs. Receives the event and matching element.
* @param callback - Function to invoke when the event occurs. Receives the event.
* @param options - Optional event listener options (capture, once, passive, etc.)
* @returns A cleanup function that removes all event listeners and stops observing
*
* @example
* ```ts
* // Listen for clicks on all buttons with inferred types
* on('click', 'button', (event, element) => {
* console.log('Button clicked:', element);
* on('click', 'button', (event) => {
* console.log('Button clicked:', event.currentTarget);
* });
*
* // Use event listener options
* on('click', '.once-button', (event, element) => {
* on('click', '.once-button', (event) => {
* console.log('Clicked once');
* }, { once: true });
*
* // Manual cleanup
* const stop = on('click', 'button', (event, element) => {
* const stop = on('click', 'button', (event) => {
* console.log('Clicked');
* });
* stop();
* ```
*/
// Tag name selector with inferred event type: on('click', 'button', ...)
export function on<K extends keyof HTMLElementTagNameMap, E extends keyof HTMLElementEventMap>(
export function on<E extends keyof HTMLElementEventMap>(
eventName: E,
selector: K,
callback: (event: HTMLElementEventMap[E], element: HTMLElementTagNameMap[K]) => void,
options?: boolean | AddEventListenerOptions
): () => void;
// Tag name selector with custom event type: on<CustomEvent, 'form'>('ajax', 'form', ...)
export function on<Ev extends Event, K extends keyof HTMLElementTagNameMap>(
eventName: string,
selector: K,
callback: (event: Ev, element: HTMLElementTagNameMap[K]) => void,
options?: boolean | AddEventListenerOptions
): () => void;
// Element type with native event: on<HTMLButtonElement>('click', '.btn', ...) - event inferred from eventName
export function on<T extends HTMLElement>(
eventName: keyof HTMLElementEventMap,
selector: string,
callback: (event: Event, element: T) => void,
callback: (event: HTMLElementEventMap[E]) => void,
options?: boolean | AddEventListenerOptions
): () => void;
// Custom selector with native event and element type: on<'click', HTMLButtonElement>('click', '.btn', ...)
export function on<E extends keyof HTMLElementEventMap, T extends Element = Element>(
eventName: E,
selector: string,
callback: (event: HTMLElementEventMap[E], element: T) => void,
options?: boolean | AddEventListenerOptions
): () => void;
// Custom selector with custom event type: on<CustomEvent>('ajax', '.form', ...)
export function on<Ev extends Event, T extends Element = Element>(
// Tag name selector with custom event type: on<CustomEvent, 'form'>('ajax', 'form', ...)
export function on<Ev extends Event>(
eventName: string,
selector: string,
callback: (event: Ev, element: T) => void,
callback: (event: Ev) => void,
options?: boolean | AddEventListenerOptions
): () => void;
export function on(
eventName: string,
selector: string,
callback: (event: Event, element: Element) => void,
callback: (event: Event) => void,
options?: boolean | AddEventListenerOptions
): () => void {
return connected(selector, (element) => {
const handler = (event: Event) => callback(event, element);
element.addEventListener(eventName, handler, options);
element.addEventListener(eventName, callback, options);
return () => {
element.removeEventListener(eventName, handler, options);
element.removeEventListener(eventName, callback, options);
};
});
}
Expand Down
10 changes: 9 additions & 1 deletion packages/core/test/events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ describe('on', () => {
root.click();
expect(callback.calledOnce).to.be.true;
expect(callback.firstCall.args[0]).to.be.instanceOf(Event);
expect(callback.firstCall.args[1]).to.eq(root);
});

it('cleans up the event listener manually', async () => {
Expand Down Expand Up @@ -51,6 +50,15 @@ describe('on', () => {
root.click();
expect(callback.callCount).to.eq(1);
});

it('supports custom events', async () => {
const callback = Sinon.spy();
const root = await fixture<HTMLButtonElement>(html`<button id="selector"></button>`);
on<CustomEvent<{ foo: string }>>('ajax:success', '#selector', callback);

emit<{ foo: string }>(root, 'ajax:success', { detail: { foo: 'bar' } });
expect(callback.calledOnce).to.be.true;
});
});

describe('emit', () => {
Expand Down
19 changes: 11 additions & 8 deletions packages/docs/utilities/on.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,37 @@ The `on` function sets up event listeners that automatically attach to elements

## Usage

This function observes the DOM and automatically adds event listeners to elements matching the provided CSS selector. Event listeners are added to existing elements immediately and to new elements as they're added to the DOM. When elements are removed or no longer match the selector, their event listeners are automatically cleaned up.
This function observes the DOM and automatically adds event listeners to elements matching the provided CSS selector.
Event listeners are added to existing elements immediately and to new elements as they're added to the DOM. When
elements are removed or no longer match the selector, their event listeners are automatically cleaned up.

```ts
import { on } from '@ambiki/impulse';

// Listen for clicks on all buttons
on('click', 'button', (event, element) => {
console.log('Button clicked: ', element);
on('click', 'button', (event) => {
console.log('Button clicked: ', event.currentTarget);
});
```

## Event listener options

You can pass standard event listener [options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options) to customize the behavior:
You can pass standard event listener [options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options)
to customize the behavior:

```ts{4,9,14}
// Fire the event listener only once
on('click', '.once-button', (event, element) => {
on('click', '.once-button', (event) => {
console.log('Clicked once');
}, { once: true });

// Use capture phase
on('focus', 'input', (event, element) => {
on('focus', 'input', (event) => {
console.log('Input focused');
}, { capture: true });

// Mark as passive for better scroll performance
on('touchstart', '.slider', (event, element) => {
on('touchstart', '.slider', (event) => {
handleTouch(event);
}, { passive: true });
```
Expand All @@ -41,7 +44,7 @@ on('touchstart', '.slider', (event, element) => {
The `on` function returns a cleanup function that removes all event listeners and stops observing when called.

```ts{1,6}
const stop = on('click', 'button', (event, element) => {
const stop = on('click', 'button', (event) => {
console.log('Clicked');
});

Expand Down