How to create an efficient Modal component in React using Hooks and Portals
Modal is a common UX element. A modal is a dialog box/popup window that is displayed on top of the current page. You must have used pop-ups and notifications for your website. For some people pop-ups are really annoying if it doesn’t behave as it should. It must have good UI/UX.
In this article we’re going to create an efficient Modal component from scratch without using any library.
Our main goal is to create an efficient modal which,
- Has a good layout
- Doesn’t break ui when we use overflow in parent component
- Can render content Dynamically
- Clean and Elegant animation
- Looks good (good UI)
- Have more control for User (like clicking outside can close modal) etc.
Let’s get started!
If you prefer video format then you can watch this video
Create your react-app using,
npx create-react-app react-modal
For this tutorial i’m going to use Sass so make sure you have installed node-sass package. To install it do,
npm install node-sass
Let’s create Basic Modal component
open App.js file.
clean unnecessary code.
Now create one button in the App.js file to open and close modal just like this
Exit fullscreen mode
Create one state to open and close modal.
Write below code:
Line 6: Contains modal state which is false initially.
Line 7: A Toggle method to toggle modal state from false to
true and vice-versa.
Line 11: Make sure to connect Toggle() method to onClick of
Next create Modal.js file and Write below code:
Exit fullscreen mode
Now import Modal in the App.js file.
Line 17: Here we have imported Modal component. And passed
modal state as show in the props.
Now open Modal.js and write below code
Line 3: Deconstruct Show from the props.
Line 7: We will display modal only when show state is true.
Line 9 to 30: This is the whole Modal layout.
- ModalContainer div contains the modal
- In the modal div, There is one header which contains modal title and close Button (You can use any icon for close button).
- Main tag contains content of the modal.
- Footer has 2 buttons one is submit and another is cancel.
Now when you press a button modal will show and on pressing again it will close the modal.
First Let’s add some styling to our modal.
Create Modal.scss file and import it in the Modal.js file.
Copy and paste this styling in the Modal.scss file.
This will give your modal a better look.
Line 21: By applying this backdrop-filter you can make it
look like frost-glass.
Let’s add Close event in modal
In the App.js file pass toggle method as a props in the modal just like this
Exit fullscreen mode
open Modal.js file and deconstruct close from the props.
Line 3: Deconstruct Close from the props.
We have added close method in 3 places:
Line 16: At the close button.
Line 22: At the cancel button.
Line 11: We have also added close method here too. Why? because whenever user clicks outside it should close the modal. So here, when user clicks on the modalContainer it closes the modal.
Line 13: here we have to stop this click events in the modal else it will close it so for that we have used e.stopPropagation().
hint: You can also add event Listener and add functionality
in which when user clicks esc key, It closes the
modal. (It is good for user experience)
Let’s use Portals to render Modal component
- Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.
- Sometimes when we use overflow or z-index properties in the parent component then we need child component (like modal or dialogues) to break container visually, and portals can be very handy to do that because it renders children outside of DOM hierarchy.
ReactDOM.createPortal
(
element,
DOM node where you want to render this element
)
So let’s implement portals in our Modal component.
To use portal we have to create one more element in the dom.
Generally our whole App renders in the div with the id root.
Open index.html file.
and above the root div create one more div with the id modal.
Just like this
Exit fullscreen mode
Open Modal.js file and edit it just like this,
Line 1: Import ReactDom.
Line 6: After return create portal using ReactDom.createPortal , As it’s first argument we have passed whole modal component and for the second argument we have passed the dom node where we want to render it.
Line 34: We want to render our component in the div with id modal.
Let’s make Modal content Dynamic:
open App.js file and pass title as a props and content inside the component as shown below,
Exit fullscreen mode
Here we have passed title as props and modal content as the children.
Open Modal.js and write,
(Final Modal.js code)
Line 5: Deconstruct title and children from the props.
Line 17: Insert title in the curly braces.
Line 22: Render children using the curly braces.
Now if you want to add a little animation in the modal you can watch the video or you can go to the git repository and read code.
Create an Accessible and Reusable React Modal (TypeScript)
Have you ever used a modal where you cannot interact with the modal using your keyboard? I don't know about you, but I find it annoying. I do not want to use my mouse every time I want to close a modal.
In this tutorial, I'm going to show you how to build an accessible, but also a reusable and responsive React modal using TypeScript and styled-components. We are going to follow the WAI-ARIA Practices set by W3C to make the modal accessible.
By the end of this tutorial, we will have a modal like this.
If you're in a hurry and just want to see the code , here's the stackblitz link.
Prerequisites
Besides TypeScript, I assume you are familiar with styled-components. It is just another way of styling React components in a declarative way. In case you are not familiar, I recommend you to first check the basics in the docs before continuing with this tutorial.
I also assume you already know React and hooks. If you are not familiar with TypeScript, don't worry, you can still follow this tutorial with your JavaScript knowledge.
Why create your own modal
There are already many libraries out there that can be use to create a responsive, accessible modal in React. However, sometimes, you have requirements in your design that cannot be fully met by those libraries. Sometimes customizing the library to fit your need is difficult.
In such a case, you might want to create your own modal, but still follow the standards that are already in place.
My suggestion is that if a library can meet your needs, then just use that library; otherwise, create your own modal. The reason is that making your modal fully accessible is difficult. You may not want to go through all the hurdles.
React-modal is a popular library you can start with.
Creating the modal component
Here is the actual modal component. It is pretty much self-explanatory. We have a functional component that receives ModalProps described in the interface. Through the props, we could set the title and content of our modal dynamically. We can determine whether our modal is open and we can also close it programatically.
Our HTML markup is created with styled-components imported from the modal.style.tsx file. Here is how our styles look like:
The interesting part of our modal is in the return statement.
What is createPortal and why do we need it?
createProtal
createPortal is part of the ReactDOM API that allows us to render a React component outside the parent component. We usually render the React app in the root div element, but by using portals, we can also render a component outside the root div.
We need portals in our modal because we only want to include the modal in the DOM when it is rendered. Having the modal outside the parent container also helps us avoid conflicting z-index with other components.
createPortal accepts two arguments: the first is the component you want to render, and the second is the location in the DOM where you want to render the component.
In our example, we are rendering the modal at the end of the body of the html ( document.body ) if the modal is open. If not, then we hide it by returning null .
Using the modal
To use our modal, we are going to create a custom React hook that will manage the state of the modal. We can use the custom hook in any component where we want to render our modal.
Inside our App component, we could render our modal like this.
We use the isShown state and toogle function from the custom hook to show and hide the modal. At the moment, we are only showing a simple statement in our modal, which isn't very helpful.
Let us try to create a more specific kind of modal, a confirmation modal. In your app you may need several types of modal, like a confirmation modal, a success or error modal, or even a modal with a form in it. To customize our modal depending on the type of modal we need, we can create a component and pass it as a content to our modal props.
Here is the content of our confirmation modal.
This is a just simple component asking for a confirmation to delete an element, and the props are the actions we execute when the user clicks yes or no, and the message to display.
Now we could pass this confirmation component to our modal in App component.
This is the modal that we get.
Making the modal accessible
An accessible website is a website that can be used by as many people as possible regardless of their disability. "The Web must be accessible to provide equal access and equal opportunity to people with diverse abilities."
If you try to run the code we have so far, you will notice that it is not so pleasant to use (at least for me ). When you click outside the modal, it will still be open. We cannot also use Esc key to close modal. Let us try to fix those small details in this section.
WAI-ARIA gives us guidelines on how to make a modal (or dialog as it is also called) accessible.
- the element that will be our modal container needs to have role of dialog
- the modal container needs to have aria-modal set to true
- the modal container needs to have either aria-labelledby or aria-label
- clicking outside the modal (or backdrop) will close the modal
- keyboard interaction where:
- Esc key closes the modal
- pressing Shift moves the focus to the next tabbable element inside the modal
- pressing Shift + Tab moves the focus to the previous tabbable element
Let us see how we can implement them in our modal.
HTML attributes for accessible modal
I have highlighted the changes that we have added to our modal. First, for the backdrop, we have added an onClick event so that when it is clicked, the modal will be close.
Next, we have added the attributes aria-modal , aria-labelledby , tabIndex , and role to the wrapper or container of our modal, just as specified by WAI-ARIA
The tabIndex attribute allows us to set the order of elements to be focused when pressing the tab key. We set it to -1 because we don't want the modal itself to be focused. Instead, we want the elements inside the modal to be focused when traversing the elements.
So, in our checklist above, we have accomplished the following:
- the element that will be our modal container needs to have role of dialog
- the modal container needs to have aria-modal set to true
- the modal container needs to have either aria-labelledby or aria-label
- clicking outside the modal (or backdrop) will close the modal
Now let us see how to add keyboard interaction with our modal.
Adding keyboard interaction
To allow user to close the modal when pressing ESC key, we need to add an event key listener to our modal. When ESC key is pressed and the modal is shown, our function to hide the modal will be executed. We are going to use useEffect hook to achieve this.
Notice that we are removing the event listener in the return function of the useEffect hook in order to avoid memory leaks. The return function is executed when the component (modal) unmounts.
- keyboard interaction where:
- Esc key closes the modal
- pressing Shift moves the focus to the next tabbable element inside the modal
- pressing Shift + Tab moves the focus to the previous tabbable element
So, this is also checked. By the way, the Shift and Shift + Tab functionality is also already working, we can also tick it off.
Disable scrolling
One of our ARIA requirements is to not allow the user to interact with elements outside the modal, such as scrolling.
To disable scrolling, we are also going to add some code to our useEffect hook.
When the modal isShown , we set the overflow style property of the body of the page to hidden to hide the scrollbar. To test this, we are going to later add some dummy text to our App component until it overflows, and see if hiding the scroll works when the modal is shown.
- when open, interaction outside the modal should not be possible, such as scrolling
Focus trap
The last item in our checklist is to trap the focus inside the modal. We can traverse our elements inside the modal by clicking Shift or Shift + Tab . When we reach the last tabbable element, if we press Shift, the focus will move to an element outside the modal.
But that is not what we want. What we want is when we reach the last tabbable element and we keep traversing with the Shift key, the focus will go to the first tabbable element. It like a loop. Once we reach the end of the loop, we start from the beginning.
We can try to implement this functionality by getting all the focusable elements in our modal, and then loop through them to trap the focus, but since someone has already done this functionality before, we are just going to use an npm package called react-focus-lock .
After installing the package, we can wrap our modal component with <FocusLock> component provided by the library.
Now when the modal is open, our focus after pressing Shift will only be inside the modal.
- focus should be trapped inside the modal
Wow! Now we have a fully functioning modal with accessible features. Congrats .
Conclusion
You can test all the functionalities we have implemented in this stackblitz link. I have added dummy text to the App component so that the content overflows and you can test if the scroll is disabled when the modal is shown. Don't be afraid to play around with it and customize it according to your want.
If you have liked this post or it has helped you, kindly please share it
React best practice to create modal window
How to create a modal window using React? By modal window I mean a frame which overlays with the content of a page. I want it to open up using a button on the webpage, be able to close it by clicking outside area and it will contain a form which user will fill and submit.
Does React have some production ready library for this task, or should I implement it using css? Also in the case of container/representational component pattern, should I nest the modal component inside the container component of modal opening component or inside the modal opening component itself?
2 Answers 2
1 — If you want to write a generic modal component in React, the only thing that your react modal component should do is to give a consistent view such as containing an overlay, setting positioning values and all common behaviour etc. The interaction decisions depends on how you proceed along the application. For example, in this kind of approach I mention as (1), fits better with using Flux. Basically you have a store, and collect all component state related data in it, and the ui states are managed by props passed to your component instead of calling setState in order to change the state of your component. In addition, this gives you the ability to change the component’s state outside of the component. Let me clear myself with an example:
And a style file like :
And you can use it as :
This modal design only gives the proper view and basic controls of a modal, and configuration is all up to your usages. If your application logic pretty much similar in other usages of modals, then you can add them into the modal component yourself.
Модальные окна на Angular, Angular 2 и ReactJS
В этой статье мы рассмотрим, как создавать всплывающие и перекрывающие элементы на React, Angular 1.5 и Angular 2. Реализуем создание и показ модального окна на каждом из фреймворков. Весь код написан на typescript. Исходный код примеров доступен на github.
Введение
Что такое «всплывающие и перекрывающие» элементы? Это DOM элементы, которые показываются поверх основного содержимого документа.
Это различные всплывающие окна (в том числе модальные), выпадающие списки и менюшки, панельки для выбора даты и так далее.
Как правило, для таких элементов применяют абсолютное позиционирование в координатах окна (для модальных окон) при помощи position: fixed , или абсолютное позиционирование в координатах документа — для меню, выпадающих списков, которые должны располагаться возле своих «родительских» элементов, — при помощи position: absolute .
Простое размещение всплывающих элементов возле «родителей» и скрытие/отображение их не работают полностью. Причина — это родительские контейнеры с overflow , отличным от visible (и fixed ). Все, что выступает за границы контейнера, будет обрезано. Также такие элементы могут перекрываться элементами ниже по дереву, и z-index не всегда тут поможет, так как работает только в пределах одного контекста наложения.
По-хорошему, эту проблему элегантно мог бы решить Shadow DOM (и то, не факт), но пока он не готов. Могло бы помочь CSS свойство, запрещающее обрезку и перекрытие, либо позиционирование относительно документа (а не родителя), но его нет. Поэтому используем костыль — DOM элементы для всплывающих компонентов помещаем в body , ну или, на худой конец, поближе к нему, в специальный контейнер, у родителей которого заведомо нет «обрезающих» стилей.
Замечание: мы не будем рассматривать позиционирование элементов с разными координатами и родителями — это отдельная сложная тема, полная костылями, javascript-ом и обработкой неожиданных событий браузера.
Например, ни один из существующих способов не решает идеально проблему позиционирования, если нам нужно сделать компонент типа select или autocomplete с выпадающим списком возле элемента.
Использование position: fixed , по-видимому, позволяет избежать обрезки родительским контейнером, но вынуждает обрабатывать скроллинг документа и контейнера (проще всего тупо закрывать выпадающий список). Использование position: absolute и помещение элемента в body обрабатывает прокрутку документа правильно, но требует пересчета позиции при прокрутке контейнера.
Все способы требуют обработки события resize. В общем, нет тут хорошего решения.
Примеры
Все примеры содержат одинаковую верстку, и состоят из поля ввода с текстом и кнопки. По нажатию на кнопку введенный текст появляется в «модальном» окошке с кнопкой «Закрыть».
Все примеры написаны на typescript. Для компиляции и бандлинга используется webpack. Чтобы запустить примеры, у вас должен быть установлен NodeJS.
Если это делать лень, можно просто открыть в браузере index.html из папки соответствующего примера.
Angular 1.5
Компоненты
В версии 1.5 Angular приобрел синтаксический сахар в виде метода component у модуля, который позволяет объявлять компоненты. Компоненты — это на самом деле директивы, но код их объявления ориентирован на создание кирпичиков предметной области приложения, тогда как директивы больше ориентированы (идеологически, технически все идентично) на низкоуровневую и императивную работу с DOM. Это нововведение простое, но прикольное, и позволяет объявлять компоненты способом, схожим с Angular 2. Никаких новых фич этот способ не привносит, но может кардинально повлиять на структуру приложения, особенно, если раньше вы пользовались <div ng-controller=»MyController as $c»>. </div> .
От себя я могу добавить, что я в восторге от этой возможности. Она позволяет создавать компоненты с четким контрактом и высоким потенциалом для повторного использования. Более того, с этой возможностью я отказался от вынесения HTML разметки компонента в отдельный файл, т.к. разметка получается маленькая и аккуратная — в ней используются вложенные компоненты — и не загромождает исходник компонента.
В примере я тоже использую эту возможность, поэтому, если вы еще не знакомы с ней, почитать можно здесь.
Два способа
Наверное, существует больше способов поместить компонент в произвольное место DOM. Я покажу два из них, один при помощи сервиса $compile , второй — при помощи директивы с transclude .
Первый способ императивный, и подходит больше для ad-hoc показа модальных окон, например, для вывода сообщений или запроса у пользователя каких-то параметров. Также этот способ можно применять, если тип компонента неизвестен, или разметка динамическая.
Второй способ — декларативный, он позволяет встроить всплывающий элемент в шаблон компонента,
но при показе помещать его в body . Подходит для компонентов типа дроп-дауна, позволяя реактивно управлять видимостью.Способ 1: $compile
Сервис $compile позволяет преобразовать строку с Angular разметкой в DOM элемент и связать его со $scope .
Получившийся элемент может быть добавлен в произвольное место документа.Все довольно просто.
Вот документация сервиса. По ссылке полное руководство по API директив, интересующая нас часть в самом конце — использование $compile как функции.
Получаем доступ к $compile
Объявление static $inject=[«$compile»] эквивалентно следующему Javascript коду:
$compile работает в две фазы. На первой он преобразует строку в функцию-фабрику. На второй нужно вызвать полученную фабрику и передать ей $scope . Фабрика вернет DOM элементы, связанные с этим $scope.
$compile принимает три аргумента, нас интересует только первый. Первый аргумент — это строка, содержащая HTML шаблон, который будет потом преобразован в работающий фрагмент Angular приложения. В шаблоне можно использовать любые зарегистрированные компоненты из вашего модуля и его зависимостей, а также любые валидные конструкции Angular — директивы, интерполяцию строк и т.п.
Результатом компиляции будет фабрика — функция, которая позволит связать строковый шаблон с любым $scope . Таким образом, задавая шаблон, можно использовать любые поля и методы вашего скоупа. Например, вот как выглядит код открытия всплывающего окна:
Обратите внимание на несколько вещей.
Во-первых, шаблон содержит компонент <popup-content></popup-content> .
Во-вторых, шаблон содержит обращение к полю text контроллера: text=»$c.text» .
$c — это алиас контроллера, заданный при объявлении компонента.PopupService.open также возвращает фабрику, позволяющую связать шаблон со $scope . Для того, чтобы связать динамический компонент со $scope нашего компонента, приходится передавать $scope в контроллер.
Вот как выглядит PopupService.open :
В нашей функции мы оборачиваем переданный шаблон в разметку модального окна. Потом компилируем шаблон, получая фабрику динамических компонентов. Потом вызываем полученную фабрику, передавая $scope , и получаем HTML элемент, который представляет собой полностью рабочий фрагмент Angular приложения, связанный с переданным $scope . Теперь его можно добавить в любое место документа.
Хотя наш метод PopupService.open тоже возвращает фабрику для связи с $scope , он делает дополнительную работу. Во-первых, когда фабрика вызывается, он не только создает элемент, но и добавляет его в body . Во-вторых, он создает функцию, которая позволит «закрыть» поп-ап окно, удалив его из документа. PopupService.open возвращает эту функцию для закрытия окна.
Что ж, вариант не так плох. Хотя само отображение окна императивное, тем не менее, содержимое окна все еще реактивно, и может быть декларативно связано с родительским $scope . Хотя для отображения контента приходится использовать строки, но если сам контент окна сделать в виде компонента, то связывать нужно будет только input и output свойства, а не весь контент. Метод позволяет поместить поп-ап элемент в любое место документа, даже если оно вне ng-app .
Способ 2: Директива с transclude
Второй способ позволяет задавать содержимое всплывающего элемента прямо возле его «родителя». При показе элемент будет на самом деле добавлен в body .
Здесь искомая директива — <popup>. </popup> . Все, что внутри нее, будет показано во всплывающем окне, и расположено в body .
Небольшой недостаток этого метода в том, что показывать и прятать окно необходимо при помощи директивы ng-if , которая физически будет убирать/добавлять содержимое в DOM дерево.
transclude
transclude — это способ директив работать со своим содержимым. Под содержимым понимается то, что расположено между открывающимся и закрывающимся тегами директивы.
Это очень мощная возможность, на основе которой можно сделать много интересного. Мы же будем брать содержимое и помещать его в body .
Как использовать transclude ? Напрямую использовать контент (например, через $element.children ) нельзя — он не связан с правильным scope, и не скомпилирован (не заменены директивы и т.д.). Для использования transclude нужно получить доступ к т.н. transclude function . Это фабрика, которая умеет создавать скомпилированные копии (клоны) содержимого. Эти клоны будут скомпилированы и связаны с правильным scope, и вообще, очень похожи на результат работы $compile . Transclude function, однако, не возвращает значение, как обычная фабрика, а передает его в коллбек-функцию.
Можно создавать сколько угодно клонов, переопределять им scope, добавлять в любое место документа, и так далее. Здорово.
Для директив, которые сами управляют содержимым (вызывают transclude function), необходимо реализовывать lifecycle методы для очистки содержимого. Эти методы реализуются в контроллере директивы. Удалять добавленное содержимое нужно в $onDestroy .
Осталось последнее — как получить доступ к transclude function. Она передается в нескольких местах, но мы ее заинжектим в контроллер. Для того, чтобы она передалась, в конфигурации директивы должно быть установлено transclude: true .
Итак, полный код:
Неплохо, всего 36 строк.
- Полностью реактивное отображение и скрытие, реактивное содержимое
- Удобно разносит «виртуальное» расположение в дереве компонентов и «физическое» расположение в DOM дереве.
- Декларативно привязано к текущему scope.
- В этом варианте реализации нужно использовать ng-if для управления отображением.
Angular 2
Новая версия Angular, отличающаяся от первого настолько, что, фактически, это новый продукт.
Мои впечатления от него двоякие.С одной стороны, код компонентов несомненно чище и яснее. При написании бизнес-компонентов разделение кода и представления отличное, change tracking работает прекрасно, прощайте $watch и $apply , прекрасные средства для описания контракта компонента.
С другой стороны, не оставляет ощущение монструозности. 5 min quickstart выглядит издевательством. Множество дополнительных библиотек, многие из которых обязательны к использованию (как rxjs ). То, что я успеваю увидеть надпись Loading. при открытии документа с файловой системы, вселяет сомнения в его скорости. Размер бандла в 4.3MB против 1.3MB у Angular 1 и 700KB React (правда, это без оптимизаций, дефолтный бандлинг webpack-а). (Напоминаю, что webpack собирает (бандлит) весь код приложения и его зависимостей (из npm) в один большой javascript файл).
Минифицированный размер: Angular 1 — 156KB, Angular 2 — около 630KB, в зависимости от варианта, React — 150KB.
Angular 2 на момент написания еще RC. Код практически готов, багов вроде бы нет, основые вещи сделаны (ну кроме разве что переделки форм). Однако документация неполная, многие вещи приходится искать в комментариях к github issue
(как, например, динамическая загрузка компонентов, что, собственно, и подтолкнуло меня к написанию этой статьи).Disclaimer
Тратить полтора часа на шаги, описанные в упомянутом 5 min quickstart, не хотелось, поэтому проект сконфигурирован не совсем, кхм, традиционно для Angular 2. SystemJS не используется, вместо этого бандлится webpack-ом. Причем Angular 2 не указывается как externals, а берется из npm пакета как есть. В результате получается гигантский бандл в 4.5MB весом. Поэтому не используйте эту конфигурацию в продакшене, если, конечно, не хотите, чтобы пользователи возненавидели ваш индикатор загрузки. Вторая странность, которая не знаю, чем вызвана, это отличающиеся названия модулей. Во всех примерах (в том числе в официальной документации) импорт Angular выглядит как import < >from «@angular/core» . В то же время, у меня так не заработало, а работает import <> from «angular2/core» .
Динамическая загрузка
К чести Angular 2, код динамической загрузки вызывает трудности только при поиске. Для динамической загрузки используется класс ComponentResolver в сочетании с ViewContainerRef.
ComponentResolver легко получить через dependency injection. ViewContainerRef , по-видимому, не может быть создан для произвольного DOM элемента, и может быть только получен для существующего Angular компонента. Это значит, что поместить динамически созданный элемент в произвольное место DOM дерева невозможно, по крайней мере, в релиз-кандидате.
Поэтому, наш механизм для показа поп-апов будет составным.
Во-первых, у нас будет компонент, в который будут динамически добавляться поп-ап элементы. Его нужно будет разместить где-нибудь в дереве компонентов, желательно поближе к корневому элементу. Кроме того, никакой из его родительских контейнеров не должен содержать стилей, обрезающих содержимое. В коде это overlay-host.component.ts .
Во-вторых, у нас есть вспомогательный компонент, содержащий в себе разметку для поп-ап окна. Это OverlayComponent , в который оборачивается динамически создаваемый компонент.
В-третьих, у нас есть сервис, который обеспечивает связь между хост-компонентом для поп-апов и клиентами, которые хотят показывать компонент. Сервис достаточно простой, хост-компонент регистрирует себя в нем при создании, и метод сервиса просто перенаправляет вызовы открытия окна этому хост-компоненту.
Хост-компонент
Я приведу класс целиком, он не очень большой, и потом пройдусь по тонким местам:
Что делает этот код? Он динамически создает компонент, используя его тип (тип компонента — это его функция-конструктор). Предварительно создается компонент-обертка ( OverlayComponent ), наш запрошенный компонент добавляется уже к нему. Также мы подписываемся на событие destroy , чтобы уничтожить обертку при уничтожении компонента.
Первое, на что нужно обратить внимание, это как получается ViewContainerRef при помощи запроса к содержимому.
Декоратор @ViewChild() позволяет получать ViewContainerRef по имени template variable template: <template #container></template> .
#container — это и есть template variable, переменная шаблона. К ней можно обращаться по имени, но только в самом шаблоне. Чтобы получить доступ к ней из класса компонента, используется упомянутый декоратор.Честно говоря, я это нагуглил, и как по мне, это вообще неинтуитивно. Это одна из особенностей второго Angular-а, которая мне очень сильно бросилась в глаза, — в документации очень сложно, или же вообще невозможно, найти решения для типовых задач низкоуровневой разработки директив. Документация для создания именно бизнес-компонентов нормальная, да и ничего там особо сложного нет. Однако для сценариев написания контролов, низкоуровневых компонентов, невозможно найти документации. Динамическое создание компонентов, взаимодействие с шаблоном из класса — эти области просто не документированы. Даже в описании @ViewChild ничего не сказано о втором параметре.
Что ж, надеюсь, к релизу задокументируют.
Код OverlayHostComponent , который я привел выше, — это самое интересное в нашем примере. OverlayComponent содержит похожий код для динамического добавления содержимого, OverlayService перенаправляет вызовы открытия поп-апа к хост-компоненту. Я не привожу листинги по причине тривиальности, если интересно, посмотрите в исходниках.
Посмотрим теперь, как этим пользоваться:
OverlayService указан в providers Root компонента, в нашем компоненте его регистрировать не нужно.
После создания экземпляра компонента можно получить к нему доступ через ComponentRef.instance .
Потом начинается страшная императивщина: устанавливаем свойства компонента, подписываемся на события, все это руками. Ни о какой декларативности или реактивности речи не идет. Особенно весело, если нужно обеспечить двухстороннее связывание. Несколько вариантов с бесконечными циклами и флагами isRunning вам обеспечены.
Вывод
Честно говоря, это выглядит ужасно. Я искренне надеюсь, что я искал недостаточно хорошо, и где-то есть красивый способ, позволяющий разместить компонент в произвольном месте DOM дерева, и нормально связать свойства динамически созданного компонента с родителем.
Я долго рассматривал исходник ngFor, но не смог решить проблему связывания. Я думал над фабрикой компонентов с динамическими шаблонами, но не уверен, что существует способ динамической регистрации компонентов в массиве directives .
Отсутствие способа помещать компоненты в произвольное место DOM это не очень хорошо, и может вносить ограничения, особенно если страница это несколько микро-приложений. Однако отсутствие динамического связывания компонентов это, на мой взгляд, гораздо более серьезная проблема.
ReactJS
В Реакте стандартный способ отображения компонента в DOM дерево — это метод render , который возвращает виртуальный узел виртуального DOM. Однако, это совсем не значит, что этот способ единственный. Для вставки компонента в произвольное место из метода render возвращается null , и перехватываются lifecycle-методы componentDidMount , componentWillUnmount , componentDidUpdate . В componentDidMount и componentDidUpdate , используя ReactDOM.render , можно отрендерить содержимое в любое место. В componentWillUnmount содержимое, соответственно, уничтожается.
Все просто и понятно. Видно, что такой сценарий создателями Реакта продумывался.
Вообще Реакт после Angular производит очень приятное впечатление. Отсутствуют костыли отслеживания изменений, которые вроде бы не нужно использовать, но всегда приходится. Простой доступ к DOM элементам, если он нужен. Простой доступ к содержимому реакт-элемента через children , причем это не строка и не HTMLElement, а структура, содержащая в себе полноценные реакт-элементы (для работы с ними нужно использовать React.Children ).
Ладно, теперь посмотрим, как это использовать. Привожу, для краткости, только метод render :
Ifc это костылик, который рендерит содержимое, только если condition истинно. Это позволяет избавиться от монструозных IIFE, если нужно отрендерить кусок компонента по условию.
В остальном все просто: если компонент <Popup></Popup> есть в виртуальном дереве — поп-ап окошко показывается, если нет — то прячется. При этом физически в DOM дереве оно находится в body .
Как видим, очень похоже на второй способ с Angular 1.5, с директивой.
Итоги
В принципе, поп-ап в Реакте можно сделать и императивным способом, похожим на способ Angular с $compile . Это может упростить некоторые сценарии и не создавать флаг в состоянии приложения для показа каждого алерта. Принцип тот же (используя ReactDOM.render ), но только не в методах жизненного цикла компонента, а в методе openPopup . Это, конечно же, нарушает реактивность, но сделает код понятнее, увеличив связность.
Недостатки приведенного способа — не будет работать в серверном рендеринге.
Заключение
Подходы, изначально заложенные в Реакте — однонаправленные потоки данных, компоненты, четкий input/output контракт компонента — нашли свое отражение и в Angular: by design в Angular 2, и в обновлениях Angular 1.5. Это изменение без сомнения пошло на пользу первому Angular.
Что касается показа всплывающих элементов — это пример костылей, которые возникли из-за несовершенства CSS, но повлияли на всю экосистему веб-разработки. Это яркий пример текущей абстракции, а также баланса между «чистой архитектурой» и «реальной жизнью» веб разработки. Как видим, разработчики Angular 2 либо не задумывались об этом сценарии, либо реализовали его, но никому не сказали. В то же время, первый Angular и React достаточно гибкие (а разработчики Реакта видимо еще и продуманные), чтобы можно было реализовать рендеринг элемента в отличное от его расположения в дереве компонентов.