Универсальная модальная форма для тарифов: FetchIt + data-атрибуты

FetchIt + data-атрибуты Курс

В предыдущих уроках мы создали блок тарифов (урок «Сборка страниц») и научились делать рабочие формы через FetchIt (урок «Интеграция форм»). Теперь решим типичную задачу: как сделать одну универсальную форму для всех тарифов, которая открывается в модальном окне и автоматически подставляет название выбранного тарифа.

Проблема: у вас на странице 3 тарифа (Базовый, Стандарт, Премиум). Каждая карточка имеет кнопку «Заказать». Создавать 3 отдельные формы — неэффективно.

Решение: одна универсальная форма в Bootstrap 5 Modal, которая получает данные о тарифе через data-атрибуты кнопок.

Структура файлов урока

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, Метрика)

 

Оцените статью
MODX 3
Добавить комментарий

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.