Конструктор форм

Приложение для Webasyst
Формы и виджеты для сбора заявок и обратной связи

«Конструктор форм» (formkit) — приложение Webasyst для форм обратной связи, заявок и виджетов на сайте. Соберите форму из 24 типов полей без единой строчки кода, оформите одной из 5 готовых тем (одна базовая и четыре премиум), встройте на сайт так, как удобно, и получайте заявки в Email, SMS, VK, Telegram, amoCRM или Bitrix24. Статусы, статистика и три уровня защиты от спама — уже внутри.

Формы разделены на поля и элементы — от простого текстового ввода до диапазона, рейтинга, карточек выбора и капчи. Каждое поле настраивается отдельно: маска ввода, ограничение длины со счётчиком символов, диапазон дат, подстановка значения по умолчанию со сдвигом от текущего момента.

Конструктор форм Webasyst — редактор полей

Уведомления о новой заявке приходят туда, где удобно: на email с HTML-шаблоном письма, по SMS, в VK, в Telegram — или сразу сделкой в amoCRM либо лидом в Bitrix24 с помощью плагинов. Каждый канал включается независимо для каждой формы.

Пять готовых тем оформления (Базовая и 4 премиум: Vega , Orion , Epsilon , Proxima ) задают не только цвета и шрифты, а весь визуальный стиль формы, попапа и виджета. Ширина, скругления, рамка, центрирование — без единой строчки CSS.

Конструктор форм Webasyst — выбор дизайна

Начало работы

Установите приложение через Инсталлер Webasyst — оно появится в общем списке приложений слева. При первой установке автоматически создаются 7 демо-форм и 2 демо-заявки, чтобы сразу увидеть, как это работает вживую.

  1. Откройте приложение «Конструктор форм» и нажмите «Создать форму».
  2. На вкладке «Добавление полей» соберите форму из нужных полей и элементов — кнопками «Добавить поле» / «Добавить элемент», порядок меняется перетаскиванием.
  3. На вкладке «Дизайн» выберите тип формы (кнопка с модальным окном, виджет, встройка в текст и т.д.) и оформление.
  4. На вкладке «Уведомления» включите каналы, куда должны приходить заявки.
  5. На вкладке «Встроить на сайт» скопируйте код и вставьте его в шаблон темы или в HTML-блок страницы.
<!-- Пример для формы с ID 1 -->
{$wa->formkit->getForm(1)}

Код вставки принимает номер формы — его видно в списке «Все формы» рядом с каждой строкой. Работает в любом месте, где разрешён Smarty: в шаблонах темы, на странице сайта, в блоге.


Размещение на сайте

Тип размещения задаётся на вкладке «Дизайн» формы, один и тот же код вставки работает для любого варианта — переключать тип можно в любой момент без изменения кода на странице.

Способ 1. Привязка формы к кнопкам или ссылкам на сайте

Скопируйте код формы и вставьте его в настройках темы дизайна, в раздел Основные настройкиПрочееДополнительный код , а если такой настройки в вашей теме нет — прямо в файл index.html. Так форма будет доступна на всех страницах сайта. Если форма нужна только на одной странице, код вставки можно разместить в настройках конкретной страницы в бэкенде Webasyst.

Конструктор форм Webasyst — размещение формы на сайте

При создании формы на вкладке «Дизайн» укажите тип Модальное окно без кнопки , а в настройке Селектор триггера — URL-хэш, например #mini-forma. Такой способ удобен тем, что клиенту можно сразу отправить ссылку с открытой формой, достаточно добавить этот хэш в конец адреса страницы: https://example.com/#zapis-na-priem.

Конструктор форм Webasyst — модальное окно без кнопки

После этого в теме дизайна в настройках любой кнопки или ссылки можно указать в качестве URL этот же хэш — и по нажатию на такую кнопку или ссылку будет открываться окно формы.

Конструктор форм Webasyst — привязка кнопки

Способ 2. Добавление кнопки с модальным окном

Если выбрать тип формы Кнопка с модальным окном , то кнопка будет выводиться там, куда вставлен код формы, например на текстовую страницу магазина. Добавление кода на такие страницы следует осуществлять исключительно в режиме HTML. По клику посетитель видит форму в попапе поверх страницы, сайт при этом не покидается и не перезагружается.

Модальное окно с формой в Конструкторе форм Webasyst

