[✨] add useOnElement(el, event, callback)
Is your feature request related to a problem?
There is a way to listen on the first element of a component with useOn, but there is no way to listen on the event of another element.
I'm creating a Select and a Combobox who both use a Listbox. I would like to bind the keydown event of the controller (button for the select, and input for the combobox) to navigate inside the Listbox and set the aria-activedescendant on the controller.
export const Combobox = component$(() => {
const controller = useSignal();
return <>
<input ref={controller} .../>
<Popover>
<Listbox controller={controller}>
<Slot/>
</Listbox>
</Popover>
</>
});
export const Select = component$(() => {
const controller = useSignal();
return <>
<button ref={controller} ...>...</button>
<Popover>
<Listbox controller={controller}>
<Slot/>
</Listbox>
</Popover>
</>
});
Describe the solution you'd like
I would like to be able to add asynchronous event listener on any HTMLElement or Signal<HTMLElement>, in my example above it would look like that:
interface ListboxProps {
controller?: Signal<HTMLElement | undefined>
}
export const Listbox = component$((props: ListboxProps) => {
const ref = useSignal<HTMLElement>();
// Start listening on the keydown of the controller. If no controller exist listen on ul keydown
useOnElement(props.controller ?? ref, 'keydown', $(event => {
if (event.key === 'ArrowDown') activateNext();
...
}));
...
const tabIndex = props.controller ? undefined : 0;
return <ul ref={ref} tabIndex={tabIndex}>
<Slot/>
</ul>
})
Describe alternatives you've considered
I could addListeners in a synchronous manner. Or I could move the logic into each component using the listbox with utils functions.
Additional context
No response
I created this utils function but I'm not sure this is the right way :
export function useOnElement<K extends keyof GlobalEventHandlersEventMap>(
ref: Signal<HTMLElement | undefined>,
eventName: K,
eventQrl: QRL<(event: GlobalEventHandlersEventMap[K]) => void>
) {
useVisibleTask$(() => {
ref.value?.addEventListener(eventName, eventQrl);
return () => ref.value?.removeEventListener(eventName, eventQrl);
});
}
👍 in here
Having the possibility to get event listeners on other elements than the host is quiet handy.
Alternatively, listeners could be attached to the window then executed when the target is the expected element.
- Qwik can only place the listener on events which are not yet rendered (otherwise SSR would not work)_
- Why can't this be achieved with just basic props spreading?
import { component$, $ } from '@builder.io/qwik';
export default component$(() => {
const event = 'click';
const props = {
['on' + event + '$']: $(() => alert('WORKS')),
};
return (
<>
<button {...props}>click me</button>
</>
);
});
https://stackblitz.com/edit/qwik-starter-o8mpvr
I'm just going to close this issue as I don't even recall why I needed that in the first place.
On note on spreading props though: it's not easy to merge them. Being able to attach some properties & event listener on an element would make things easier I think.
before
export const useRipple() {
return {
class: 'ripple',
onClick$: $(() => ...),
style: `--x: 0; --y: 0;`
}
}
interface IconButtonProps extends QwikJSX.IntrinsicElements['button'] {
icon: string;
}
export const IconButton = component$((props) => {
const rippleProps = useRipple();
const { icon, ...attr } = props;
const localClickHandler = $(() => ...);
return <button
{...attr}
{...rippleProps}
style=[attr.style, rippleProps.style] // <-- I'm not even sure this work if attr.style is an object
class=[attr.class, rippleProps.class]
onClick$=[attr.onClick$, rippleProps.onClick$, localClickHandler]
>
...
</button>
})
after
export const useRipple(ref) {
useOnElement(ref, 'click', $(() => ...));
useSetAttribute(ref, {
class: 'ripple',
style: '--x: 0; --y: 0'
});
}
interface IconButtonProps extends QwikJSX.IntrinsicElements['button'] {
icon: string;
}
export const IconButton = component$((props) => {
const ref = useSignal<HTMLElement>();
useRipple(ref);
useOnElement(ref, 'click', $(() => ...));
return <button {...attr}>
...
</button>
})