Кнопки, меню и диалоги ядра UI - состояния, форматы, ловушки
Собираем интерфейс на штатных элементах ядра: кнопка с ожиданием, кнопка из серверного кода, системное меню и диалог подтверждения.
Механика
Элементы интерфейса живут в отдельных расширениях, и каждое подключают явно. Кнопки, всплывающие окна, меню и диалоги - это разные расширения, а не один общий набор, и подключение одного не тянет остальные.
Кнопка умеет состояние ожидания, и это её главное практическое свойство. На время запроса кнопку блокируют и показывают индикатор, чтобы посетитель не нажал её второй раз и не создал два одинаковых заказа.
Кнопку можно нарисовать серверным кодом, а управлять ею с клиента. Серверная кнопка получает свой идентификатор, а на клиенте по нему достают готовый объект и меняют состояние, текст и обработчик.
Форматы пунктов различаются между расширениями, и это постоянный источник ошибок. В системном меню пункт описывается одними ключами, во всплывающем окне - другими, а перепутанные ключи молча не работают.
Диалог сам кнопок не создаёт: он показывает ровно то, что ему передали. Кнопки собирают отдельно и отдают диалогу объектами, поэтому оформление кнопки не зависит от самого диалога.
Разделение полезно на практике: одну и ту же кнопку можно показать и в диалоге, и в панели инструментов страницы. Обработчик при этом пишется один раз, а не копируется под каждое место вывода.
Оформление «неактивно» - это только внешний вид, а не запрет. Обработчик у выключенного на вид пункта продолжает срабатывать, и доступность проверяют своим кодом, а не цветом.
Всплывающие окна по умолчанию кэшируются между показами на странице. Окно с тем же идентификатором переиспользуется, и если содержимое каждый раз новое, окно создают заново или убирают за собой.
Шаги
- Подключить нужные расширения интерфейса до первого обращения к их объектам.
- Собрать кнопку и включать состояние ожидания на время каждого запроса.
- Для серверной кнопки задать идентификатор и достать её объект на клиенте.
- Меню и диалоги описывать в формате именно того расширения, которое подключено.
- Проверять доступность действия в коде, а не полагаться на внешний вид элемента.
Код
Подключаем расширения интерфейса:
\Bitrix\Main\UI\Extension::load(['ui.buttons', 'ui.system.menu', 'ui.system.dialog']);// каждое расширение подключают явно: одно не тянет за собой остальныеПодключение делают на странице или в компоненте, где элементы используются. Забытое расширение проявляется ошибкой об отсутствующем объекте в консоли, а не пустым местом на странице.
Список подключаемых расширений держат коротким и осознанным. Каждое тянет свои файлы стилей и скриптов, и десяток лишних подключений на странице заметно утяжеляет её загрузку.
Кнопка с состоянием ожидания:
import { Button, ButtonColor, ButtonState } from 'ui.buttons';
const button = new Button({ text: 'Выгрузить', color: ButtonColor.PRIMARY });button.setWaiting(true); // блокировка и индикатор на время запросаBX.ajax.runAction('vendor:export.Start.run') .then(() => { button.setWaiting(false); button.setState(ButtonState.ACTIVE); }) .catch(() => button.setWaiting(false)); // состояние снимают и при отказеСостояние ожидания снимают в обеих ветках ответа. Забытое снятие при ошибке оставляет кнопку заблокированной навсегда, и посетитель перезагружает страницу, чтобы повторить действие.
Кнопка из серверного кода:
use Bitrix\UI\Buttons\{Button, Color, JsCode};
$button = new Button([ 'text' => 'Сохранить', 'color' => Color::PRIMARY, 'onclick' => new JsCode("console.log('сохранение');"),]);$button->setUniqId('vendor-save-button'); // по этому имени кнопку найдут на клиентеecho $button->render();Берём её объект на клиенте:
import { ButtonManager } from 'ui.buttons';
const button = ButtonManager.createByUniqId('vendor-save-button');if (button) { button.setWaiting(true); } // объект есть только после отрисовкиСерверная кнопка удобна там, где разметку собирает шаблон компонента. Клиентский код при этом не рисует её заново, а получает готовый объект и управляет им как своим.
Системное меню с опасным пунктом:
import { Menu, MenuItemDesign } from 'ui.system.menu';
const menu = new Menu({ items: [ { id: 'edit', title: 'Редактировать', onClick: () => openEditor() }, { id: 'delete', title: 'Удалить', design: MenuItemDesign.Alert, onClick: () => confirmDelete() },]});menu.show(document.getElementById('actions-button'));Опасное действие красят отдельным оформлением, а не своим классом стилей. Идентификаторы пунктов делают уникальными: пункт с уже занятым идентификатором добавиться не сможет, и меню окажется короче ожидаемого.
Меню удобно собирать данными, а не разметкой: список пунктов приходит массивом. Тогда доступные действия легко фильтруются правами прямо перед показом, и лишние пункты просто не попадают в набор.
Диалог подтверждения с кнопками:
import { Dialog } from 'ui.system.dialog';import { Button } from 'ui.buttons';
const content = document.createElement('div');content.textContent = 'Отменить действие будет нельзя.'; // безопасно для чужого текстаconst dialog = new Dialog({ title: 'Удалить элемент', content, width: 420, hasOverlay: true, // затемнение под окном rightButtons: [{ render: () => okButton.render() }] });dialog.show();Текст в диалог кладут через содержимое узла, а не готовой разметкой. Так пользовательские данные не превращаются в разметку, и подтверждение не становится способом выполнить чужой скрипт на странице.
Диалог подтверждения стоит показывать только для необратимых действий. Если подтверждение выскакивает на каждый чих, сотрудники перестают читать его текст и нажимают согласие не глядя.
Ограничения
Оформление элементов задаёт платформа, и вписать их в любой макет не выйдет. На витрине с собственным дизайном штатные кнопки часто выглядят чужеродно, и там берут свою вёрстку.
Названия ключей различаются между расширениями и версиями платформы. Пример из чужой статьи может не подойти вашей сборке, и формат сверяют с документацией той версии, что стоит на проекте.
Элементы рассчитаны прежде всего на административные интерфейсы платформы. Для публичной части их берут там, где важнее скорость разработки, а не точное соответствие макету дизайнера.
Обновление платформы приносит новые оформления и новые расширения интерфейса. Старый код продолжает работать, но выглядит иначе соседних экранов, и это обычная причина «поехавшего» вида после планового обновления.
Оформление «выключено» не запрещает само действие ни в одном из элементов. Любую проверку доступности пишут в коде обработчика, иначе действие выполнится по нажатию на бледную кнопку.
Типичные проблемы
Кнопка навсегда осталась заблокированной.
Состояние ожидания снимается только в успешной ветке ответа сервера. Его снимают и при ошибке запроса, иначе действие больше не повторить.
Объект кнопки на клиенте не находится.
Кнопка ещё не отрисована сервером либо на клиенте задан другой идентификатор. Объект достают после вывода разметки и по тому же имени.
Пункт меню не добавился и ошибок нет.
Идентификатор пункта уже занят другим пунктом того же меню. Идентификаторы пунктов делают уникальными, особенно при обновлении меню на лету.
Обработчик срабатывает у выключенного на вид элемента.
Оформление недоступности не отменяет назначенный элементу обработчик нажатия. Доступность действия проверяют в самом обработчике нажатия, а не оформлением.
Окно показывает старое содержимое.
Всплывающее окно с тем же идентификатором переиспользуется платформой по умолчанию. Окно создают заново или убирают за собой сразу после его закрытия.
Частые вопросы
Когда брать штатные элементы, а когда свои?
Штатные - в административной части и во внутренних интерфейсах, где важна скорость. На витрине с собственным дизайном обычно дешевле своя вёрстка.
Чем системное меню отличается от меню всплывающего окна?
Это разные расширения с разными форматами пунктов. Перепутанные ключи не вызывают ошибок, поэтому формат сверяют с тем расширением, которое подключено.
Как показать подтверждение удаления?
Диалогом с двумя кнопками, где опасное действие выделено оформлением. Кнопки собирают отдельно и передают диалогу объектами.
Можно ли поменять текст серверной кнопки без перезагрузки?
Да, через её объект на клиенте по заданному идентификатору. Так же меняют состояние ожидания и цвет.
Безопасно ли выводить в окно данные посетителя?
Только через содержимое текстового узла, а не вставкой разметки. Иначе чужой текст становится кодом на вашей странице.
Смежное
- Свои расширения JS - оглавление подтемы
- Окна и уведомления ядра UI: подтверждение, всплывающее окно, сообщение - соседний набор элементов
- Боковая панель: открытие страницы, события, запрет закрытия - страница поверх страницы
- Своё расширение JS: каталог, зависимости, подключение - как подключают эти расширения
- Расширение JS не подключается: разбор причин - если объекта нет в консоли
- AJAX-действия своего компонента: действия, параметры, ошибки - запрос за кнопкой
- Ядро BX и AJAX - устройство клиентского ядра