Способ 3. Добавление прямо в текст страницы

Если выбран тип формы Встроить на страницу , то форма будет выводиться прямо в том месте страницы, куда вставлен код, без кнопки и всплывающего окна. Для того, чтобы форма удачно вписалась на страницу, выберите подходящий размер формы, включите или отключите рамку. Если форма должна находиться в уже готовом контейнере со своими отступами, включите в настройках формы режим «Без отступов».

Форма, встроенная в текст страницы в Конструкторе форм Webasyst

Способ 4. Виджет в углу сайта и компактный виджет с формой

Для размещения виджета достаточно выбрать тип формы Виджет в нижнем углу сайта и один раз вставить код в файлы темы, например в index.html или в настройки темы дизайна, в раздел Основные настройкиПрочееДополнительный код . По клику посетителя форма разворачивается поверх содержимого страницы.


Дизайн и оформление

Вкладка «Дизайн» формы отвечает за тип размещения и визуальное оформление. В премиум-версии приложения есть возможность выбрать помимо дефолтного варианта оформления одну из 4 премиум-тем: Vega, Orion, Epsilon, Proxima. Меняет не только цвета, но и весь визуальный стиль полей. Премиум-оформление добавляет анимации кнопок, появления и исчезновения модальных окон.

Конструктор форм Webasyst — тонкая настройка дизайна формы

Защита от заявок и спама

На вкладке «Настройки» формы — три независимых уровня защиты, работают без дополнительных сервисов:

  • HoneyPot («Защита от спама», включена по умолчанию) — на форму добавляется скрытое поле, которое видят и заполняют только боты. Обычные посетители его не замечают, а заявка с заполненным скрытым полем тихо отбрасывается.
  • Ограничить отправку с одного IP — по умолчанию один IP-адрес сможет отправить не более 5 заявок в час, после достижения лимита форма скрывается от этого посетителя. Так же исключается спам-рассылка заявок с одного компьютера.
  • Ограничить работу формы по времени — форма принимает заявки только в указанный период, затем скрывается на сайте, а отправка перестаёт работать — в том числе у посетителей, которые открыли страницу заранее и с тех пор не обновляли её в браузере.

Четвёртый уровень — поле-элемент Капча , которое можно добавить в саму форму как обычное поле. Доступны два типа:

  • Стандартная капча Webasyst — картинка с кодом, ничего дополнительно настраивать не нужно.
  • Yandex SmartCaptcha — клиентский и серверный ключ выдаются на cloud.yandex.ru/services/smartcaptcha для конкретного домена сайта. Доступен невидимый режим — проверка идёт в фоне без взаимодействия посетителя, дополнительное окно появляется, только если поведение показалось Яндексу подозрительным.

SMS уведомления

Использует общий SMS-шлюз Webasyst — отдельного аккаунта заводить не нужно, но отправка платная: баланс и тариф настраиваются один раз в Настройках Webasyst → SMS, действуют сразу для всех приложений, использующих SMS.


VK уведомления

Уведомление приходит в личные сообщения сообщества ВКонтакте.

Шаг 1 Создайте ключ доступа

В настройках вашего сообщества откройте УправлениеДополнительноРабота с APIКлючи доступа и создайте новый ключ с правом «Сообщения сообщества». Такой токен не имеет срока действия, пока вы не отзовёте его вручную.

Шаг 2 Вставьте токен

Вставьте этот токен в поле «Токен сообщества» в приложении.

Шаг 3 Укажите ID получателя

В поле «ID получателя» укажите числовой ID профиля ВКонтакте (не логин и не ссылку на профиль) — можно несколько через запятую, максимум 5. Узнать свой числовой ID можно через сторонние сервисы, такие как regvk.com/id.

ВКонтакте разрешает сообществу писать только тем пользователям, которые уже сами хотя бы раз написали этому сообществу — иначе отправка не пройдёт. Это ограничение самого ВКонтакте. Если вы хотите получать уведомления, обязательно начните диалог (напишите первое сообщение) с вашим сообществом.
Конструктор форм Webasyst — VK уведомления

Telegram уведомления

Уведомление приходит от собственного Telegram-бота.

Шаг 1 Создайте бота

Создайте бота через @BotFather в Telegram: команда /newbot , следуйте подсказкам — в конце BotFather пришлёт токен, скопируйте его в поле «Токен бота».

