PopupForm — универсальная динамическая всплывающая форма
Вот основной код для работы класса PopupForm
/* Основной контейнер попапа */
.popup-form {
--transition: 250ms; /* Длительность переходов */
position: fixed; /* Фиксированное позиционирование по окну */
inset: 0; /* Верх, право, низ, лево = 0 */
display: flex; /* Центрируем содержимое */
align-items: center;
justify-content: center;
opacity: 0; /* Начальная невидимость */
transition: opacity var(--transition) ease; /* Плавное изменение прозрачности */
}
/* Класс, активирующий отображение попапа */
.popup-form.active {
opacity: 1;
}
/* Полупрозрачный фон-оверлей */
.popup-form__overlay {
position: absolute;
inset: 0;
background-color: #00000066; /* Черный с прозрачностью */
backdrop-filter: blur(2px); /* Размытие фона за оверлеем */
z-index: 999; /* Позади модального окна */
cursor: pointer; /* Курсор указывает, что можно кликнуть для закрытия */
}
/* Модальное окно */
.popup-form__modal {
background-color: #fff;
border: 1px solid #00000033; /* Светлая рамка */
border-radius: 8px;
padding: 2ch; /* Внутренние отступы */
position: relative;
width: 100%;
max-width: 320px; /* Фиксированная максимальная ширина */
z-index: 1000; /* Поверх оверлея */
}
/* Кнопка закрытия */
.popup-form__close {
position: absolute;
top: 8px;
right: 8px;
height: 40px;
width: 40px;
background: none;
border: none;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
transition: transform var(--transition) ease;
}
.popup-form__close:hover {
transform: rotate(90deg); /* Легкая анимация при ховере */
}
/* Кнопка отправки */
.popup-form [type=submit] {
padding: 0.5em 1em;
border: none;
border-radius: 4px;
background-color: #007BFF;
color: #fff;
cursor: pointer;
transition: background-color var(--transition) ease;
}
.popup-form [type=submit]:hover {
background-color: #0056b3;
}
/* Форма и её элементы */
.popup-form__form {
display: grid;
gap: 16px; /* Интервал между полями */
}
/* Стили для обёртки каждого поля */
.popup-form__label {
font-size: 14px;
color: #666666;
display: grid;
gap: 4px; /* Отступ между подписью и полем */
}
/* Заголовок */
.popup-form h2 {
margin: 0;
font-size: 1.25rem;
}
/* Поля ввода и textarea */
.popup-form input,
.popup-form textarea {
border: 1px solid #00000033;
border-radius: 4px;
padding: 1ch;
width: 100%;
box-sizing: border-box;
resize: vertical;
}/**
* Класс PopupForm — универсальная динамическая всплывающая форма
*/
class PopupForm {
/**
* @param {Object} defaultOptions — опции по умолчанию:
* title: Заголовок формы,
* onSubmit: функция-колбэк при отправке
*/
constructor(defaultOptions = {}) {
this.options = defaultOptions;
this.container = null; // Контейнер попапа в DOM
}
/**
* Рендерит попап с переданными полями
* @param {Object} customOptions — опции для конкретного вызова:
* title, fields (массив конфигураций полей)
*/
render(customOptions = {}) {
const opts = { ...this.options, ...customOptions };
const { title, fields = [] } = opts;
// Если попап уже отрисован — ничего не делаем
if (this.container) return;
// Создаём оверлей (фон)
const overlay = document.createElement("div");
overlay.className = "popup-form__overlay";
overlay.setAttribute("role", "presentation");
// При клике по оверлею — закрыть попап
overlay.onclick = () => this.destroy();
// Создаём контейнер модального окна
const modal = document.createElement("div");
modal.className = "popup-form__modal";
modal.setAttribute("role", "dialog");
modal.setAttribute("aria-modal", "true");
modal.setAttribute("aria-labelledby", "popup-title");
// Внутреннее HTML-содержимое модального окна:
// — кнопка закрытия
// — заголовок
// — форма с динамическими полями и кнопкой отправки
modal.innerHTML = `
<button class="popup-form__close" aria-label="Закрыть">×</button>
<h2 id="popup-title">${title || "Форма"}</h2>
<form class="popup-form__form">
${this._buildFields(fields)}
<button type="submit">Отправить</button>
</form>
`;
// Обработчик кнопки закрытия
modal.querySelector(".popup-form__close").onclick = () => this.destroy();
// Обработчик отправки формы
modal.querySelector("form").onsubmit = (event) => this._onSubmit(event);
// Оборачиваем overlay и modal в общий контейнер
this.container = document.createElement("div");
this.container.className = "popup-form";
this.container.append(overlay, modal);
// Добавляем контейнер в DOM и блокируем скролл фона
document.body.append(this.container);
document.body.style.overflow = "hidden";
// На следующем кадре запускаем анимацию появления
requestAnimationFrame(() => {
this.container.classList.add("active");
});
// Обработчик клавиши Escape для закрытия
this._onKeydown = this._onKeydown.bind(this);
document.addEventListener("keydown", this._onKeydown);
// Устанавливаем фокус на первое поле ввода
modal.querySelector("input, textarea, select")?.focus();
}
/**
* Закрывает попап с анимацией
*/
destroy() {
if (!this.container) return;
// Убираем слушатель клавиатуры и разблокируем скролл
document.removeEventListener("keydown", this._onKeydown);
document.body.style.overflow = "";
// Убираем класс active — начнётся переход opacity 1→0
this.container.classList.remove("active");
// Ждём окончания transition по opacity и удаляем контейнер
const onTransitionEnd = (event) => {
if (event.propertyName === "opacity") {
this.container.removeEventListener("transitionend", onTransitionEnd);
this.container.remove();
this.container = null;
}
};
this.container.addEventListener("transitionend", onTransitionEnd);
}
/**
* Обработчик отправки формы
* @param {Event} event
*/
_onSubmit(event) {
event.preventDefault(); // Отменяем перезагрузку страницы
const formData = new FormData(event.target);
const data = Object.fromEntries(formData.entries());
// Вызываем пользовательский колбэк, если он передан
if (typeof this.options.onSubmit === "function") {
this.options.onSubmit(data, this);
}
}
/**
* Обработчик нажатия клавиши Escape
* @param {KeyboardEvent} event
*/
_onKeydown(event) {
if (event.key === "Escape") {
this.destroy();
}
}
/**
* Строит HTML-разметку для полей формы
* @param {Array} fields — массив объектов конфигурации полей
* @returns {string} — HTML с <label> и элементами input/textarea/select
*/
_buildFields(fields = []) {
return fields
.map(({ type, name, label, placeholder = "", required = false, options = [] }) => {
const req = required ? "required" : "";
switch (type) {
case "text":
case "email":
case "tel":
return `
<label class="popup-form__label">
${label}
<input
type="${type}"
name="${name}"
placeholder="${placeholder}"
${req}
/>
</label>
`;
case "textarea":
return `
<label class="popup-form__label">
${label}
<textarea
name="${name}"
placeholder="${placeholder}"
${req}
></textarea>
</label>
`;
case "select":
const opts = options
.map((opt) => `<option value="${opt.value}">${opt.text}</option>`)
.join("");
return `
<label class="popup-form__label">
${label}
<select name="${name}" ${req}>
${opts}
</select>
</label>
`;
default:
console.warn("Unknown field type:", type);
return "";
}
})
.join("");
}
}
/* Пример использования */
/* Создаём экземпляр формы с заголовком и колбэком отправки */
const contactPopup = new PopupForm({
title: "Свяжитесь с нами",
onSubmit: (data, inst) => {
console.log("Отправлены данные: ", data);
inst.destroy(); // Закрываем попап после отправки
}
});
/* Ждём загрузки DOM и подкл. обработчик клика на кнопку */
document.addEventListener("DOMContentLoaded", () => {
const btn = document.getElementById("open-contact");
btn.addEventListener("click", () => {
contactPopup.render({
fields: [
{ type: "text", name: "name", label: "Ваше имя", required: true, placeholder: "Иван Иванов" },
{ type: "email", name: "email", label: "Email", required: true, placeholder: "name@example.com" },
{ type: "textarea", name: "message", label: "Сообщение", placeholder: "Ваше сообщение" }
]
});
});
});Как это сделано
Шаг 1. Создание каркаса класса и конструктора
Опишем класс PopupForm и добавим в него конструктор, который будет принимать объект с опциями по умолчанию.
- Создаем файл
popupForm.js. - Объявляем класс
PopupForm. - Добавляем конструктор, который принимает параметр
defaultOptionsи сохраняет его в свойствоthis.options. - Инициализируем свойство
this.container = null, чтобы позже понять, отрисована ли форма.
// popupForm.js
class PopupForm {
constructor(defaultOptions = {}) {
// Сохраняем переданные опции как базовые
this.options = defaultOptions;
// Контейнер формы в DOM (пока не создан)
this.container = null;
}
}
export default PopupForm;Шаг 2. Реализация метода render()
1. В начале метода объединяем опции:
const opts = { ...this.options, ...customOptions };2. Добавляем защиту от повторного рендера:
if (this.container) return;3. Создаём оверлей и модальное окно:
const overlay = document.createElement('div');
overlay.className = 'popup-form__overlay';
const modal = document.createElement('div');
modal.className = 'popup-form__modal';4. Наполняем modal через innerHTML с учетом заголовка и места для полей:
modal.innerHTML = `
<button class="popup-form__close">×</button>
<h2>${opts.title}</h2>
<form class="popup-form__form">
<!-- поля -->
<button type="submit">Отправить</button>
</form>
`;5. Повесим слушатели:
overlay.onclick→this.destroy()modal.querySelector('.popup-form__close').onclick→this.destroy()modal.querySelector('form').onsubmit→this._onSubmit
6. Соберем контейнер и вставим в document.body:
this.container = document.createElement('div');
this.container.className = 'popup-form';
this.container.append(overlay, modal);
document.body.append(this.container);7. Зарегистрируем слушатель ESC:
this._onKeydown = this._onKeydown.bind(this);
document.addEventListener('keydown', this._onKeydown);Шаг 3. Реализация метода destroy()
1. Проверим, есть ли контейнер (форма уже отрисована).
2. Снимите слушатель клавиатуры:
document.removeEventListener('keydown', this._onKeydown);3. Удалите контейнер из DOM и обнулите this.container:
this.container.remove();
this.container = null;4. При необходимости можно добавить колбэк onClose в опции и вызвать его перед удалением.
Шаг 4. Реализация метода _onKeydown
- Метод должен принимать событие
event. - Если нажата клавиша Escape (
event.key === 'Escape'), вызватьthis.destroy(). - Важно использовать правильный контекст, мы уже сделали
.bind(this).
Добавляем в класс следующий код:
_onKeydown(event) {
if (event.key === 'Escape') {
this.destroy();
}
}Шаг 5. Реализация метода _onSubmit
Теперь напишем обработчик отправки формы:
- Метод должен принимать
event - Предотвратить стандартную отправку:
event.preventDefault() - Собрать данные формы через
new FormData(event.target) - Преобразовать в объект:
Object.fromEntries(formData.entries()) - Если в опциях есть
onSubmit, вызвать его с данными и экземпляром класса:
_onSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
const data = Object.fromEntries(formData.entries());
if (typeof this.options.onSubmit === 'function') {
this.options.onSubmit(data, this);
}
}Шаг 6. Реализация приватного метода _buildFields()
Метод _buildFields(fields) будет принимать массив конфигураций полей и возвращать HTML-строку с разметкой элементов:
- Перебрать
fieldsи для каждого элемента сформировать соответствующую разметку:- Для типов
text,email,telиспользовать<input> - Для типа
textarea—<textarea> - Для
select—<select>с опциями
- Для типов
- Учесть атрибуты:
name,label,placeholder,required - Собрать все строки и вернуть единый HTML
_buildFields(fields = []) {
return fields.map(field => {
const { type, name, label, placeholder = '', required = false, options = [] } = field;
const req = required ? 'required' : '';
switch (type) {
case 'text':
case 'email':
case 'tel':
return `
<label class="popup-form__label">
${label}
<input
type="${type}"
name="${name}"
placeholder="${placeholder}"
${req}
/>
</label>
`;
case 'textarea':
return `
<label class="popup-form__label">
${label}
<textarea
name="${name}"
placeholder="${placeholder}"
${req}
></textarea>
</label>
`;
case 'select':
const opts = options
.map(opt => `<option value="${opt.value}">${opt.text}</option>`)
.join('');
return `
<label class="popup-form__label">
${label}
<select name="${name}" ${req}>
${opts}
</select>
</label>
`;
default:
console.warn('Unknown field type:', type);
return '';
}
}).join('');
}Интеграция в render()
Вместо <!-- поля --> вызовите:
modal.innerHTML = `
<button class="popup-form__close">×</button>
<h2>${opts.title || 'Форма'}</h2>
<form class="popup-form__form">
${this._buildFields(opts.fields)}
<button type="submit">Отправить</button>
</form>
`;Шаг 7. Разметка и подключение триггера для открытия формы
1. В нашем HTML-файле (например, index.html) добавим кнопку для открытия попапа. Рекомендуется дать ей id или data-атрибут для удобства селектора:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<title>Пример PopupForm</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<!-- Кнопка-триггер -->
<button id="open-contact" class="btn">Связаться с нами</button>
<script type="module" src="popupForm.js"></script>
<script type="module" src="main.js"></script>
</body>
</html>2. В файле-инициализаторе (main.js) импортируем класс и создадим экземпляр формы с нужными дефолтными опциями (например, title и onSubmit):
import PopupForm from './popupForm.js';
const contactPopup = new PopupForm({
title: 'Свяжитесь с нами',
onSubmit: (data, inst) => {
console.log('Отправлены данные:', data);
// Здесь можно отправить запрос на сервер через fetch/ajax
inst.destroy(); // Закрыть попап после успешной отправки
}
});3. Назначим слушатель клика на кнопку, чтобы по нажатию вызвать метод render() с конфигом полей:
document.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('open-contact');
btn.addEventListener('click', () => {
contactPopup.render({
fields: [
{ type: 'text', name: 'name', label: 'Ваше имя', required: true, placeholder: 'Иван Иванов' },
{ type: 'email', name: 'email', label: 'Email', required: true, placeholder: 'name@example.com' },
{ type: 'textarea', name: 'message', label: 'Сообщение', placeholder: 'Ваше сообщение' }
]
});
});
});После этих шагов:
- При загрузке страницы кнопка «Связаться с нами» появится в верстке.
- По клику на неё вызовется
PopupForm.render(), который на лету отрисует окно с полями. - Данные формы будут обработаны в колбэке
onSubmit, а затем попап закроется.