Модальные окна, возможно, являются одним из наиболее распространенных шаблонов пользовательского интерфейса при разработке одностраничных приложений, и существует множество способов их реализации. Мы используем React для нашего слоя рендеринга в наших клиентских приложениях Moneyhub, и в этой статье показано, как мы снимали шкуру с этой конкретной кошки.

Эта проблема

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

Как вы понимаете, каждый из этих потоков должен держать моего пользователя IFA в курсе, давая им обратную связь и предлагая им вводить данные по мере их продвижения. Идея не столько в том, чтобы выполнять каждую задачу, сколько в том, чтобы побудить их сделать простой и интуитивно понятный выбор. Помните, что мы говорим о корпоративном пользователе, поэтому у него, вероятно, есть много клиентов, которыми они хотят управлять. Очень важно максимально упростить их работу.

Задания

Как только мы определили задачи, которые необходимо было выполнить, например подключаясь к ISA-провайдеру или получая данные клиентской учетной записи, нам было необходимо решение, которое давало бы пользователю правильный результат по мере его продвижения. Мы решили смоделировать простой конечный автомат, заключив его логику в компонент React. В нашем случае этот компонент известен как ‹Task /›.

Каждая задача очень проста, для ее обхода требуется массив экранов, а также идентификатор текущего отображаемого экрана (статуса). Это можно установить, просто используя следующий псевдокод.

Затем мы заключаем саму задачу в компонент ‹TaskProvider /›, который будет связывать события для продвижения пользователя по экранам.

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

Это действительно полезно, поскольку это означает, что мы начали разделять наши компоненты между теми, которые отвечают за организацию данных и оркестровку обратных вызовов и действий (Контейнеры), и теми, которые должны представлять контент пользователю (Презентационные). Вы можете прочитать более подробное объяснение этого здесь: https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0

Теперь настоящая проблема

Однако наличие шаблона для решения проблемы пути пользователя к задаче - это только часть решения. Мы быстро поняли, что наша реализация должна учитывать анимацию между экранами и, что более важно, визуализацию этих экранов как часть модального шаблона. Это означало, что теперь нам нужно было решить две дополнительные проблемы:

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

- Рендеринг каждой задачи вне основного приложения

Первая часть была довольно простой. Мы заранее знали, какие типы экранов нам нужны, они были разделены на две группы: первая будет принимать пользовательский ввод, а вторая будет информировать пользователя о ходе выполнения задачи. Поскольку различия между двумя группами были настолько малы, мы решили абстрагировать большую часть основных функций для данного экрана в основной компонент Presentational ‹Modal /›, который будет выполнять для нас все общие задачи, например позиционирование, отмена задачи и анимация между экранами.

Итак, исходя из этого архитектурного разделения компонентов, наш компонент ‹TaskProvider /› явно является компонентом-контейнером. Его единственная цель - организовать отображение экранов под конкретную задачу.

Поскольку наша презентация абстрагируется от модального компонента, наши экраны затем берут на себя роль компонентов контейнера. Это очень простые компоненты без сохранения состояния, которые имеют общий API. Они занимаются только делегированием обработчиков и данных в наши модальные окна, которые, в свою очередь, несут ответственность за захват пользовательского ввода (когда пользовательский ввод необходим) и отображение сообщения пользователю. Хорошим примером может служить экран, предлагающий пользователю выполнить одно из двух действий. Мы решили назвать это компонентом ‹Подсказка /›. Вот его API:

Вы можете видеть, что фактическое позиционирование и управление потоком задач не имеют никакого отношения к компонентам экрана. ‹Modal /›, абстракцией которого является ‹Prompt /›, связан с позиционированием и т. Д. В основном компонент ‹Modal /› будет выглядеть примерно так:

Я собрал рабочий пример, чтобы показать, как контейнер и презентационные компоненты работают вместе: http://codepen.io/jeanpaulgorman/pen/RpPjVJ

Становится ясно, что фактическое содержимое экранов задач может быть любым, каким вы хотите, а это означает, что вы можете отобразить задачу в приложении в любой момент, когда вам нужно. Это приводит к последней проблеме: поскольку мы используем модальное окно для рендеринга наших экранов, мы действительно не хотим полагаться на свойство css `position: fixed`, чтобы переместить модальное окно в верхнюю часть порядка стекирования DOM. На самом деле мы должны продолжать использовать контейнер / шаблон представления, чтобы мы могли отображать модальное окно вне основного приложения, даже если оно вызывается глубоко внутри основного приложения.