Шаг 2 Узнайте свой Chat ID

Узнайте свой числовой Chat ID через @userinfobot — просто напишите ему (/start).

Шаг 3 Укажите Chat ID получателя

Вставьте ID в поле «Chat ID получателя», можно несколько через запятую, максимум 5. Для групп и каналов ID начинается со знака «−».

Telegram не позволяет боту первым написать пользователю, получатель должен сам хотя бы раз написать боту (например, отправить команду /start), иначе сообщения не будут доходить.
Конструктор форм Webasyst — Telegram уведомления

Интеграция с amoCRM

Отдельный плагин «Интеграция с amoCRM» — устанавливается через Инсталлер дополнительно к самому приложению. Подключение аккаунта amoCRM — одно на весь аккаунт Webasyst (раздел «Плагины»), а отправлять туда заявки или нет — решает каждая форма своим тумблером на вкладке «Уведомления».

Шаг 1 Создайте внешнюю интеграцию

В amoCRM откройте раздел «amoМаркет» → кнопку с тремя точками в правом верхнем углу → «Создать интеграцию»«Внешняя интеграция».

Шаг 2 Заполните данные интеграции

Заполните «Название интеграции» и «Описание» (обязательны для сохранения, подойдёт любой текст), в поле «Ссылка для перенаправления» вставьте значение из настроек плагина, обязательно отметьте «Предоставить доступ: Все».

Создание внешней интеграции в amoCRM
Шаг 3 Подключите amoCRM в плагине

После сохранения на вкладке «Установленные» скопируйте ID интеграции и секретный ключ, вставьте их вместе с поддоменом аккаунта в настройках плагина formkit и нажмите «Подключить amoCRM».

Конструктор форм, интеграция с amoCRM

После подключения плагин при каждой заявке ищет контакт в amoCRM по телефону или email — находит, привязывает к нему новую сделку и дополняет только пустые поля контакта; не находит — создаёт новый контакт. Заголовок сделки собирается по шаблону с переменными полей заявки.

На вкладке каждого поля формы есть блок «Отправить в amoCRM» — выпадающий список полей сделки вашего аккаунта (обновляется кнопкой в настройках плагина) либо телефон/email/имя контакта. Если поле сделки в amoCRM само является закрытым списком (выпадающий список, радио-кнопки, мультисписок), редактор вариантов формkit-поля автоматически блокируется и заполняется точным списком значений из amoCRM — это исключает расхождение текста между формой и CRM. На вкладке «Уведомления» формы можно также выбрать воронку, этап и ответственного менеджера для новой сделки.

Привязка поля формы к полю сделки amoCRM

Интеграция с Bitrix24

Отдельный плагин «Интеграция с Bitrix24». Подключение — входящим вебхуком, одно на весь портал, каждая форма включает канал своим тумблером на вкладке «Уведомления».

Шаг 1 Создайте входящий вебхук

В Bitrix24 откройте раздел Приложения → «Разработчикам»«Другое»«Входящий вебхук», в блоке «Настройка прав» доступа отметьте «CRM» и «Пользователи» и сохраните.

Шаг 2 Скопируйте ссылку вебхука

Bitrix24 выдаст готовую ссылку вида https://портал.bitrix24.ru/rest/1/xxxxxxxxxxxxxxxx/ — скопируйте её целиком (она содержит ключ доступа, храните как пароль).

Шаг 3 Подключите Bitrix24 в плагине

Вставьте ссылку в поле «Ссылка на вебхук» настроек плагина и нажмите «Подключить Bitrix24» — плагин сразу проверит связь с порталом.

Создание входящего вебхука в Bitrix24

Каждая заявка становится лидом. Поле «Поле в Bitrix24» на вкладке поля формы — телефон, email или имя лида (подставляются по умолчанию для полей соответствующего типа), любое произвольное поле лида из вашего портала, либо «Только в комментарии к лиду». Всё, что не привязано ни к одному полю, всё равно попадает в текст комментария — заявка никогда не теряет данные. На вкладке «Уведомления» формы можно выбрать этап и ответственного менеджера.

Привязка поля формы к полю лида Bitrix24

