PopupForm — универсальная динамическая всплывающая форма

Вот основной код для работы класса PopupForm

CSS
/* Основной контейнер попапа */
.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;
}
JavaScript
/**
 * Класс 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 и добавим в него конструктор, который будет принимать объект с опциями по умолчанию.

  1. Создаем файл popupForm.js.
  2. Объявляем класс PopupForm.
  3. Добавляем конструктор, который принимает параметр defaultOptions и сохраняет его в свойство this.options.
  4. Инициализируем свойство this.container = null, чтобы позже понять, отрисована ли форма.
JavaScript
// popupForm.js

class PopupForm {
  constructor(defaultOptions = {}) {
    // Сохраняем переданные опции как базовые
    this.options = defaultOptions;
    // Контейнер формы в DOM (пока не создан)
    this.container = null;
  }
}

export default PopupForm;

Шаг 2. Реализация метода render()

1. В начале метода объединяем опции:

JavaScript
const opts = { ...this.options, ...customOptions };

2. Добавляем защиту от повторного рендера:

JavaScript
if (this.container) return;

3. Создаём оверлей и модальное окно:

JavaScript
const overlay = document.createElement('div');
overlay.className = 'popup-form__overlay';
const modal = document.createElement('div');
modal.className = 'popup-form__modal';

4. Наполняем modal через innerHTML с учетом заголовка и места для полей:

JavaScript
modal.innerHTML = ` 
	<button class="popup-form__close">×</button> 
	<h2>${opts.title}</h2> 
	<form class="popup-form__form"> 
	<!-- поля --> 
	<button type="submit">Отправить</button> 
	</form> 
`;

5. Повесим слушатели:

  • overlay.onclickthis.destroy()
  • modal.querySelector('.popup-form__close').onclickthis.destroy()
  • modal.querySelector('form').onsubmitthis._onSubmit

6. Соберем контейнер и вставим в document.body:

JavaScript
this.container = document.createElement('div'); 
this.container.className = 'popup-form'; 
this.container.append(overlay, modal); 
document.body.append(this.container);

7. Зарегистрируем слушатель ESC:

JavaScript
this._onKeydown = this._onKeydown.bind(this); 
document.addEventListener('keydown', this._onKeydown);

Шаг 3. Реализация метода destroy()

1. Проверим, есть ли контейнер (форма уже отрисована).

2. Снимите слушатель клавиатуры:

JavaScript
document.removeEventListener('keydown', this._onKeydown);

3. Удалите контейнер из DOM и обнулите this.container:

JavaScript
this.container.remove(); 
this.container = null;

4. При необходимости можно добавить колбэк onClose в опции и вызвать его перед удалением.

Шаг 4. Реализация метода _onKeydown

  1. Метод должен принимать событие event.
  2. Если нажата клавиша Escape (event.key === 'Escape'), вызвать this.destroy().
  3. Важно использовать правильный контекст, мы уже сделали .bind(this).

Добавляем в класс следующий код:

JavaScript
_onKeydown(event) {
  if (event.key === 'Escape') {
    this.destroy();
  }
}

Шаг 5. Реализация метода _onSubmit

Теперь напишем обработчик отправки формы:

  1. Метод должен принимать event
  2. Предотвратить стандартную отправку: event.preventDefault()
  3. Собрать данные формы через new FormData(event.target)
  4. Преобразовать в объект: Object.fromEntries(formData.entries())
  5. Если в опциях есть onSubmit, вызвать его с данными и экземпляром класса:
JavaScript
_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-строку с разметкой элементов:

  1. Перебрать fields и для каждого элемента сформировать соответствующую разметку:
    • Для типов text, email, tel использовать <input>
    • Для типа textarea<textarea>
    • Для select<select> с опциями
  2. Учесть атрибуты: name, label, placeholder, required
  3. Собрать все строки и вернуть единый HTML
JavaScript
_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()
Вместо <!-- поля --> вызовите:

JavaScript
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-атрибут для удобства селектора:

HTML
<!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):

JavaScript
import PopupForm from './popupForm.js';

const contactPopup = new PopupForm({
  title: 'Свяжитесь с нами',
  onSubmit: (data, inst) => {
    console.log('Отправлены данные:', data);
    // Здесь можно отправить запрос на сервер через fetch/ajax
    inst.destroy(); // Закрыть попап после успешной отправки
  }
});

3. Назначим слушатель клика на кнопку, чтобы по нажатию вызвать метод render() с конфигом полей:

JavaScript
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, а затем попап закроется.