watch-selector
Version:
Runs a function when a selector is added to dom
622 lines • 27 kB
TypeScript
/**
* @module watch-selector/events
*
* # The Unified Event System
*
* This module provides a powerful, unified API for handling DOM and lifecycle
* events. It seamlessly supports both standalone usage (on a specific DOM
* element) and usage within a `watch()` generator context.
*
* ## Key Features
*
* - **Full Type Safety:** Event objects and their `detail` payloads are
* correctly and automatically typed.
* - **Generator-Powered Handlers:** Event handlers can be generators, allowing
* you to `yield` other library functions for complex, asynchronous flows.
* - **Robust Feature Set:** Includes event delegation, debouncing, throttling,
* event filtering, and async generator queuing.
* - **Automatic Cleanup:** All listeners are automatically cleaned up when their
* associated element is removed from the DOM.
* - **Standalone & Generator Compatibility:** The same functions work identically
* everywhere, with or without a `watch()` context.
*/
import type { ElementFn, CleanupFunction, HybridEventOptions, HybridEventHandler, HybridCustomEventHandler, AttributeChange, TextChange, VisibilityChange, ResizeChange, Workflow } from "../types";
export declare function on<El extends Element, K extends keyof HTMLElementEventMap>(element: El, event: K, handler: HybridEventHandler<El, K>, options?: HybridEventOptions): CleanupFunction;
export declare function on<K extends keyof HTMLElementEventMap>(selector: string, event: K, handler: HybridEventHandler<HTMLElement, K>, options?: HybridEventOptions): CleanupFunction | null;
export declare function on<El extends Element, K extends keyof HTMLElementEventMap>(event: K, handler: HybridEventHandler<El, K>, options?: HybridEventOptions): ElementFn<El, CleanupFunction>;
export declare function on<El extends Element, K extends keyof HTMLElementEventMap>(event: K, handler: HybridEventHandler<El, K>, options?: HybridEventOptions): Workflow<CleanupFunction>;
export declare function on<El extends Element, T>(element: El, event: CustomEvent<T>, handler: HybridCustomEventHandler<El, T>, options?: HybridEventOptions): CleanupFunction;
export declare function on<T>(selector: string, event: CustomEvent<T>, handler: HybridCustomEventHandler<HTMLElement, T>, options?: HybridEventOptions): CleanupFunction | null;
export declare function on<El extends Element, T>(event: CustomEvent<T>, handler: HybridCustomEventHandler<El, T>, options?: HybridEventOptions): ElementFn<El, CleanupFunction>;
export declare function on<El extends Element, T>(event: CustomEvent<T>, handler: HybridCustomEventHandler<El, T>, options?: HybridEventOptions): Workflow<CleanupFunction>;
export declare function on<T = any, El extends Element = HTMLElement>(element: El, eventType: string, handler: HybridCustomEventHandler<El, T>, options?: HybridEventOptions): CleanupFunction;
export declare function on<T = any>(selector: string, eventType: string, handler: HybridCustomEventHandler<HTMLElement, T>, options?: HybridEventOptions): CleanupFunction | null;
export declare function on<T = any, El extends Element = HTMLElement>(eventType: string, handler: HybridCustomEventHandler<El, T>, options?: HybridEventOptions): ElementFn<El, CleanupFunction>;
export declare function on<T = any, El extends Element = HTMLElement>(eventType: string, handler: HybridCustomEventHandler<El, T>, options?: HybridEventOptions): Workflow<CleanupFunction>;
/**
* Interface for event shortcut functions that includes the gen property
*/
interface EventShortcutFunction<K extends keyof HTMLElementEventMap> {
<El extends Element>(element: El, handler: HybridEventHandler<El, K>, options?: HybridEventOptions): CleanupFunction;
(selector: string, handler: HybridEventHandler<HTMLElement, K>, options?: HybridEventOptions): CleanupFunction | null;
<El extends Element>(handler: HybridEventHandler<El, K>, options?: HybridEventOptions): ElementFn<El, CleanupFunction>;
gen<El extends Element>(handler: HybridEventHandler<El, K>, options?: HybridEventOptions): Workflow<CleanupFunction>;
}
/**
* Attaches a click event listener using the full dual API pattern.
*
* This is a convenient shortcut for `on('click', ...)` that provides the complete
* dual API functionality. It works with direct elements, CSS selectors, and within
* watch generators, supporting advanced features like debouncing, delegation, and
* queue management.
*
* @param element - HTMLElement to attach listener to (direct API)
* @param selector - CSS selector to find element (selector API)
* @param handler - Event handler function (can be a generator)
* @param options - Advanced event options
* @returns CleanupFunction when used directly, ElementFn when in generator mode
*
* @example Pattern 1: Direct element manipulation
* ```typescript
* import { click } from 'watch-selector';
*
* const button = document.getElementById('my-button');
* const cleanup = click(button, (event) => {
* console.log('Button clicked!', event.target);
* });
*
* // Later, clean up the listener
* cleanup();
* ```
*
* @example Pattern 2: CSS selector manipulation
* ```typescript
* import { click } from 'watch-selector';
*
* // Attach to element found by selector
* click('#submit-button', () => {
* console.log('Submit button clicked!');
* });
*
* // With event options
* click('.once-button', (event) => {
* console.log('This only fires once');
* }, { once: true });
* ```
*
* @example Pattern 3: Traditional generator usage
* ```typescript
* import { watch, click, addClass } from 'watch-selector';
*
* watch('.interactive-button', function* () {
* yield click(function* (event) {
* yield addClass('clicked');
* console.log('Button clicked at:', event.clientX, event.clientY);
* });
* });
* ```
*
* @example Pattern 4: Unified yield* pattern with $ wrapper
* ```typescript
* import { watch, $, click, addClass } from 'watch-selector';
*
* watch('.button', async function* () {
* yield* $(click(async function* (event) {
* yield* $(addClass('processing'));
* await processClick(event);
* yield* $(removeClass('processing'));
* }));
* });
* ```
*
* @example Pattern 5: Pure generator submodule
* ```typescript
* import { watch } from 'watch-selector';
* import { click, addClass } from 'watch-selector/generator';
*
* watch('.button', async function* () {
* yield* click(async function* (event) {
* yield* addClass('active');
* // Handle click...
* });
* });
* ```
*
* @example Advanced options with debouncing
* ```typescript
* import { watch, click, addClass, removeClass } from 'watch-selector';
*
* watch('.rapid-click-btn', function* () {
* yield click(function* (event) {
* yield addClass('processing');
* // Process click
* yield removeClass('processing');
* }, {
* debounce: { wait: 300, leading: true, trailing: false },
* queue: 'latest' // Cancel previous processing
* });
* });
* ```
*
* @example Event delegation for dynamic content
* ```typescript
* import { click } from 'watch-selector';
*
* const container = document.getElementById('dynamic-list');
* click(container, (event, delegatedElement) => {
* console.log('Clicked item:', delegatedElement.textContent);
* }, {
* delegate: '.list-item' // Handle clicks on any .list-item inside container
* });
* ```
*
* watch('.button', function* () {
* yield click(function* (event) {
* yield addClass('clicked');
* yield delay(150);
* yield removeClass('clicked');
* }, {
* debounce: { wait: 300 },
* queue: 'latest'
* });
* });
* ```
*/
export declare const click: EventShortcutFunction<"click">;
/**
* Generator version of click event handler for use with yield*.
*
* @example Generator usage with yield*
* ```typescript
* import { watch, click } from 'watch-selector';
*
* watch('button', function* () {
* yield* click.gen(function* (event) {
* yield* addClass('clicked');
* console.log('Button clicked!');
* });
* });
* ```
*/
/**
* Attaches an input event listener using the dual API pattern.
*
* This is a convenient shortcut for `on('input', ...)` that's particularly useful
* for handling real-time input changes in forms. It supports advanced features
* like debouncing, which is commonly needed for search inputs or live validation.
*
* @param element - HTMLElement to attach listener to (direct API)
* @param handler - Event handler function (can be a generator)
* @param options - Advanced event options including debouncing
* @returns CleanupFunction when used directly, ElementFn when in generator mode
*
* @example Real-time search with debouncing
* ```typescript
* import { watch, input, text } from 'watch-selector';
*
* watch('#search-input', function* () {
* yield input(function* (event) {
* const query = (event.target as HTMLInputElement).value;
* if (query.length > 2) {
* const results = yield* searchAPI(query);
* yield* updateResults(results);
* }
* }, {
* debounce: { wait: 300, trailing: true }
* });
* });
* ```
*
* @example Form validation
* ```typescript
* import { watch, input, addClass, removeClass, attr } from 'watch-selector';
*
* watch('input[required]', function* () {
* yield input(function* (event) {
* const input = event.target as HTMLInputElement;
* const isValid = input.checkValidity();
*
* if (isValid) {
* yield removeClass('invalid');
* yield addClass('valid');
* yield attr('aria-invalid', 'false');
* } else {
* yield removeClass('valid');
* yield addClass('invalid');
* yield attr('aria-invalid', 'true');
* }
* });
* });
* ```
*
* @example Character counter
* ```typescript
* import { watch, input, text } from 'watch-selector';
*
* watch('.search-input', function* () {
* yield input(function* (event) {
* const query = (event.target as HTMLInputElement).value;
* const results = await searchAPI(query);
* yield text(`.results`, `Found ${results.length} results`);
* }, {
* debounce: { wait: 300 }
* });
* });
* ```
*
* @example Form validation
* ```typescript
* import { watch, input, addClass, removeClass } from 'watch-selector';
*
* watch('.email-input', function* () {
* yield input(function* (event) {
* const email = (event.target as HTMLInputElement).value;
* const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
*
* if (isValid) {
* yield addClass('valid');
* yield removeClass('invalid');
* } else {
* yield addClass('invalid');
* yield removeClass('valid');
* }
* });
* });
* ```
*/
export declare const input: EventShortcutFunction<"input">;
/**
* Attaches a change event listener using the dual API pattern.
*
* This is a convenient shortcut for `on('change', ...)` that's ideal for handling
* discrete changes in form elements like selects, checkboxes, and radio buttons.
* Unlike input events, change events fire when the user finishes changing a value.
*
* @param element - HTMLElement to attach listener to (direct API)
* @param handler - Event handler function (can be a generator)
* @param options - Advanced event options
* @returns CleanupFunction when used directly, ElementFn when in generator mode
*
* @example Select dropdown handler
* ```typescript
* import { watch, change, text, addClass, removeClass } from 'watch-selector';
*
* watch('#theme-selector', function* () {
* yield change(function* (event) {
* const select = event.target as HTMLSelectElement;
* const theme = select.value;
*
* // Update UI based on selection
* yield removeClass('theme-light theme-dark theme-auto');
* yield addClass(`theme-${theme}`);
* yield text('#current-theme', `Current theme: ${theme}`);
* });
* });
* ```
*
* @example Checkbox toggle
* ```typescript
* import { watch, change, toggleClass, attr } from 'watch-selector';
*
* watch('#newsletter-checkbox', function* () {
* yield change(function* (event) {
* const checkbox = event.target as HTMLInputElement;
* const isChecked = checkbox.checked;
*
* yield toggleClass('subscribed', isChecked);
* yield attr('aria-pressed', isChecked.toString());
*
* // Update related UI
* const statusText = isChecked ? 'Subscribed' : 'Not subscribed';
* yield text('#subscription-status', statusText);
* });
* });
* ```
*
* @example Radio button group
* ```typescript
* import { watch, change, text, removeClass, addClass } from 'watch-selector';
*
* watch('.category-select', function* () {
* yield change(function* (event) {
* const select = event.target as HTMLSelectElement;
* const category = select.value;
* yield text('.selected-category', `Selected: ${category}`);
* });
* });
* ```
*
* @example Checkbox handling
* ```typescript
* import { watch, change, addClass, removeClass } from 'watch-selector';
*
* watch('.feature-toggle', function* () {
* yield change(function* (event) {
* const checkbox = event.target as HTMLInputElement;
* const container = self();
*
* if (checkbox.checked) {
* yield addClass('feature-enabled');
* yield removeClass('feature-disabled');
* } else {
* yield addClass('feature-disabled');
* yield removeClass('feature-enabled');
* }
* });
* });
* ```
*/
export declare const change: EventShortcutFunction<"change">;
/**
* Attaches a submit event listener using the dual API pattern.
*
* This is a convenient shortcut for `on('submit', ...)` that's specifically designed
* for handling form submissions. It automatically provides access to the form element
* and is commonly used with preventDefault() to handle submissions via JavaScript.
*
* @param element - HTMLFormElement to attach listener to (direct API)
* @param handler - Event handler function (can be a generator)
* @param options - Advanced event options
* @returns CleanupFunction when used directly, ElementFn when in generator mode
*
* @example Form submission with validation
* ```typescript
* import { watch, submit, addClass, removeClass, text } from 'watch-selector';
*
* watch('#contact-form', function* () {
* yield submit(function* (event) {
* event.preventDefault();
*
* const form = event.target as HTMLFormElement;
* const formData = new FormData(form);
*
* // Show loading state
* yield addClass('submitting');
* yield text('#submit-btn', 'Sending...');
*
* try {
* const response = yield* submitForm(formData);
*
* if (response.ok) {
* yield addClass('success');
* yield text('#message', 'Form submitted successfully!');
* form.reset();
* } else {
* yield addClass('error');
* yield text('#message', 'Submission failed. Please try again.');
* }
* } catch (error) {
* yield addClass('error');
* yield text('#message', 'Network error. Please check your connection.');
* } finally {
* yield removeClass('submitting');
* yield text('#submit-btn', 'Submit');
* }
* });
* });
* ```
*
* @example Form validation before submission
* ```typescript
* import { watch, submit, hasClass, text } from 'watch-selector';
*
* watch('form', function* () {
* yield submit(function* (event) {
* const form = event.target as HTMLFormElement;
* const hasErrors = yield hasClass('has-validation-errors');
*
* if (hasErrors) {
* event.preventDefault();
* yield text('.error-message', 'Please fix validation errors before submitting');
* return;
* }
*
* // Allow form to submit normally or handle with AJAX
* yield text('.status', 'Submitting form...');
* });
* });
* ```
*
* @example Multi-step form submission
* ```typescript
* import { watch, submit, getState, setState, addClass, removeClass } from 'watch-selector';
*
* watch('.contact-form', function* () {
* yield submit(function* (event) {
* event.preventDefault();
*
* const form = event.target as HTMLFormElement;
* const formData = new FormData(form);
*
* // Show loading state
* yield addClass('loading');
* yield text('.submit-btn', 'Submitting...');
*
* try {
* const response = await fetch('/api/contact', {
* method: 'POST',
* body: formData
* });
*
* if (response.ok) {
* yield text('.message', 'Form submitted successfully!');
* form.reset();
* } else {
* yield text('.message', 'Submission failed. Please try again.');
* }
* } catch (error) {
* yield text('.message', 'Network error. Please try again.');
* } finally {
* yield removeClass('loading');
* yield text('.submit-btn', 'Submit');
* }
* });
* });
* ```
*
* @example Multi-step form
* ```typescript
* import { watch, submit, getState, setState } from 'watch-selector';
*
* watch('.multi-step-form', function* () {
* setState('currentStep', 1);
*
* yield submit(function* (event) {
* event.preventDefault();
*
* const currentStep = getState<number>('currentStep');
* const form = event.target as HTMLFormElement;
*
* if (currentStep < 3) {
* // Validate current step and advance
* setState('currentStep', currentStep + 1);
* yield showStep(currentStep + 1);
* } else {
* // Final submission
* yield submitForm(form);
* }
* });
* });
* ```
*/
export declare const submit: EventShortcutFunction<"submit">;
/**
* Creates a reusable event behavior that can be yielded within a `watch` generator.
* This is useful for encapsulating complex or repeated event logic.
*
* @example
* const rippleEffect = createEventBehavior('click', function*() {
* yield addClass('ripple');
* yield delay(500);
* yield removeClass('ripple');
* });
*
* watch('.material-button', function*() {
* yield* rippleEffect();
* });
*/
export declare function createEventBehavior<K extends keyof HTMLElementEventMap, T = any>(eventType: K | string, behavior: HybridEventHandler<Element, K> | HybridCustomEventHandler<Element, T>, options?: HybridEventOptions): () => Generator<ElementFn<Element, CleanupFunction>, void, unknown>;
/**
* Composes multiple event handlers into a single handler. The handlers are executed
* in the order they are provided. This is useful for layering multiple pieces of
* logic onto a single event.
*
* @example
* const logClick = (event) => console.log('Clicked!');
* const trackClick = (event) => analytics.track('click');
* const composedHandler = composeEventHandlers(logClick, trackClick);
* yield click(composedHandler);
*/
export declare function composeEventHandlers<K extends keyof HTMLElementEventMap>(...handlers: HybridEventHandler<Element, K>[]): HybridEventHandler<Element, K>;
/**
* A helper for creating a delegated event listener. This is a convenient alternative
* to using the `delegate` option in `on()`.
*
* @example
* // These two are equivalent:
* yield delegate('.list-item', 'click', handler);
* yield on('click', handler, { delegate: '.list-item' });
*
* @param selector The CSS selector for child elements to target.
* @param eventType The name of the event to listen for.
* @param handler The function to call when the event occurs on a matching child.
* @param options Additional event listener options.
*/
export declare function delegate<K extends keyof HTMLElementEventMap, T = any>(selector: string, eventType: K | string, handler: HybridEventHandler<Element, K> | HybridCustomEventHandler<Element, T>, options?: Omit<HybridEventOptions, "delegate">): ElementFn<Element, CleanupFunction>;
/**
* Creates a new `CustomEvent` with full type safety for the `detail` payload.
*
* @param type The name of the custom event.
* @param detail The data payload to include with the event.
* @param options Standard `EventInit` options.
* @returns A new, typed `CustomEvent` instance.
*/
export declare function createCustomEvent<T = any>(type: string, detail: T, options?: EventInit): CustomEvent<T>;
/**
* Dispatches a `CustomEvent` from an element.
*
* @example
* // Inside a generator:
* yield emit('user:action', { action: 'save' });
*
* @example
* // Standalone:
* emit(document.body, 'app:ready');
*/
export declare function emit<El extends Element>(element: El, eventName: string, detail?: any, options?: EventInit): void;
export declare function emit<El extends Element = HTMLElement>(eventName: string, detail?: any, options?: EventInit): ElementFn<El>;
export declare namespace emit {
var gen: <T = any>(eventTypeOrEvent: string | CustomEvent<T>, detail?: T, options?: EventInit) => Workflow<void>;
}
/** Listens for changes to an element's attributes. */
export declare function onAttr(element: Element, handler: (change: AttributeChange & {
element: Element;
}) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: MutationObserverInit): CleanupFunction;
export declare function onAttr(selector: string, handler: (change: AttributeChange & {
element: Element;
}) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: MutationObserverInit): CleanupFunction | null;
export declare function onAttr(handler: (change: AttributeChange & {
element: Element;
}) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: MutationObserverInit): ElementFn<Element, CleanupFunction>;
export declare function onAttr(handler: (change: AttributeChange & {
element: Element;
}) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: MutationObserverInit): Workflow<CleanupFunction>;
/** Listens for changes to an element's `textContent`. */
export declare function onText(element: Element, handler: (change: TextChange & {
element: Element;
}) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: MutationObserverInit): CleanupFunction;
export declare function onText(selector: string, handler: (change: TextChange & {
element: Element;
}) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: MutationObserverInit): CleanupFunction | null;
export declare function onText(handler: (change: TextChange & {
element: Element;
}) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: MutationObserverInit): ElementFn<Element, CleanupFunction>;
export declare function onText(handler: (change: TextChange & {
element: Element;
}) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: MutationObserverInit): Workflow<CleanupFunction>;
/** Listens for when an element becomes visible or hidden in the viewport. */
export declare function onVisible(element: Element, handler: (change: VisibilityChange & {
element: Element;
}) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: IntersectionObserverInit): CleanupFunction;
export declare function onVisible(selector: string, handler: (change: VisibilityChange & {
element: Element;
}) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: IntersectionObserverInit): CleanupFunction | null;
export declare function onVisible(handler: (change: VisibilityChange & {
element: Element;
}) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: IntersectionObserverInit): ElementFn<Element, CleanupFunction>;
export declare function onVisible(handler: (change: VisibilityChange & {
element: Element;
}) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: IntersectionObserverInit): Workflow<CleanupFunction>;
/** Listens for changes to an element's size. */
export declare function onResize(element: Element, handler: (change: ResizeChange & {
element: Element;
}) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: ResizeObserverOptions): CleanupFunction;
export declare function onResize(selector: string, handler: (change: ResizeChange & {
element: Element;
}) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: ResizeObserverOptions): CleanupFunction | null;
export declare function onResize(handler: (change: ResizeChange & {
element: Element;
}) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: ResizeObserverOptions): ElementFn<Element, CleanupFunction>;
export declare function onResize(handler: (change: ResizeChange & {
element: Element;
}) => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>, options?: ResizeObserverOptions): Workflow<CleanupFunction>;
/**
* Triggers a handler immediately when an element is first processed by `watch`.
* This is effectively a "setup" or "initialization" hook.
*/
export declare function onMount<El extends Element>(element: El, handler: () => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>): CleanupFunction;
export declare function onMount(selector: string, handler: () => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>): CleanupFunction | null;
export declare function onMount<El extends Element>(handler: () => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>): ElementFn<El, CleanupFunction>;
export declare function onMount(handler: () => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>): Workflow<CleanupFunction>;
/** @internal Triggers all registered unmount and general cleanup handlers for an element. */
export declare function triggerUnmountHandlers(element: Element): void;
/**
* Registers a handler to be called when an element is removed from the DOM.
* This is the primary "cleanup" hook for releasing resources or finalizing state.
*/
export declare function onUnmount<El extends Element>(element: El, handler: () => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>): CleanupFunction;
export declare function onUnmount(selector: string, handler: () => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>): CleanupFunction | null;
export declare function onUnmount<El extends Element>(handler: () => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>): ElementFn<El, CleanupFunction>;
export declare function onUnmount(handler: () => void | Promise<void> | Generator<any, void, any> | AsyncGenerator<any, void, any>): Workflow<CleanupFunction>;
export {};
//# sourceMappingURL=events.d.ts.map