Премиум-возможности

  • 4 дополнительные темы дизайна — Vega, Orion, Epsilon, Proxima
  • Экспорт заявок в CSV — одна, выбранные или все сразу
  • Экспорт и импорт самих форм — перенос формы между сайтами файлом
  • Тестовая отправка для каналов Email, SMS, VK и Telegram (тестовая отправка в amoCRM и Bitrix24 премиум-ограничением не считается — это часть отдельных плагинов)

JS-события на сайте

Публичный скрипт формы диспатчит нативные CustomEvent на document — пригодятся, если нужно добавить свою реакцию на действия формы, например отправить событие в свою систему аналитики.

Свойство Тип данных Описание
formkit:render Форма инициализирована в DOM (один раз на форму на странице) { formNumber, root }
formkit:open Форма-попап или виджет открылась { formNumber }
formkit:submit Заявка успешно отправлена { formNumber, data }
document.addEventListener('formkit:submit', function (event) {
    console.log(event.detail.formNumber, event.detail.data);
})

document.addEventListener('formkit:open', function (event) {
    console.log(event.detail.formNumber);
})

document.addEventListener('formkit:render', function (event) {
    var root = event.detail.root; // корневой элемент .formkit-root
})

formNumber в event.detail совпадает с номером в коде вставки формы. Это удобно, если события нужно различать между несколькими формами на одной странице.


Хуки для плагинов

«Конструктор форм» поддерживает плагины на платформе Webasyst — функциональность расширяется без изменения кода ядра. Плагин размещается в wa-apps/formkit/plugins/[plugin_id]/ , обязательный файл lib/config/plugin.php ничем не отличается от плагинов других приложений Webasyst: name, description, version, vendor, handlers.

<?php

return [
    'name' => 'Моё название',
    'description' => 'Что делает плагин',
    'version' => '1.0.0',
    'vendor' => 1000000,
    'handlers' => [
        'formkit_submission_after_save' => 'onSubmissionSaved',
    ],
];
Каждый обработчик, указанный в handlers, обязан быть реализован соответствующим методом класса плагина, иначе событие не сработает, даже если метод существует, но не прописан в plugin.php.

Ниже все события приложения (хуки), в порядке момента срабатывания: сначала поток приёма заявки, затем регистрация новых типов полей и каналов уведомлений, инъекция интерфейса в бэкенде и на публичной форме, и отдельная публичная точка входа для плагинов без сессии Webasyst.

События приёма заявки

Все три события объявляются в момент обработки отправленной формы, до, во время и сразу после сохранения заявки.

formkit_submission_before_validate

Срабатывает после того, как определён список активных полей формы, до цикла валидации. Нужен плагинам условной логики — например, если поле «Компания» показывается только при выборе типа клиента «Юрлицо». Такой плагин проверяет значения полей и здесь помечает те, что должны быть скрыты по текущим условиям — тогда они не будут требоваться как обязательные при отправке формы.

Свойство Тип данных Описание
form array Форма (decoded, включая settings)
fields array Активные поля формы, без учёта скрытия
posted_fields array Значения из $_POST['fields'] после обрезки пробелов (trim)
hidden_field_ids array Заполняет обработчик — ID полей, которые считать скрытыми

Поля, чей id попал в hidden_field_ids , не проверяются на обязательность и не попадают в переменные уведомлений ( {$field_N} ). Их посланное значение всё равно сохраняется как есть.

Реально влияет на дальнейшую обработку только hidden_field_ids — именно его контроллер считывает обратно после события. Изменение form, fields или posted_fields в обработчике не отражается на валидации и сохранении заявки.
<?php

public function onBeforeValidate(&$params)
{
    foreach ($params['fields'] as $field) {
        $conditions = $field['config']['conditions'] ?? null;
        if ($conditions && !myConditionsEngine::isVisible($conditions, $params['posted_fields'])) {
            $params['hidden_field_ids'][] = (int)$field['id'];
        }
    }
}
formkit_submission_compute_fields

Срабатывает после успешной валидации, до сохранения заявки в БД. Назначение — плагины вычисляемых полей: пересчитать значение на сервере, не доверяя тому, что посчитал браузер.

Свойство Тип данных Описание
form array Форма
fields array Поля формы, уже прошедшие валидацию
data array [field_id => значение] — можно перезаписывать
posted_fields array Сырые значения из $_POST['fields']
<?php

public function onComputeFields(&$params)
{
    foreach ($params['fields'] as $field) {
        if ($field['type'] !== 'formula') {
            continue;
        }
        $params['data'][$field['id']] = myFormulaEngine::evaluate(
            $field['config']['expression'] ?? '',
            $params['data']
        );
    }
}
formkit_submission_after_save

