qwik icon indicating copy to clipboard operation
qwik copied to clipboard

[✨] add useOnElement(el, event, callback)

Open GrandSchtroumpf opened this issue 2 years ago • 2 comments

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

GrandSchtroumpf avatar Apr 13 '23 13:04 GrandSchtroumpf

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);
  });
}

GrandSchtroumpf avatar Apr 13 '23 13:04 GrandSchtroumpf

👍 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.

tleperou avatar Apr 14 '23 08:04 tleperou

  1. Qwik can only place the listener on events which are not yet rendered (otherwise SSR would not work)_
  2. 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

mhevery avatar Oct 26 '23 10:10 mhevery

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>
})

GrandSchtroumpf avatar Oct 26 '23 11:10 GrandSchtroumpf