Dialogs on Promise

What is a dialog box?



Wikipedia says the following:

A dialog box (English dialog box) in a graphical user interface is a special interface element, a window designed to display information and (or) receive a response from the user. It got its name because it performs two-way computer-user interaction (“dialogue”): telling the user something and waiting for a response from him.



We are interested in
waiting for an answer from him
In other words, we open a modal window to get feedback and do something after that. Doesn’t resemble anything? And I thought so.



Imagine a situation, we have an application for user management.

The scenario is as follows.



On the main page, the user can open a modal window for a list of other users.

From the list you can open a modal window with user information, also in this window there is a form for sending letters.



When sending a letter, the user opens a modal window about successful sending.

When the user closes the modal window, he returns to the modal window of the user list, but there is a button to write another letter, when clicked, the user gets to the user’s page.



To read it is quite difficult to imagine this task in the form of a sequence diagram.



image



Now everything is much simpler.



From the point of view of the code, opening a modal window is a synchronous action, we can open it, inside it it can close, but what if changing data in the modal window, when closing it, you need to get data from there?



A simple example, from the modal window of the user, we change the data, returning to the modal window of the list, you need to update the information about this user.



It smacks of asynchronism ...



When we open the module, we need to wait for it to close and get the data that the user entered. Asynchronous actions are very well implemented with promises.



In fact, promises are already laid in our diagram, we just mark them as actions. You can redo it a little.



image



Now everything becomes simple when the user opens a modal window, we wait until he finishes his work, after which resolve is called at the promis. It sounds easy, let's get started.



My main framework is a react, so we’ll immediately do it based on it. In order to be able to open modal windows from any part of the application, we will use the Context API.



First of all, we need to create a context and a place where it will be stored.



// ./Provider.js export const DialogContext = React.createContext(); export const Provider = ({ children, node, Layout, config }) => { const [instances, setInstances] = useState([]); const [events, setEvents] = useState([]); const context = { instances, setInstances, config, events, setEvents }; const Component = instances.map(instance => ( <Layout key={instance.instanceName} component={config[instance.instanceName]} {...instance} /> )); const context = { instances setInstances }; //   state     const child = useMemo(() => React.Children.only(children), [children]); return ( <DialogContext.Provider value={context}> <> {child} {createPortal(Component, node)} </> </DialogContext.Provider> ); };
      
      





Everything is simple here, we use the first useState to create an array of open modal windows. Something like a stack.



The second one, useState, is necessary in order to add references to resolve and reject at promise. This we will see below.



We redirect the render through the portal so that we don’t have to fight if something happens with the z-index.



Layout is a component that will be the base component for all modal windows.



The config parameter is just an object, where the key is the identifier of the modal window, and the value is the component of the modal window.



 //  config.js export const exampleInstanceName = 'modal/example'; export default { [exampleInstanceName]: React.lazy(() => import('./Example')), };
      
      





Now we will write an implementation of a method that will open modal windows.



This will be the hook:



 export const useDialog = () => { const { setEvents, setInstances, config } = useContext(DialogContext); const open = instance => new Promise((resolve, reject) => { if (instance.instanceName in config) { setInstances(prevInstances => [...prevInstances, instance]); setEvents(prevEvents => [...prevEvents, { resolve, reject }]); } else { throw new Error(`${instance['instanceName']} don't exist in modal config`); } }); return { open }; };
      
      





The hook returns an open function that we can use to invoke a modal window.



 import { exampleInstanceName } from './config'; import { useDialog } from './useDialog'; const FillFormButton = () => { const { open } = useDialog(); const fillForm = () => open(exampleInstanceName) return <button onClick={fillForm}>fill form from modal</button> }
      
      





In this option, we will never wait for the modal window to close; we need to add methods to complete the promise:



 // ./Provider.js export const DialogContext = React.createContext(); export const Provider = ({ children, node, Layout, config }) => { const [instances, setInstances] = useState([]); const [events, setEvents] = useState([]); const close = useCallback(() => { const { resolve } = events[events.length - 1]; const resolveParams = { action: actions.close }; setInstances(prevInstances => prevInstances.filter((_, index) => index !== prevInstances.length - 1)); setEvents(prevEvents => prevEvents.filter((_, index) => index !== prevEvents.length - 1)); resolve(resolveParams); }, [events]); const cancel = useCallback((values): void => { const { resolve } = events[events.length - 1]; const resolveParams = { action: actions.cancel, values }; setInstances(prevInstances => prevInstances.filter((_el, index) => index !== prevInstances.length - 1)); setEvents(prevEvents => prevEvents.filter((_el, index) => index !== prevEvents.length - 1)); resolve(resolveParams); }, [events]); const success = useCallback((values) => { const { resolve } = events[events.length - 1]; const resolveParams = { action: actions.success, values }; setInstances(prevInstances => prevInstances.filter((_el, index) => index !== prevInstances.length - 1)); setEvents(prevEvents => prevEvents.filter((_el, index) => index !== prevEvents.length - 1)); resolve(resolveParams); }, [events]); const context = { instances, setInstances, config, events, setEvents }; const Component = instances.map(instance => ( <Layout key={instance.instanceName} component={config[instance.instanceName]} cancel={cancel} success={success} close={close} {...instance} /> )); const context = { instances setInstances }; //   state     const child = useMemo(() => React.Children.only(children), [children]); return ( <DialogContext.Provider value={context}> <> {child} {createPortal(Component, node)} </> </DialogContext.Provider> ); };
      
      





Now, when in the Layout component or if it passes these methods to the modal window component, the success, cancel or close methods will be called and we will have the resolve at the required promise. Here a concept such as action is added, this is a line that answers in what status the dialogue was completed. This can be useful to us when we perform some action after the modal window:



 const { useState } from 'rect'; import { exampleInstanceName } from './config'; import { useDialog } from './useDialog'; const FillFormButton = () => { const [disabled, setDisabled] = useState(false); const { open } = useDialog(); const fillForm = () => open(exampleInstanceName) .then(({ action }) => { if (action === 'success') setDisabled(true); }); return <button onClick={fillForm} disabled={disabled}>fill form from modal</button> }
      
      





That's all. It remains to add the transfer of parameters from the modal window to the modal window from the open function. Well, I think you can handle this yourself, but if you are too lazy, then there is a ready-made package that you can use in your projects.



All Articles