Срабатывает сразу после сохранения заявки и отправки всех включённых уведомлений — не только штатных (email/SMS/VK/Telegram), но и через formkit_notify_channels (например, amoCRM или Bitrix24). Основное назначение — интеграции с внешними системами.

Свойство Тип данных Описание
submission_id int ID сохранённой заявки
form array Форма
data array [field_id => значение], уже после formkit_submission_compute_fields
vars array Готовые переменные для уведомлений: id, form_name, date, ip, field_N — человекочитаемые значения
<?php

public function onSubmissionSaved(&$params)
{
    if (empty($this->getSettings('enabled'))) {
        return;
    }
    myExternalApiClient::pushLead($params['form'], $params['vars']);
}
Для интеграций с CRM есть более удобный путь — регистрация как канала уведомлений (formkit_notify_channels, ниже), с готовым UI: тумблер, настройки, кнопка теста. Событие formkit_submission_after_save при этом остаётся рабочим для тех, кому весь этот UI не нужен.

Реестры

В отличие от событий выше, эти два события — регистрация: обработчик не реагирует на что-то произошедшее, а объявляет новую сущность, которую ядро начинает использовать наравне со встроенными. Оба срабатывают один раз за запрос.

formkit_field_types

Регистрирует новый тип поля — ядро после этого умеет его рендерить на публичной форме и подставлять значение в переменные уведомлений.

Свойство Тип данных Описание
types array Заполняет обработчик — массив описаний новых типов
types[].type string Машинное имя типа. Совпадает с уже существующим — регистрация игнорируется
types[].template_path string Абсолютный путь к Smarty-шаблону рендера поля на публичной форме
types[].notification_variable bool Участвует ли значение поля в {$field_N} — необязательно
types[].format_callback callable function(string $value, array $config): string — форматирование значения для уведомления и CSV, необязательно
<?php

class myFormulaPlugin extends waPlugin
{
    public function registerFieldTypes(&$params)
    {
        $params['types'][] = [
            'type' => 'formula',
            'template_path' => $this->path . '/templates/public/fields/formula.html',
            'notification_variable' => true,
            'format_callback' => ['myFormulaHelper', 'formatValue'],
        ];
    }
}
formkit_notify_channels

Регистрирует канал уведомлений — появляется на вкладке «Уведомления» формы наравне с Email/SMS/VK/Telegram и участвует в отправке при получении заявки. Именно так реализованы плагины amoCRM и Bitrix24.

Свойство Тип данных Описание
channels array Заполняет обработчик — массив описаний каналов
channels[].id string Машинное имя канала, например amocrm
channels[].label string Название в UI, необязательно — по умолчанию id
channels[].enabled_key string Ключ в form.settings, хранящий состояние тумблера
channels[].settings_template string Абсолютный путь к Smarty-партиалу настроек канала, необязательно
channels[].plugin_id string ID вашего плагина — обязателен вместе с settings_template, иначе переводы резолвятся по домену локализации formkit
channels[].send_callback callable function(array $form, array $vars, array $attachments): void — вызывается при получении заявки
channels[].test_callback callable function(array $form, array $vars): array{success, error, sent_at} — кнопка тестовой отправки, необязательно

Регистрация без send_callback или enabled_key игнорируется целиком. Тумблер «Включено/Выключено» сохраняется автоматически ядром, плагину не нужен свой JS для этого. Сохранение остальных полей внутри settings_template (токены, адреса и т.п.) забота плагина , своим экшеном.

<?php

class myAmoCrmPlugin extends waPlugin
{
    public function registerNotifyChannel(&$params)
    {
        $params['channels'][] = [
            'id' => 'amocrm',
            'label' => 'amoCRM',
            'enabled_key' => 'notify_amocrm_enabled',
            'settings_template' => $this->path . '/templates/notify/AmoCrmSettings.html',
            'plugin_id' => 'amo',
            'send_callback' => ['myAmoCrmSender', 'send'],
        ];
    }
}

Инъекция интерфейса

formkit_field_edit_sections

Добавляет свою секцию в единый диалог редактирования поля в бэкенде — тот же диалог для всех типов полей, секции показываются и скрываются в зависимости от выбранного типа. Срабатывает один раз на загрузку страницы редактора формы, не на каждое открытие диалога.

