
Использование React и Typescript для создания многоразовых и настраиваемых модальных окон
20 декабря 2022 г.Как интерфейсному разработчику, вам, вероятно, приходилось создавать модальное окно более одного раза. Этот тип элемента отличается от обычных всплывающих окон тем, что он не появляется автоматически, вместо этого пользователь должен щелкнуть где-нибудь на веб-сайте (обычно кнопку), чтобы он появился.
В этом руководстве вы узнаете, как разработать и реализовать модальный компонент в вашем проекте React с помощью TypeScript. Его можно будет повторно использовать в любой части вашего приложения, и вы сможете настраивать его и добавлять контент любого типа.
Что такое модальные окна?
Модали, несомненно, являются одним из наиболее часто используемых компонентов в Интернете, поскольку их можно использовать в различных контекстах, от сообщений до пользовательского ввода. Они разместили оверлей на экране. Поэтому они имеют визуальный приоритет над всеми остальными элементами.
Как и многие другие компоненты React, для помощи в этом процессе можно установить зависимость. Однако мы всегда оказываемся ограниченными в нескольких аспектах, и один из них — стиль.
Мы можем создать модальное окно внутри или снаружи элемента, из которого мы его вызываем в иерархии DOM, но чтобы выполнить определение модального окна, оно должно быть на том же уровне, что и элемент, используемый в качестве корня в React, и для этого мы будут использовать порталы.
Что такое порталы в React?
Порталы обеспечивают быстрый и простой способ отображения дочерних элементов в узле DOM, который существует вне иерархии DOM родительского компонента.
В React поведение по умолчанию заключается в отображении всего приложения в одном узле DOM — корневом каталоге приложения, но что, если мы хотим отображать дочерние элементы за пределами корневого узла DOM? И вы хотите, чтобы дочерние элементы визуально отображались поверх его контейнера.
Портал можно создать с помощьюReactDOM.createPortal(child, container)
. Здесь дочерний элемент — это элемент, фрагмент или строка React, а контейнер — это расположение (узел) DOM, в которое следует внедрить портал.
Ниже приведен пример модального компонента, созданного с использованием вышеуказанного API.
const Modal =({ message, isOpen, onClose, children })=> {
if (!isOpen) return null
return ReactDOM.createPortal(
<div className="modal">
<span className="message">{message}</span>
<button onClick={onClose}>Close</button>
</div>,
domNode)
}
Хотя портал визуализируется вне родительского элемента DOM, он ведет себя аналогично обычному компоненту React в приложении. Он может получить доступ к свойствам и API контекста.
Это связано с тем, что порталы находятся в иерархии дерева React, а порталы влияют только на структуру HTML DOM и не влияют на дерево компонентов React.
Разработка модальных окон в React
Настройка
Мы создаем наше приложение с приглашением со следующими команда:
yarn create vite my-modals-app --template react-ts
Устанавливаем зависимости, которые нам понадобятся в проекте:
yarn add styled-components @types/styled-components
После этого создаем следующую структуру проекта:
src/
├── components/
│ ├── layout/
│ │ ├── Header.tsx
│ │ └── styles.tsx
│ ├── modals/
│ │ ├── Buttons.tsx
│ │ ├── Modal.tsx
│ │ ├── PortalModal.tsx
│ │ ├── index.ts
│ └── └── styles.ts
├── hooks/
│ └── useOnClickOutside.tsx
├── styles/
│ ├── modal.css
│ ├── normalize.css
│ └── theme.ts
├── ts/
│ ├── interfaces/
│ │ └── modal.interface.ts
│ ├── types/
│ └── └── styled.d.ts
├── App.tsx
├── main.tsx
└── config-dummy.ts
Компоненты
Как видно из структуры папок, у нас есть несколько функциональных и стилевых компонентов для этого приложения, но чтобы не делать этот урок длинным, мы сосредоточимся только на основных компонентах.
App.tsx
: в этом компоненте у нас есть примеры использования нашего пользовательского модального окна. У нас есть кнопки, которые показывают модальные окна с различными конфигурациями, чтобы дать нам представление о том, чего мы можем достичь с этим модальным окном.
В этом компоненте мы также определяем тему для нашего модального окна, добавляя ThemeProvider
и создавая глобальный стиль с помощью createGlobalStyle
из styled-components
.< /p>
import { FC, useState } from "react";
import Header from "./components/layout/Header";
import { Buttons, Modal } from "./components/modals";
import { ThemeProvider } from "styled-components";
import { lightTheme, darkTheme, GlobalStyles } from "./styles/theme";
import * as S from "./components/modals/styles";
import { INITIAL_CONFIG } from "./config-dummy";
import imgModal from "./assets/images/imgModal.jpg";
const App: FC = () => {
const [theme, setTheme] = useState("dark");
const [show1, setShow1] = useState < boolean > false;
const [show2, setShow2] = useState < boolean > false;
const [show3, setShow3] = useState < boolean > false;
const [show4, setShow4] = useState < boolean > false;
const isDarkTheme = theme === "dark";
return (
<ThemeProvider theme={isDarkTheme ? darkTheme : lightTheme}>
<>
<GlobalStyles />
<Header isDarkTheme={isDarkTheme} setTheme={setTheme} />
<main>
<Buttons
show1={show1}
setShow1={setShow1}
show2={show2}
setShow2={setShow2}
show3={show3}
setShow3={setShow3}
show4={show4}
setShow4={setShow4}
/>
<Modal show={show1} setShow={setShow1} config={INITIAL_CONFIG.modal1}>
<h1>My Modal 1</h1>
<p>Reusable Modal with options to customize.</p>
<S.ModalFooter>
<S.ModalButtonSecondary onClick={() => setShow1(!show1)}>
Cancel
</S.ModalButtonSecondary>
<S.ModalButtonPrimary>Acept</S.ModalButtonPrimary>
</S.ModalFooter>
</Modal>
<Modal show={show2} setShow={setShow2} config={INITIAL_CONFIG.modal2}>
<p>Reusable Modal with options to customize.</p>
<input type="email" placeholder="Email" />
<S.ModalFooter>
<S.ModalButtonPrimary>Send</S.ModalButtonPrimary>
</S.ModalFooter>
</Modal>
<Modal show={show3} setShow={setShow3} config={INITIAL_CONFIG.modal3}>
<img src={imgModal} alt="My Modal" />
</Modal>
<Modal show={show4} setShow={setShow4} config={INITIAL_CONFIG.modal4}>
<h1>My Modal 4</h1>
<p>Reusable Modal with options to customize.</p>
</Modal>
</main>
</>
</ThemeProvider>
);
};
export default App;
Modal.tsx
: отображение этого компонента зависит от действий, выполняемых пользователем. Он заключен в компонент стиля, который накладывается на экран.
Этот компонент получает в качестве свойства конфигурацию, в которой мы определим, как будет отображаться наше модальное окно, то есть положение, в котором оно будет отображаться, заголовок модального окна, отступы и т. д.
Он также получает дочерние элементы, которые содержат весь контент, который будет отображаться внутри модального окна. Это может быть любой тип содержимого tsx
.
Кроме того, в этом компоненте у нас есть несколько функций, которые служат нам для закрытия модального окна.
useOnClickOutside
: это настраиваемый хук, который закроет модальное окно, когда обнаружит, что пользователь щелкнул за пределами модального окна.
Этот хук получает в качестве параметра ссылку на элемент, который мы хотим обнаружить, и обратный вызов, который представляет собой действие, которое мы хотим выполнить при обнаружении клика.
Этот хук добавляет EventListener
, который будет реагировать на события mousedown
и touchstart
, после чего он будет оценивать, был ли щелчок внутри элемента или вне его.
handleKeyPress
: это обратный вызов, который будет выполнен, когда он обнаружит, что пользователь нажимает клавишу ESC, чтобы закрыть модальное окно.
Он делает это, добавляя EventListener
к событию keydown
, чтобы затем оценить, какая клавиша была нажата.
import { useCallback, useEffect, useRef } from "react"
import PortalModal from "./PortalModal"
import useOnClickOutside from "../../hooks/useOnClickOutside"
import { ModalConfig } from "../../ts/interfaces/modal.interface"
import * as S from "./styles"
import "../../styles/modal.css"
interface Props {
show: boolean;
config: ModalConfig;
setShow: (value: boolean) => void;
children: JSX.Element | JSX.Element[];
}
const Modal = ({ children, show, setShow, config }: Props) => {
const modalRef = useRef < HTMLDivElement > null
// handle what happens on click outside of modal
const handleClickOutside = () => setShow(false)
// handle what happens on key press
const handleKeyPress = useCallback((event: KeyboardEvent) => {
if (event.key === "Escape") setShow(false)
}, [])
useOnClickOutside(modalRef, handleClickOutside)
useEffect(() => {
if (show) {
// attach the event listener if the modal is shown
document.addEventListener("keydown", handleKeyPress)
// remove the event listener
return () => {
document.removeEventListener("keydown", handleKeyPress)
}
}
}, [handleKeyPress, show])
return (
<>
{show && (
<PortalModal wrapperId="modal-portal">
<S.Overlay
showOverlay={config.showOverlay}
positionX={config.positionX}
positionY={config.positionY}
show={show}
style={{
animationDuration: "400ms",
animationDelay: "0",
}}
>
<S.ModalContainer padding={config.padding} ref={modalRef}>
{config.showHeader && (
<S.ModalHeader>
<h3>{config.title}</h3>
</S.ModalHeader>
)}
<S.Close onClick={() => setShow(!show)}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
className="bi bi-x"
viewBox="0 0 16 16"
>
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
</svg>
</S.Close>
<S.Content>{children}</S.Content>
</S.ModalContainer>
</S.Overlay>
</PortalModal>
)}
</>
)
}
export default Modal
PortalModal.tsx
: этот компонент использует порталы React, о которых мы уже упоминали ранее.
Он получает дочерние элементы, которые будут нашим модальным окном, и идентификатор, который мы будем использовать для его назначения элементу HTML.
В этом компоненте мы используем хук useLayoutEffect
. Этот хук немного отличается от useEffect
, так как он выполняется, когда обнаруживает изменение в виртуальном DOM, а не в состоянии, что мы и делаем при создании нового элемента в DOM. .
Внутри useLayoutEffect
мы ищем и проверяем, был ли уже создан элемент с переданным нами идентификатором, и устанавливаем этот элемент. В противном случае мы создаем новый элемент в DOM с помощью функции createWrapperAndAppenToBody
.
С помощью этой функции мы можем создать элемент там, где он нам больше всего подходит. В этом случае он создается на том же уровне, что и корневой элемент в теле.
После того, как мы создали элемент, в который собираемся вставить модальное окно, мы создаем портал с помощью createPortal
.
import { useState, useLayoutEffect } from "react";
import { createPortal } from "react-dom";
interface Props {
children: JSX.Element;
wrapperId: string;
}
const PortalModal = ({ children, wrapperId }: Props) => {
const [portalElement, setPortalElement] =
(useState < HTMLElement) | (null > null);
useLayoutEffect(() => {
let element = document.getElementById(wrapperId) as HTMLElement
let portalCreated = false;
// if element is not found with wrapperId or wrapperId is not provided,
// create and append to body
if (!element) {
element = createWrapperAndAppendToBody(wrapperId);
portalCreated = true;
}
setPortalElement(element);
// cleaning up the portal element
return () => {
// delete the programatically created element
if (portalCreated && element.parentNode) {
element.parentNode.removeChild(element);
}
};
}, [wrapperId]);
const createWrapperAndAppendToBody = (elementId: string) => {
const element = document.createElement("div");
element.setAttribute("id", elementId);
document.body.appendChild(element);
return element;
};
// portalElement state will be null on the very first render.
if (!portalElement) return null;
return createPortal(children, portalElement);
};
export default PortalModal;
configDummy.ts
: это файл, который мы будем использовать в качестве шаблона для создания различных модальных окон, в данном случае 4.
Как видите, вы можете создавать множество комбинаций для создания модальных окон, отличающихся друг от друга, и при желании вы можете добавить больше конфигураций.
import {
ModalConfigDummy,
ModalPositionX,
ModalPositionY,
} from "./ts/interfaces/modal.interface";
export const INITIAL_CONFIG: ModalConfigDummy = {
modal1: {
title: "Modal Header 1",
showHeader: true,
showOverlay: true,
positionX: ModalPositionX.center,
positionY: ModalPositionY.center,
padding: "20px",
},
modal2: {
title: "Modal Header 2",
showHeader: false,
showOverlay: true,
positionX: ModalPositionX.center,
positionY: ModalPositionY.center,
padding: "20px",
},
modal3: {
title: "Modal Header 3",
showHeader: false,
showOverlay: true,
positionX: ModalPositionX.left,
positionY: ModalPositionY.start,
padding: "0",
},
modal4: {
title: "Modal Header 4",
showHeader: false,
showOverlay: true,
positionX: ModalPositionX.right,
positionY: ModalPositionY.end,
padding: "0",
},
};
Вот и все! у нас есть наш крутой модальный.
Заключение
В этом руководстве мы создали повторно используемый компонент, который мы можем использовать в любом месте нашего приложения. Используя React Portals, мы можем вставить его в любое место в DOM, поскольку он создаст новый элемент с id
, который мы ему назначаем.
У нас также есть различные варианты стиля для нашего модального окна, и мы можем добавить те, которые мы можем придумать, помимо реализации темного режима, который мне особенно нравится.
Я надеюсь, что это руководство было для вас полезным и что вы узнали что-то новое при разработке этого приложения.
Также опубликовано здесь. < /p>
Оригинал