Это не только даст нам более чистую и простую структуру DOM, но и позволит нам передавать наши обработчики в Modal из основного приложения без необходимости передавать эти обработчики из приложения.

Короче говоря, нам нужно следующее:

дает нам:

Этот тип перемещения контента или, скорее, транспортировка того места, где он отображается, известен как портал в сообществе React. Посмотрев на код, вы поймете, почему. Как разработчик вы определяете свой контент и помещаете его в свой ‹Modal /›, и результат переносится через портал для отображения в другом месте.

Так как же на самом деле будет работать портал? Во-первых, у него не должно быть шаблона, это должен быть простой компонент-оболочка или компонент более высокого порядка, который принимает дочерние элементы в качестве основных свойств. Если вы не знакомы с компонентами высокого порядка (HOC), вы можете прочитать о них подробнее здесь https://facebook.github.io/react/docs/higher-order-components.html, однако основной принцип - что они являются функцией, которая принимает компонент как минимум в качестве одного аргумента и возвращает новый компонент, который обертывает первый, улучшая его поведение. Вот простой пример HOC, который регистрирует новые реквизиты по мере их поступления в компонент, например

Из этого примера видно, что наш компонент более высокого порядка добавляет только минимальную функциональность нашему обернутому компоненту, используя методы жизненного цикла React. Это то, что довольно часто встречается для компонентов более высокого порядка, и что-то, что мы будем использовать сейчас, чтобы прикрепить наш модальный компонент к правильному узлу DOM.

Начнем с создания HOC и присвоения ему имени.

Большой! Теперь, когда у нас есть основная структура, мы знаем, как создавать новые компоненты, которые будут добавлены к документу.

```
const Portal = createPortal(Modal)
```

Тогда мы будем использовать следующее, и мы можем передать любые свойства этому компоненту, зная, что они найдут свой путь к нашему Wrapped Component, в данном случае Modal:

```
<Portal />
```

Теперь нам нужно заставить наш HOC фактически выполнить задачу загрузки нашего обернутого компонента в выбранную нами папку DOM. Для этого воспользуемся методом ReactDOM.render. Этот метод позволяет прикрепить наш компонент к заданному узлу DOM, и если этот компонент затем будет повторно отрисован, он выполнит различие React и теневой отрисовку DOM, чего мы ожидаем от любого другого компонента React. Давайте добавим это.

Здесь вы можете увидеть рабочий пример: http://codepen.io/jeanpaulgorman/pen/MpwqmE

Если вы откроете инструменты разработчика, вы увидите, что модальное окно рендерится в узел DOM `append-to-container`, полностью отделенный от основного приложения. Это замечательно, так как теперь мы можем свободно стилизовать этот узел DOM по своему усмотрению, без необходимости иметь дело со спецификой CSS или полагаться на `position: fixed`, чтобы подтолкнуть наш Modal к вершине стека.

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

Во-вторых, в настоящее время мы можем визуализировать только один компонент за раз. Это нормально для многих ситуаций, но вскоре станет неуправляемым в производственном SPA. На самом деле нам нужен реестр компонентов, которые мы хотим отображать вне основного приложения, и мы хотели бы сопоставить каждый из них с правильным узлом DOM для загрузки.

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

Помня об этом, нам нужно разделить этот код на три отдельные части:

1. Способы обработки согласования DOM.

2. Реестр компонентов для рендеринга.

3. Методы, которые будут перебирать наш реестр и отображать каждый компонент в правильный узел DOM.

К счастью, мы уже сделали это за вас. Вы можете проверить исходный код здесь и запустить ДЕМО: https://github.com/jpgorman/react-append-to-body

Если вам понравилось читать этот пост, подпишитесь на нас в Twitter @MoneyhubEnterpr и LinkedIn или перейдите на наш сайт https://www.moneyhubenterprise.com/, чтобы быть в курсе наших новостей и мнений.

Автор: Жан-Поль Горман (@_jpgorman), инженер-программист Moneyhub Enterprise