Свойство Тип данных Описание
sections array Заполняет обработчик — массив описаний секций
sections[].html string Готовый HTML секции, отрендеренный на вашей стороне
sections[].types array Список типов полей, для которых показывать секцию. Не указано — секция показывается для всех типов

HTML рендерьте сами через wa()->getView()->fetch(...) — ядро его не экранирует, только оборачивает в контейнер, который сам показывается и скрывается при переключении типа поля в диалоге.

<?php

class myConditionsPlugin extends waPlugin
{
    public function registerFieldEditSection(&$params)
    {
        $params['sections'][] = [
            'html' => wa()->getView()->fetch($this->path . '/templates/FieldEditSection.html'),
        ];
    }
}

Диалог рендерится один раз на страницу, поэтому чтобы секция знала, для какого именно поля она сейчас открыта, и получила его полный конфиг. Строка таблицы полей несёт весь конфиг поля в data-config (JSON), а на document диспатчится JS-событие formkit:field-edit-open в конце открытия диалога — с detail.fieldId , detail.type (текущий тип поля) и detail.row :

document.addEventListener('formkit:field-edit-open', function (event) {
    var fieldId = event.detail.fieldId;
    var type = event.detail.type;
    var row = event.detail.row;
    var config = JSON.parse(row.dataset.config || '{}');
    var myValue = config.my_plugin_key ?? '';
    // выставить значение в свою секцию, повесить onchange с сохранением
})

Сохранение своего ключа не через общий экшен обновления поля (он не пропускает произвольные ключи), а прямым обновлением конфига поля из своего экшена:

<?php

$model = new formkitFormFieldModel();
$field = $model->getById($fieldId); // config тут ещё JSON-строка
$config = json_decode($field['config'], true) ?: [];
$config['my_plugin_key'] = waRequest::post('value', '', 'string');
$model->updateById($fieldId, ['config' => json_encode($config, JSON_UNESCAPED_UNICODE)]);
formkit_field_render

Срабатывает на публичной форме отдельно для каждого поля, перед тем как оно рендерится. Позволяет добавить свои data-* атрибуты в корневой элемент поля, например, сериализованные условия показа, которые потом читает JS плагина через formkit:render .

Свойство Тип данных Описание
field array Поле формы (id, type, name, config — уже с применёнными дефолтами)
attrs array Заполняет обработчик — [имя_атрибута => значение], экранируются ядром автоматически при выводе
attrs — единственный безопасный канал вывода на публичную страницу. Не пытайтесь передать туда готовый HTML, он будет показан как текст.
<?php

public function onFieldRender(&$params)
{
    $conditions = $params['field']['config']['conditions'] ?? null;
    if ($conditions) {
        $params['attrs']['data-my-conditions'] = json_encode($conditions, JSON_UNESCAPED_UNICODE);
    }
}

Публичная точка входа

formkit_public_callback

Нужен, если плагину требуется публичный URL, доступный без сессии Webasyst. Типичный случай: OAuth-колбэк (внешний сервис возвращает браузер администратора с ?code=...&state=...) или вебхук от стороннего API. Собственный routing.php плагина для этого не годится, formkit даёт один универсальный публичный роут ( plugin-callback/<plugin_id>/<callback_id>/ ), на любой запрос по которому генерируется это событие.

Свойство Тип данных Описание
plugin_id string Вход — сегмент &lt;plugin_id&gt; из URL
callback_id string Вход — сегмент &lt;callback_id&gt; из URL, удобно для нескольких разных колбэков
handled bool Заполняет обработчик — установите true, если колбэк ваш и вы его обработали. Если после события handled остался false, formkit отдаёт 404
<?php

class myAmoCrmPlugin extends waPlugin
{
    public function handlePublicCallback(&$params)
    {
        if ($params['plugin_id'] !== 'myamocrm' || $params['callback_id'] !== 'redirect') {
            return; // не наш колбэк — молчим
        }
        $params['handled'] = true;

        $code = waRequest::get('code', '', 'string');
        // обмен кода на токены, сохранение в saveSettings()
        echo '<p>Готово, можно закрыть вкладку.</p>';
    }
}

Обработчик сам решает, что выводить — обычно echo HTML или header('Location: ...') с exit, formkit ничего не подмешивает после события.