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 🌈
yarn add react-imperial-modal
or
npm install react-imperial-modal
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' });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);
};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', // ..
)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 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:
resolveModaldoes not offer type safety. Use with caution!- the
idarugment ofresolveModal,rejectModal, andcloseModalis 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 modalNo default CSS is provided nor applied.