1

In Qwik JS I am trying to create a modal that closes when Esc is pressed. The modal needs to be accessible from anywhere, so I put it in layout.tsx.

layout.tsx

import {component$, Slot, useStore} from '@builder.io/qwik';
import Header from '../components/header/header';
import Modal from '~/components/modal/modal';

export default component$(() => {
  const store = useStore({
    showModal: true,
  });

  return (
    <>
      <main>
        <Header/>
        <section>
          <Slot/>
        </section>
        <button onclick$={() => store.showModal = true}>Show modal</button>
      </main>
      {
        store.showModal
        && (
          <Modal onClose={$(() => {
            store.showModal = false;
          })}>
            foo
          </Modal>
        )
      }
    </>
  );
});

I want to close the modal only when Esc is pressed, so the "keyup" callback first checks if Esc was pressed, then resolves the passed QRL, and then executes it.

modal.tsx

import {component$, QRL, Slot, useVisibleTask$} from '@builder.io/qwik';

export type Props = {
  onClose: QRL<() => void>;
}

export default component$((props: Props) => {
  useVisibleTask$(() => {
    const onKeyUp = async (e: KeyboardEvent) => {
      const key = (e as KeyboardEvent).key || (e as KeyboardEvent).code;
      if (key === 'Escape') {
        const cb = await props.onClose.resolve();
        cb();
      }
    };

    document.addEventListener('keyup', onKeyUp, true);

    return () => {
      document.removeEventListener('keyup', onKeyUp, true);
    };
  }, {
    strategy: 'document-ready',
  });

  return (
    <div class="modal-bg">
      <div class="modal">
        <Slot/>
      </div>
    </div>
  );
});

However, this results in an error:

Error: Code(14): Invoking 'use*()' method outside of invocation context.

How do I get around this problem? How do I execute more than just the QRL callback that is passed?

minitauros
  • 1,920
  • 18
  • 20

1 Answers1

1

The best I found so far is a workaround (or who knows, perhaps this is actually a proper way to do it).

I solved it / worked around it by using a context.

layout.tsx

import {component$, createContextId, Slot, useContextProvider, useStore} from '@builder.io/qwik';
import Header from '../components/header/header';
import Modal from '~/components/modal/modal';

export type ShowModalContextData = {
  showModal: boolean;
};

export const ShowModalContextId = 'my.context';
export const ShowModalContext = createContextId<ShowModalContextData>(ShowModalContextId);

export default component$(() => {
  const store = useStore<ShowModalContextData>({
    showModal: true,
  });

  useContextProvider(ShowModalContext, store);

  return (
    <>
      <main>
        <Header/>
        <section>
          <Slot/>
        </section>
        <button onclick$={() => store.showModal = true}>Foo</button>
      </main>
      {
        store.showModal
        && (
          <Modal>
            foo
          </Modal>
        )
      }
    </>
  );
});

modal.tsx

import {component$, Slot, useContext, useVisibleTask$} from '@builder.io/qwik';
import {ShowModalContext} from '~/routes/layout';

export default component$(() => {
  const showModalCtx = useContext(ShowModalContext);

  useVisibleTask$(() => {
    const onKeyUp = async (e: KeyboardEvent) => {
      const key = (e as KeyboardEvent).key || (e as KeyboardEvent).code;
      if (key === 'Escape') {
        showModalCtx.showModal = false;
      }
    };

    document.addEventListener('keyup', onKeyUp, true);

    return () => {
      document.removeEventListener('keyup', onKeyUp, true);
    };
  }, {
    strategy: 'document-ready',
  });

  return (
    <div class="modal-bg">
      <div class="modal">
        <Slot/>
      </div>
    </div>
  );
});

This completely removes the need of having to pass a callback down to the child component.

document.addEventListener() vs useOnDocument()

Why did I use document.addEventListener() in useVisibleTask$(), and not useOnDocument(), as the docs suggest?

I tried that, but as soon as the modal was closed and reopened, the listeners were gone, making it impossible to close the modal.

minitauros
  • 1,920
  • 18
  • 20