Skip to content

Instantly share code, notes, and snippets.

@sdjnes
Last active June 20, 2023 09:33
Show Gist options
  • Save sdjnes/3a1ce8b48d54cc39b49e8775266c7969 to your computer and use it in GitHub Desktop.
Save sdjnes/3a1ce8b48d54cc39b49e8775266c7969 to your computer and use it in GitHub Desktop.
Modal system idea
// To use it, you simply define a Modal and then render it
type MyComponentProps = {
description: string;
links: Array<{ href: string, label: string}>
}
const MyComponent = (props: MyComponentProps) => ...
const Page = () => {
// This returns an object with a Component, and 3 functions: toggle, open, close
const myModal = useModal<MyComponentProps>(
MyComponent,
"an-id-for-this-modal",
() => { console.log('onOpen callback') },
() => { console.log('onClose callback') }
);
// To open a modal, you trigger its `open` function
useEffect(() => {
myModal.open()
});
// We always render the Modal out to the tree, but whether it shows something is determined by the open/close
// The props of myModal.Component align with the props of MyComponent (the thing you are rendering in the modal)
return <>
<myModal.Component description="This is my modal" links={[ { href: '/', label: 'Link one' ]} />
</>
}
// This is where we implement how the stack works, pop/push
const Wrapper = () => {
const [modalStack, setModalStack] = useState<Array<string>>([]);
return <ModalStackContext.Provider
value={{
stack: modalStack,
push: (modalId: string) => {
setModalStack((modals) => [...modals, modalId]);
},
pop: () => {
let newStack = [...modalStack];
const poppedItem = newStack.pop();
setModalStack(newStack);
return poppedItem;
},
}}
>
<Page />
</ModalStackContext.Provider>
}
// This is a specific type of Stack for modals
import { useContext } from "react";
import { createStackContext } from "./StackContext";
type ModalStackItem = string;
const ModalStackContext = createStackContext<ModalStackItem>();
const useModalStack = () => {
const context = useContext(ModalStackContext);
return context;
};
export { ModalStackContext, useModalStack };
// Using a stack means we can open modals one on top of another if needed
// A stack needs to implement a push and a pop
import { createContext } from "react";
type Props<T> = {
stack: Array<T>;
push: (modal: T) => void;
pop: () => T | undefined;
};
function createStackContext<T>() {
return createContext<Props<T>>({
stack: [],
push: (x: T) => {},
pop: () => undefined
});
}
export { createStackContext };
// This hook returns the open/close for a specific modal, and the component to render
import { useCallback, useMemo, useState, type FC } from "react";
import ReactDOM from "react-dom";
import { Modal } from "~/components";
import { useModalStack } from "~/contexts";
function useModal<T>(
ModalContent: FC<T>,
id: string,
onOpen?: () => void,
onClose?: () => void
): {
Component: FC<T & { title: string }>;
toggle: (instanceProps?: Partial<T>) => void;
open: (instanceProps?: Partial<T>) => void;
close: () => void;
} {
const modalStack = useModalStack();
const [instanceProps, setInstanceProps] = useState<Partial<T>>();
const isInStack = useMemo(
() => modalStack.stack.includes(id),
[modalStack, id]
);
const open = useCallback(
(instanceProps?: Partial<T>) => {
modalStack.push(id);
setInstanceProps(instanceProps);
onOpen?.();
},
[id, modalStack, onOpen]
);
const close = useCallback(() => {
modalStack.pop();
onClose?.();
}, [modalStack, onClose]);
const toggle = useCallback(
(instanceProps?: Partial<T>) => {
isInStack ? close() : open(instanceProps);
},
[close, isInStack, open]
);
const Component = useCallback(
(props: T & { title: string }) =>
isInStack
? ReactDOM.createPortal(
// <Modal> is a wrapper that centers a white box on a darkened background
<Modal
title={props.title}
onClose={() => {
close();
}}
>
<ModalContent {...props} {...instanceProps} />
</Modal>,
document.getElementById("modal-container")!
)
: null,
[ModalContent, close, instanceProps, isInStack]
);
return useMemo(
() => ({
Component,
toggle,
open,
close,
}),
[Component, toggle, open, close]
);
}
export { useModal };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment