JavaScript
ReactJS
TypeScript
9 October

Диалоговые окна на Promise

Что такое диалоговое окно?

Википедия говорит следующее:
Диалоговое окно (англ. dialog box) в графическом пользовательском интерфейсе — специальный элемент интерфейса, окно, предназначенное для вывода информации и (или) получения ответа от пользователя. Получил своё название потому, что осуществляет двустороннее взаимодействие компьютер-пользователь («диалог»): сообщая пользователю что-то и ожидая от него ответа.

Нас интересует
ожидая от него ответа
Другими словами мы открываем модальное окно, чтобы получить обратную связь и что-то после этого выполнить. Ничего не напоминает? И я так подумал.

Представим себе ситуацию, у нас есть приложение для управления пользователями.
Сценарий следующий.

На главной странице пользователь может открыть модальное окно списка других пользователей.
Из списка можно открыть модальное окно с информацией о пользователе, так же в этом окне есть форма для отправки письма.

При отправке письма, пользователю открывается модальное окно об успешной отправке.
Когда пользователь закрывает модальное окно, он возвращается в модальное окно списка пользователей, но есть кнопка написать еще письмо, при нажатии пользователь попадает на страницу пользователя.

Читать достаточно сложно представим эту задачу в виде диаграммы последовательности.

image

Теперь все гораздо проще.

С точки зрения кода, открытие модального окна это синхронное действие, мы его можем открыть, внутри себя оно может закрыться, но что если при изменении данных в модальном окне, при его закрытии нужно получить оттуда данные?

Простой пример, из модального окна пользователя, изменяем данные, возвращаясь в модальное окно списка, надо обновить информацию об этом пользователе.

Попахивает асинхронщиной…

Когда мы открываем модалку, нам необходимо дождаться ее закрытия и получить данные которые ввел пользователь. Асинхронные действия очень хорошо реализуются с помощью промисов.

На самом деле в нашей диаграмме уже заложены промисы, просто мы их помечаем как действия. Можно ее немного переделать.

image

Теперь все становится просто, когда пользователь открывает модальное окно, мы дожидаемся пока он не закончит свои дела, после этого вызывается resolve у промиса. Звучит просто, приступим к реализации.

Мой основной фреймворк это реакт, поэтому сразу будем делать на его основе. Для того, чтобы можно было открывать модальные окна из любой части приложения, будем использовать Context API.

Первым делом нам необходимо создать контекст и место, где он будет храниться.

// ./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>
  );
};

Тут все просто, мы используем первый useState для создания массива открытых модальных окон. Нечто похожее на стек.

Второй это useState необходим для того, чтобы складывать туда ссылки на resolve и reject у promise. Это мы увидим ниже.

Перенаправляем рендер через портал, чтобы не приходилось бороться в случае чего с z-index.

Layout это компонент, который будет базовым компонентом для всех модальных окон.

Параметр config это просто объект, где ключ это идентификатор модального окна, а значение это компонент модального окна.

// Пример config.js
export const exampleInstanceName = 'modal/example';

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

Теперь напишем реализацию метода, который будет открывать модальные окна.

Это будет хук:


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 };
};

Хук возвращает функцию open, которую мы можем использовать, чтобы вызвать модальное окно.

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>
}

В данном варианте, мы никогда не дождемся закрытия модального окна, нам необходимо добавить методы для завершения промиса:

// ./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>
  );
};

Теперь когда в компоненте Layout или если он передает эти методы в компонент модального окна, будут вызваны методы success, cancel или close у нас выполнится resolve у необходимого promise. Тут добавляется такое понятие как action, это строка которая отвечает в каком статусе был завершен диалог. Это нам может пригодиться, когда мы будем выполнять какое либо действие после выполнения модального окна:

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>
}

Вот и все. Остается добавить передачу параметров из модального окна и в модальное окно из функции open. Ну думаю с этим вы справитесь сами, а если лень, то есть уже готовый пакет, который вы можете использовать в своих проектах.

+14
4.2k 63
Comments 15
Top of the day