В предыдущих уроках мы создали блок тарифов (урок «Сборка страниц») и научились делать рабочие формы через FetchIt (урок «Интеграция форм»). Теперь решим типичную задачу: как сделать одну универсальную форму для всех тарифов, которая открывается в модальном окне и автоматически подставляет название выбранного тарифа.
Проблема: у вас на странице 3 тарифа (Базовый, Стандарт, Премиум). Каждая карточка имеет кнопку «Заказать». Создавать 3 отдельные формы — неэффективно.
Решение: одна универсальная форма в Bootstrap 5 Modal, которая получает данные о тарифе через data-атрибуты кнопок.
- Структура файлов урока
- Создаем универсальную модальную форму для блока тарифов
- Часть 1: Дополнение блока тарифов ценами и data-атрибутами
- Шаг 1: Добавление полей в PageBlocks
- Шаг 2: Обновление чанка блока с ценами и data-атрибутами
- Что изменилось?
- CSS для блока цены
- Часть 2: Модальное окно с формой FetchIt
- Создание формы для модалки
- Создание обёртки модального окна
- Подключение модалки в шаблон
- Часть 3: JavaScript для передачи data-атрибутов
- Часть 4: Шаблон email уведомления (10 мин)
- Часть 5: Обработка ответа FetchIt (опционально, 15 мин)
- Опционально: маска телефона (IMask.js)
- Тестирование: проверочный чеклист
- Итоги урока
Структура файлов урока
assets/
└── elements/
└── chunks/
├── pageblocks/
│ └── pb.pricing.tpl ← Обновим: добавим data-атрибуты
└── fetchit/
├── form_pricing_modal.tpl ← Новая форма для модалки
└── email_pricing_order.tpl ← Шаблон email заказа
Создаем универсальную модальную форму для блока тарифов
Часть 1: Дополнение блока тарифов ценами и data-атрибутами
У вас уже есть блок тарифов из урока «Сборка страниц». Нужно добавить цены и подготовить кнопки для работы с модальным окном.
Шаг 1: Добавление полей в PageBlocks
Откройте блок тарифов в админке PageBlocks и добавьте новые поля:
Элементы → PageBlocks → Таблица plans → Редактировать
Добавить поля для каждого тарифа (plans):
| Название поля | Тип | Ключ | Описание поля |
|---|---|---|---|
| Цена (руб/мес) | Числовое поле | price |
Например: 2990 |
| Период оплаты | Текст | period |
Например: «мес», «год» |
| Старая цена (необязательно) | Числовое поле | old_price |
Для зачёркивания |
Не забудьте, так же добавить их в поля сетки.
Шаг 2: Обновление чанка блока с ценами и data-атрибутами
Файл: assets/elements/chunks/pageblocks/pb.pricing.tpl
{* Блок тарифов с ценами и модальной формой заказа *}
<section id="pricing" class="section position-relative custom-bg-color-light-2 border-0 pt-4 m-0">
<svg class="custom-section-curved-top-5" width="100%" height="284" xmlns="http://www.w3.org/2000/svg">
<path id="svg_2" fill="#eff1f3" stroke="#000" stroke-width="0" d="m-30.75698,18.18081c99.6013,28.33304 337.54319,327.06425 868.97187,168.96887c265.71434,-79.04769 585.03969,-5.59538 690.67474,14.9602c105.63504,20.55559 381.87048,2.11063 555.67444,-75.27753c86.90199,-38.69407 736.04117,-78.60276 742.95015,12.26524c6.90898,90.86801 -66.08835,361.6009 -103.97283,363.40912c-37.88449,1.80823 -2793.7397,-55.67435 -2792.62804,-56.56315"/>
</svg>
<div class="container position-relative z-index-1 pb-lg-4 mb-lg-5">
{* Декоративные элементы *}
<div class="custom-circle custom-circle-medium custom-circle-pos-44 appear-animation" data-appear-animation="expandIn" data-appear-animation-delay="200">
<div class="custom-bg-color-grey-1 rounded-circle w-100 h-100" data-plugin-float-element data-plugin-options="{ 'startPos': 'bottom', 'speed': 0.8, 'transition': true, 'transitionDuration': 3000 }"></div>
</div>
<div class="custom-circle custom-circle-small custom-circle-pos-45 appear-animation" data-appear-animation="expandIn" data-appear-animation-delay="500">
<div class="custom-bg-color-grey-2 rounded-circle w-100 h-100" data-plugin-float-element data-plugin-options="{ 'startPos': 'bottom', 'speed': 0.3, 'transition': true, 'transitionDuration': 2000 }"></div>
</div>
<div class="custom-circle custom-circle-extra-small custom-circle-pos-46 appear-animation" data-appear-animation="expandIn" data-appear-animation-delay="800">
<div class="custom-bg-color-grey-2 rounded-circle w-100 h-100" data-plugin-float-element data-plugin-options="{ 'startPos': 'bottom', 'speed': 0.3, 'transition': true, 'transitionDuration': 1500 }"></div>
</div>
{* Заголовок секции *}
<div class="row justify-content-center mb-3">
<div class="col-md-10 col-lg-8 text-center">
<div class="overflow-hidden">
<span class="d-block appear-animation" data-appear-animation="maskUp" data-appear-animation-delay="250">{$subtitle}</span>
</div>
<div class="overflow-hidden mb-2">
<h2 class="font-weight-bold text-color-quaternary text-7 mb-0 appear-animation" data-appear-animation="maskUp" data-appear-animation-delay="400">{$title}</h2>
</div>
<p class="appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="550">{$description}</p>
</div>
</div>
{* Карточки тарифов *}
<div class="row pricing-table custom-pricing-table-style-1 justify-content-center mb-4">
{foreach $plans as $plan}
<div class="col-md-8 col-lg-4">
{* ПОПУЛЯРНЫЙ тариф (is_featured = 1) *}
{if $plan.is_featured == 1}
<div class="plan plan-featured bg-color-quaternary text-center appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="400">
<span class="plan-popular-tag text-color-light font-weight-bold">POPULAR</span>
{* Header *}
<div class="plan-header bg-color-quaternary pt-5 px-4">
<h4 class="text-color-light font-weight-bold text-5 mt-4 mb-2">{$plan.title}</h4>
<p class="text-color-light font-weight-light opacity-6 px-2 mb-0">{$plan.desc}</p>
</div>
{* БЛОК ЦЕНЫ (новое) *}
<div class="plan-price py-4 bg-color-quaternary">
{if $plan.old_price}
<p class="text-color-light text-3 mb-1">
<del class="opacity-6">{$plan.old_price} ₽</del>
</p>
{/if}
<h3 class="text-color-light font-weight-bold mb-0">
<span class="text-8">{$plan.price}</span>
<span class="text-4">₽</span>
<span class="text-3 font-weight-light opacity-8">/{$plan.period ?: 'мес'}</span>
</h3>
</div>
{* Список возможностей *}
<div class="plan-features px-4 text-color-light">
{$plan.features}
</div>
{* Кнопка "Заказать" с data-атрибутами (обновлено) *}
<div class="plan-footer pt-3 pb-5">
<button type="button"
class="btn btn-light-2 btn-outline btn-rounded font-weight-semibold px-5 py-3 text-3 mb-4"
data-bs-toggle="modal"
data-bs-target="#pricingModal"
data-plan-name="{$plan.title}"
data-plan-price="{$plan.price}"
data-plan-period="{$plan.period ?: 'мес'}"
data-plan-id="{$plan@index}">
{$plan.button_text ?: 'Заказать'}
</button>
</div>
</div>
{* ОБЫЧНЫЙ тариф (is_featured = 0) *}
{else}
<div class="plan text-center appear-animation" data-appear-animation="fadeInUpShorter" data-appear-animation-delay="550">
{* Декор для первого блока *}
{if $plan@first}
<div class="custom-dots-rect-5" style="background-image: url(img/home/dots-group.png);"></div>
{/if}
{* Header *}
<div class="plan-header bg-color-light pt-5 px-4">
<h4 class="text-color-dark font-weight-bold text-color-quaternary text-5 mt-4 mb-2">{$plan.title}</h4>
<p class="px-2 mb-0">{$plan.desc}</p>
</div>
{* БЛОК ЦЕНЫ (новое) *}
<div class="plan-price py-4 bg-color-light">
{if $plan.old_price}
<p class="text-color-dark text-3 mb-1">
<del class="opacity-6">{$plan.old_price} ₽</del>
</p>
{/if}
<h3 class="text-color-quaternary font-weight-bold mb-0">
<span class="text-8">{$plan.price}</span>
<span class="text-4">₽</span>
<span class="text-3 font-weight-light opacity-8">/{$plan.period ?: 'мес'}</span>
</h3>
</div>
{* Список возможностей *}
<div class="plan-features px-4">
{$plan.features}
</div>
{* Кнопка "Заказать" с data-атрибутами (обновлено) *}
<div class="plan-footer pt-3 pb-5">
<button type="button"
class="btn btn-secondary btn-outline btn-rounded font-weight-semibold px-5 py-3 text-3 mb-4"
data-bs-toggle="modal"
data-bs-target="#pricingModal"
data-plan-name="{$plan.title}"
data-plan-price="{$plan.price}"
data-plan-period="{$plan.period ?: 'мес'}"
data-plan-id="{$plan@index}">
{$plan.button_text ?: 'Заказать'}
</button>
</div>
</div>
{/if}
</div>
{/foreach}
</div>
</div>
</section>
Что изменилось?
1. Добавлен блок цены:
<div class="plan-price py-4 bg-color-quaternary">
{if $plan.old_price}
<p class="text-color-light text-3 mb-1">
<del class="opacity-6">{$plan.old_price} ₽</del>
</p>
{/if}
<h3 class="text-color-light font-weight-bold mb-0">
<span class="text-8">{$plan.price}</span>
<span class="text-4">₽</span>
<span class="text-3 font-weight-light opacity-8">/{$plan.period}</span>
</h3>
</div>
2. Ссылка <a> заменена на кнопку <button> с data-атрибутами:
Было:
<a href="{$plan.link}" class="btn ...">Заказать</a>
Стало:
<button type="button"
class="btn ..."
data-bs-toggle="modal"
data-bs-target="#pricingModal"
data-plan-name="{$plan.title}"
data-plan-price="{$plan.price}"
data-plan-period="{$plan.period ?: 'мес'}"
data-plan-id="{$plan@index}">
{$plan.button_text ?: 'Заказать'}
</button>
3. Data-атрибуты:
| Атрибут | Что передаёт | Пример |
|---|---|---|
data-plan-name |
Название тарифа | «Стандарт» |
data-plan-price |
Цена | «2990» |
data-plan-period |
Период | «мес» |
data-plan-id |
Индекс тарифа (для аналитики) | «1» |
CSS для блока цены
Добавьте в ваш custom.css:
/* Блок цены в карточке тарифа */
.plan-price {
border-top: 1px solid rgba(255, 255, 255, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
/* Для обычных тарифов */
.plan:not(.plan-featured) .plan-price {
border-top: 1px solid rgba(0, 0, 0, 0.05);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
/* Старая цена (зачёркнутая) */
.plan-price del {
font-size: 18px;
}
/* Основная цена */
.plan-price .text-8 {
font-size: 48px;
line-height: 1;
}
/* Символ рубля */
.plan-price .text-4 {
font-size: 28px;
vertical-align: super;
}
/* Период (/мес) */
.plan-price .text-3 {
font-size: 16px;
}
Часть 2: Модальное окно с формой FetchIt
Создание формы для модалки
Файл: assets/elements/chunks/fetchit/form_pricing_modal.tpl
{* Форма заказа тарифа в модальном окне *}
<form action="{$_modx->resource.id | url}"
method="post"
class="pricing-order-form"
id="pricingOrderForm">
{* Скрытые поля (заполняются JavaScript из data-атрибутов) *}
<input type="hidden" name="plan_name" id="planNameInput" value="">
<input type="hidden" name="plan_price" id="planPriceInput" value="">
<input type="hidden" name="plan_period" id="planPeriodInput" value="">
<input type="hidden" name="plan_id" id="planIdInput" value="">
{* Блок ошибок FormIt *}
{if $errors}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong>Ошибка!</strong> Проверьте правильность заполнения полей.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{/if}
{* Поле: Имя *}
<div class="mb-3">
<label for="customerName" class="form-label">
Ваше имя <span class="text-danger">*</span>
</label>
<input type="text"
class="form-control {if $errors['name']}is-invalid{/if}"
id="customerName"
name="name"
value="{$name}"
placeholder="Иван Петров"
required>
{if $errors['name']}
<div class="invalid-feedback">{$errors['name']}</div>
{/if}
</div>
{* Поле: Телефон *}
<div class="mb-3">
<label for="customerPhone" class="form-label">
Телефон <span class="text-danger">*</span>
</label>
<input type="tel"
class="form-control {if $errors['phone']}is-invalid{/if}"
id="customerPhone"
name="phone"
value="{$phone}"
placeholder="+7 (___) ___-__-__"
required>
{if $errors['phone']}
<div class="invalid-feedback">{$errors['phone']}</div>
{/if}
</div>
{* Поле: Email *}
<div class="mb-3">
<label for="customerEmail" class="form-label">
Email <span class="text-danger">*</span>
</label>
<input type="email"
class="form-control {if $errors['email']}is-invalid{/if}"
id="customerEmail"
name="email"
value="{$email}"
placeholder="ivan@example.com"
required>
{if $errors['email']}
<div class="invalid-feedback">{$errors['email']}</div>
{/if}
</div>
{* Поле: Комментарий (необязательно) *}
<div class="mb-3">
<label for="customerComment" class="form-label">
Комментарий (необязательно)
</label>
<textarea class="form-control"
id="customerComment"
name="comment"
rows="3"
placeholder="Есть вопросы или пожелания?">{$comment}</textarea>
</div>
{* Согласие на обработку *}
<div class="mb-3 form-check">
<input type="checkbox"
class="form-check-input {if $errors['privacy_agree']}is-invalid{/if}"
id="privacyAgree"
name="privacy_agree"
value="1"
required>
<label class="form-check-label" for="privacyAgree">
Я согласен с <a href="/privacy-policy/" target="_blank">политикой конфиденциальности</a>
</label>
{if $errors['privacy_agree']}
<div class="invalid-feedback">{$errors['privacy_agree']}</div>
{/if}
</div>
{* Кнопка отправки *}
<button type="submit" class="btn btn-primary btn-lg w-100">
<i class="fas fa-paper-plane me-2"></i>
Отправить заявку
</button>
</form>
Создание обёртки модального окна
Файл: assets/elements/chunks/fetchit/modal_pricing.tpl
{* Модальное окно для заказа тарифа *}
<div class="modal fade" id="pricingModal" tabindex="-1" aria-labelledby="pricingModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
{* Header *}
<div class="modal-header border-bottom">
<h5 class="modal-title" id="pricingModalLabel">
<i class="fas fa-shopping-cart me-2 text-color-quaternary"></i>
Заказать тариф <span id="modalPlanName" class="text-color-quaternary font-weight-bold"></span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
{* Body *}
<div class="modal-body">
{* Информация о цене (заполняется JS) *}
<div class="alert alert-info mb-4 d-none" id="planPriceAlert">
<div class="d-flex justify-content-between align-items-center">
<div>
<i class="fas fa-tag me-2"></i>
<strong>Стоимость:</strong>
</div>
<div class="text-end">
<span id="modalPlanPrice" class="h4 mb-0 font-weight-bold"></span>
<span class="h5 mb-0">₽</span>
<span class="text-muted">/<span id="modalPlanPeriod"></span></span>
</div>
</div>
</div>
{* Вызов FetchIt с формой *}
{'!FetchIt' | snippet : [
'snippet' => 'FormIt',
'form' => '@FILE chunks/fetchIt/form_pricing_modal.tpl',
'hooks' => 'email',
'validate' => 'name:required,phone:required,email:required:email,privacy_agree:required',
'validationErrorMessage' => 'Заполните все обязательные поля',
'successMessage' => 'Спасибо! Мы свяжемся с вами в течение 15 минут',
'emailTpl' => '@FILE chunks/fetchIt/email_pricing_order.tpl',
'emailTo' => $_modx->config.emailsender,
'emailFrom' => 'noreply@' ~ $_modx->config.http_host,
'emailFromName' => $_modx->config.site_name,
'emailSubject' => 'Новый заказ тарифа с сайта',
'emailReplyTo' => $_modx->config.emailsender
]}
</div>
</div>
</div>
</div>
Подключение модалки в шаблон
Откройте ваш основной шаблон (templates/base.tpl) и добавьте перед закрывающим </body>:
{* Модальное окно заказа тарифа *}
{include 'file:chunks/fetchit/modal_pricing.tpl'}
Часть 3: JavaScript для передачи data-атрибутов
Добавьте в конец файла: js/custom.js
/**
* pricing-modal.js
* Передача данных из data-атрибутов в модальное окно Bootstrap 5
*/
document.addEventListener('DOMContentLoaded', function() {
const pricingModal = document.getElementById('pricingModal');
if (!pricingModal) {
console.warn('Модальное окно #pricingModal не найдено');
return;
}
// Событие Bootstrap 5: модалка открывается
pricingModal.addEventListener('show.bs.modal', function(event) {
// Кнопка, которая вызвала модалку
const button = event.relatedTarget;
if (!button) return;
// Читаем data-атрибуты
const planName = button.getAttribute('data-plan-name');
const planPrice = button.getAttribute('data-plan-price');
const planPeriod = button.getAttribute('data-plan-period') || 'мес';
const planId = button.getAttribute('data-plan-id');
// Обновляем заголовок модалки
const modalTitle = pricingModal.querySelector('#modalPlanName');
if (modalTitle && planName) {
modalTitle.textContent = planName;
}
// Обновляем блок с ценой
const priceElement = pricingModal.querySelector('#modalPlanPrice');
const periodElement = pricingModal.querySelector('#modalPlanPeriod');
const priceAlert = pricingModal.querySelector('#planPriceAlert');
if (priceElement && planPrice) {
// Форматируем цену (2990 → 2 990)
const formattedPrice = new Intl.NumberFormat('ru-RU').format(planPrice);
priceElement.textContent = formattedPrice;
if (periodElement) {
periodElement.textContent = planPeriod;
}
if (priceAlert) {
priceAlert.classList.remove('d-none');
}
}
// Заполняем скрытые поля формы
const planNameInput = pricingModal.querySelector('#planNameInput');
const planPriceInput = pricingModal.querySelector('#planPriceInput');
const planPeriodInput = pricingModal.querySelector('#planPeriodInput');
const planIdInput = pricingModal.querySelector('#planIdInput');
if (planNameInput) planNameInput.value = planName || '';
if (planPriceInput) planPriceInput.value = planPrice || '';
if (planPeriodInput) planPeriodInput.value = planPeriod || '';
if (planIdInput) planIdInput.value = planId || '';
// Debug (можно удалить после тестирования)
console.log('Модалка открыта с данными:', {
planName: planName,
planPrice: planPrice,
planPeriod: planPeriod,
planId: planId
});
});
// Очистка формы при закрытии модалки
pricingModal.addEventListener('hidden.bs.modal', function() {
const form = pricingModal.querySelector('#pricingOrderForm');
if (form) {
form.reset();
// Скрываем блок с ценой
const priceAlert = pricingModal.querySelector('#planPriceAlert');
if (priceAlert) {
priceAlert.classList.add('d-none');
}
// Удаляем ошибки валидации
const invalidFields = form.querySelectorAll('.is-invalid');
invalidFields.forEach(field => {
field.classList.remove('is-invalid');
});
const errorMessages = form.querySelectorAll('.invalid-feedback');
errorMessages.forEach(msg => {
msg.style.display = 'none';
});
}
});
});
Часть 4: Шаблон email уведомления (10 мин)
Файл: assets/elements/chunks/fetchit/email_pricing_order.tpl
{* Email: заказ тарифа *}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; background: #f5f5f5; padding: 20px; }
.container { max-width: 600px; margin: 0 auto; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; }
.content { padding: 30px; }
.info-table { width: 100%; border-collapse: collapse; margin: 20px 0; }
.info-table td { padding: 12px; border-bottom: 1px solid #e9ecef; }
.info-table td:first-child { font-weight: bold; color: #667eea; width: 40%; }
.price-box { background: #f8f9fa; border-left: 4px solid #667eea; padding: 15px; margin: 20px 0; }
.footer { background: #f8f9fa; padding: 20px; text-align: center; color: #999; font-size: 12px; }
</style>
</head>
<body>
<div class="container">
{* Header *}
<div class="header">
<h1 style="margin: 0;">🎉 Новый заказ тарифа!</h1>
<p style="margin: 10px 0 0 0; opacity: 0.9;">с сайта {$_modx->config.site_name}</p>
</div>
{* Content *}
<div class="content">
{* Тариф *}
<div class="price-box">
<h2 style="margin: 0 0 10px 0; color: #667eea;">Тариф: {$plan_name}</h2>
<p style="margin: 0; font-size: 24px; font-weight: bold;">
{$plan_price | number_format : 0 : ',' : ' '} ₽
<span style="font-size: 16px; font-weight: normal; color: #999;">/{$plan_period ?: 'мес'}</span>
</p>
</div>
{* Контактные данные *}
<h3 style="color: #333; border-bottom: 2px solid #f0f0f0; padding-bottom: 10px;">Контактные данные клиента</h3>
<table class="info-table">
<tr>
<td>Имя:</td>
<td>{$name}</td>
</tr>
<tr>
<td>Телефон:</td>
<td><a href="tel:{$phone}" style="color: #667eea; text-decoration: none;">{$phone}</a></td>
</tr>
<tr>
<td>Email:</td>
<td><a href="mailto:{$email}" style="color: #667eea; text-decoration: none;">{$email}</a></td>
</tr>
{if $comment}
<tr>
<td>Комментарий:</td>
<td>{$comment}</td>
</tr>
{/if}
</table>
{* CTA кнопка *}
<div style="text-align: center; margin: 30px 0;">
<a href="tel:{$phone}"
style="display: inline-block; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px 40px; text-decoration: none; border-radius: 50px; font-weight: bold;">
📞 Позвонить клиенту
</a>
</div>
</div>
{* Footer *}
<div class="footer">
<p style="margin: 5px 0;">Заказ создан: {date('d.m.Y H:i')}</p>
<p style="margin: 5px 0;">Страница: <a href="https://{$_modx->config.http_host}{$_modx->resource.uri}" style="color: #667eea;">{$_modx->resource.pagetitle}</a></p>
<p style="margin: 15px 0 5px 0;">IP: {$_SERVER['REMOTE_ADDR']}</p>
</div>
</div>
</body>
</html>
Часть 5: Обработка ответа FetchIt (опционально, 15 мин)
Создайте файл для кастомной обработки событий FetchIt:
Добавьте в конец файла: js/custom.js
/**
* fetchit_custom.js
* Кастомная обработка ответов FetchIt для формы тарифов
*/
document.addEventListener('DOMContentLoaded', () => {
// Событие успешной отправки формы
document.addEventListener('fetchit:success', (e) => {
const { form, response } = e.detail;
// Логика для формы заказа тарифа
if (form.id === 'pricingOrderForm') {
// Очищаем форму
form.reset();
// Показываем Toast уведомление (Bootstrap 5)
showToast('success', 'Спасибо! Ваша заявка принята. Мы свяжемся с вами в течение 15 минут.');
// Закрываем модалку через 2 секунды
setTimeout(() => {
const modalElement = document.getElementById('pricingModal');
if (modalElement) {
const modal = bootstrap.Modal.getInstance(modalElement);
if (modal) {
modal.hide();
}
}
}, 2000);
// Google Analytics 4 событие (если подключен)
if (typeof gtag !== 'undefined') {
const planName = form.querySelector('#planNameInput').value;
const planPrice = form.querySelector('#planPriceInput').value;
gtag('event', 'purchase_intent', {
'event_category': 'Pricing',
'event_label': planName,
'value': parseFloat(planPrice)
});
}
// Яндекс Метрика (если подключена)
if (typeof ym !== 'undefined') {
ym(XXXXXX, 'reachGoal', 'pricing_order');
}
}
});
// Событие ошибки отправки
document.addEventListener('fetchit:error', (e) => {
const { form, response } = e.detail;
if (form.id === 'pricingOrderForm') {
console.error('Ошибка отправки формы тарифа:', response);
showToast('danger', 'Произошла ошибка. Пожалуйста, попробуйте позже или позвоните нам.');
}
});
});
/**
* Функция показа Toast уведомления (Bootstrap 5)
*/
function showToast(type, message) {
// Создаём контейнер для Toast (если нет)
let toastContainer = document.getElementById('toastContainer');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.id = 'toastContainer';
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
toastContainer.style.zIndex = 9999;
document.body.appendChild(toastContainer);
}
// Создаём Toast элемент
const toastEl = document.createElement('div');
toastEl.className = `toast align-items-center text-bg-${type} border-0`;
toastEl.setAttribute('role', 'alert');
toastEl.setAttribute('aria-live', 'assertive');
toastEl.setAttribute('aria-atomic', 'true');
toastEl.innerHTML = `
<div class="d-flex">
<div class="toast-body">
<i class="fas fa-${type === 'success' ? 'check-circle' : 'exclamation-circle'} me-2"></i>
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
toastContainer.appendChild(toastEl);
// Показываем Toast
const toast = new bootstrap.Toast(toastEl, { delay: 5000 });
toast.show();
// Удаляем после скрытия
toastEl.addEventListener('hidden.bs.toast', () => {
toastEl.remove();
});
}
Опционально: маска телефона (IMask.js)
<script src="https://unpkg.com/imask"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const phoneInput = document.getElementById('customerPhone');
if (phoneInput) {
IMask(phoneInput, {
mask: '+{7} (000) 000-00-00'
});
}
});
</script>
Тестирование: проверочный чеклист
1. Цены отображаются?
- Откройте страницу с тарифами
- Должна быть цена под названием каждого тарифа
2. Data-атрибуты в кнопках?
- Правой кнопкой на «Заказать» → Inspect
- Должны быть атрибуты:
data-plan-name,data-plan-price,data-plan-period
3. Модалка открывается?
- Кликните «Заказать»
- Окно должно открыться
- Заголовок: «Заказать тариф Стандарт»
- Цена: «2 990 ₽/мес»
4. Данные передаются?
- Откройте Console (F12)
- Должна быть запись:
Модалка открыта с данными: {...}
5. Форма отправляется?
- Заполните поля
- Нажмите «Отправить заявку»
- Должно появиться Toast уведомление
- Модалка закроется через 2 сек
6. Email приходит?
- Проверьте почту из
emailsender - В письме должны быть название тарифа и цена
Итоги урока
Теперь ваш блок тарифов из урока «Сборка страниц» дополнен:
- Ценами с периодом оплаты
- Универсальной модальной формой FetchIt
- Data-атрибутами для передачи данных
- Email уведомлениями администратору
- Toast уведомлениями клиенту
- Интеграцией с аналитикой (GA4, Метрика)







