Skip to content

nihlton/react-imperial-modal

Repository files navigation

👑 react-imperial-modal

Imperative API for modals

  const favoriteColor = await openModal(Prompt, {
    title: 'Question',
    message: 'what is your favorite color?',
  });

Often, a interactive branching UI flow is complex enough that a declarative approach becomes too cumbersome and verbose. Imagine an experience where an application prompts a user to confirm before a destructive action, then informs the user of the success or failure of the action. The content of those modals, the result of the actions - all has to go into the state.

Imagine that same page has actions for adding items, editing them etc. It all quickly spirals out of control, with half your state devoted to which modal is open or closed, forget whats inside them. 🤮

Now imagine an imperative API implementation 🌈

LIVE DEMO

Add it to your project

yarn add react-imperial-modal

or

npm install react-imperial-modal

Basic Usage

wrap your app with <ModalProvider>

import React from 'react';
import { createRoot } from 'react-dom/client';
import { ModalProvider } from 'react-imperial-modal'
import App from './App';

const container = document.getElementById('root');

if (container) {
  const root = createRoot(container);
  root.render(
    <ModalProvider>
      <App />
    </ModalProvider>,
  );
}

Define a modal with the ModalProps type

  import type { ModalProps } from 'react-imperial-modal';

  type MyModalProps = { message: string } & ModalProps;

  const MyModal = function (props: MyModalProps) {
    const { message, close } = props;
    return (
      <div>
        <p>{message}</p>
        <button onClick={() => close()}>ok</button>
      </div>
    );
  };

Import the useModal hook

  import { useModal } from 'react-imperial-modal';
  ...
  const openModal = useModal();
  ...
  openModal(MyModal, { message: 'hello' });

Advanced Usage

ModalProps and Promises

The openModal method returns a promise which can be resolved within the modal. The ModalProps type accepts an optional type argument that determines what values the promise will return.

given:

  export interface ModalProps<T = void> {
    modalId: string;
    resolve: (val: T) => void;           // resolve the promise with the given value
    reject: (reason?: unknown) => void;  // rejects the promise with the given value
    close: () => void;                   // closes the modal.  Does not resolve the modal - 
  }                                      //  - any code waiting for the promise will not execute.

you can:

  type PromptProps = { title: string; message: string } & ModalProps<string | void>;

  const Prompt = function (props: PromptProps) {
    const [promptValue, setPromptValue] = useState<string>('');
    const { title, message, resolve } = props;

    return (
      <div>
        <h1>{title}</h1>
        <p>{message}</p>
        <input value={promptValue} onChange={(e) => setPromptValue(e.target.value)} />
        <div>
          <button onClick={() => resolve()}>cancel</button>
          <button onClick={() => resolve(promptValue)}>ok</button>
        </div>
      </div>
    );
  };

then:

  const openModal = useModal();
  ...
  const getUsersColor = () => {
    openModal(Prompt, {
      title: 'Question',
      message: 'what is your favorite color?',
    }).then((favoriteColor) => console.log(favoriteColor));
  };

or

  const openModal = useModal();
  ...
  const getUsersColor = async () => {
    const favoriteColor = await openModal(Prompt, {
      title: 'Question',
      message: 'what is your favorite color?',
    });

    console.log(favoriteColor);
  };

openModal

The openModal method accepts a number of additional arguments.

  <T, P>(
    Component: React.ComponentType<P & ModalProps<T>>,
    componentProps: P,              
    ignoreEscape: boolean = false,  // `true` prevents the `ESC` key from closing the modal
    label?: string,                 // aria attributes 
    labelledby?: string,            // ..
    role: string = 'dialog',        // ..
  )

ModalProvider Config

you can specify class attributes for the <body/> element, as well as the each modal element, and their container

  type ModalProviderConfig = {
    bodyOpenClass?: string;
    modalContainerClass?: string;
    modalClass?: string;
  };
  const modalConfiguration = {
    bodyOpenClass: 'blur';
    modalContainerClass: 'modals';
    modalClass: 'modal';
  }

  <ModalProvider config={modalConfiguration} >
    <App />
  </ModalProvider>

useModalDangerously

useModalDangerously provides additional methods which can be used to control opened modal instances. These methods are offered as an escape hatch and should be used carefully.

  openModal: [...same as `useModal`...]
  closeModal: (id?: string) => void;
  resolveModal: (val: unknown, id?: string) => void;
  rejectModal: (reason?: unknown, id?: string) => void;

Notes:

  • resolveModal does not offer type safety. Use with caution!
  • the id arugment of resolveModal, rejectModal, and closeModal is optional. Ommitting it will operate on the most recently opened modal.
  const [openModal, closeModal, resolveModal, rejectModal ] = useModalDangerously();

  const nameModal = openModal(Prompt, { title: 'Name', message: 'choose a name' });

then:

  resolveModal('wisely', nameModal.id);  // this value cannot be type checked which can introduce runtime errors.
  rejectModal(new Error('did not choose wisely')); // id not provided.  will close the most recent modal
  closeModal(); // id not provided.  will close the most recent modal

CSS and styling

No default CSS is provided nor applied.

About

Imperative API for modals

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors