Перейти к содержимому

Кнопки, меню и диалоги ядра UI - состояния, форматы, ловушки

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

Механика

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

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

Кнопку можно нарисовать серверным кодом, а управлять ею с клиента. Серверная кнопка получает свой идентификатор, а на клиенте по нему достают готовый объект и меняют состояние, текст и обработчик.

Форматы пунктов различаются между расширениями, и это постоянный источник ошибок. В системном меню пункт описывается одними ключами, во всплывающем окне - другими, а перепутанные ключи молча не работают.

Диалог сам кнопок не создаёт: он показывает ровно то, что ему передали. Кнопки собирают отдельно и отдают диалогу объектами, поэтому оформление кнопки не зависит от самого диалога.

Разделение полезно на практике: одну и ту же кнопку можно показать и в диалоге, и в панели инструментов страницы. Обработчик при этом пишется один раз, а не копируется под каждое место вывода.

Оформление «неактивно» - это только внешний вид, а не запрет. Обработчик у выключенного на вид пункта продолжает срабатывать, и доступность проверяют своим кодом, а не цветом.

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

Шаги

  1. Подключить нужные расширения интерфейса до первого обращения к их объектам.
  2. Собрать кнопку и включать состояние ожидания на время каждого запроса.
  3. Для серверной кнопки задать идентификатор и достать её объект на клиенте.
  4. Меню и диалоги описывать в формате именно того расширения, которое подключено.
  5. Проверять доступность действия в коде, а не полагаться на внешний вид элемента.

Код

Подключаем расширения интерфейса:

\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();

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

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

Ограничения

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

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

Элементы рассчитаны прежде всего на административные интерфейсы платформы. Для публичной части их берут там, где важнее скорость разработки, а не точное соответствие макету дизайнера.

Обновление платформы приносит новые оформления и новые расширения интерфейса. Старый код продолжает работать, но выглядит иначе соседних экранов, и это обычная причина «поехавшего» вида после планового обновления.

Оформление «выключено» не запрещает само действие ни в одном из элементов. Любую проверку доступности пишут в коде обработчика, иначе действие выполнится по нажатию на бледную кнопку.

Типичные проблемы

Кнопка навсегда осталась заблокированной.

Состояние ожидания снимается только в успешной ветке ответа сервера. Его снимают и при ошибке запроса, иначе действие больше не повторить.

Объект кнопки на клиенте не находится.

Кнопка ещё не отрисована сервером либо на клиенте задан другой идентификатор. Объект достают после вывода разметки и по тому же имени.

Пункт меню не добавился и ошибок нет.

Идентификатор пункта уже занят другим пунктом того же меню. Идентификаторы пунктов делают уникальными, особенно при обновлении меню на лету.

Обработчик срабатывает у выключенного на вид элемента.

Оформление недоступности не отменяет назначенный элементу обработчик нажатия. Доступность действия проверяют в самом обработчике нажатия, а не оформлением.

Окно показывает старое содержимое.

Всплывающее окно с тем же идентификатором переиспользуется платформой по умолчанию. Окно создают заново или убирают за собой сразу после его закрытия.

Частые вопросы

Когда брать штатные элементы, а когда свои?

Штатные - в административной части и во внутренних интерфейсах, где важна скорость. На витрине с собственным дизайном обычно дешевле своя вёрстка.

Чем системное меню отличается от меню всплывающего окна?

Это разные расширения с разными форматами пунктов. Перепутанные ключи не вызывают ошибок, поэтому формат сверяют с тем расширением, которое подключено.

Как показать подтверждение удаления?

Диалогом с двумя кнопками, где опасное действие выделено оформлением. Кнопки собирают отдельно и передают диалогу объектами.

Можно ли поменять текст серверной кнопки без перезагрузки?

Да, через её объект на клиенте по заданному идентификатору. Так же меняют состояние ожидания и цвет.

Безопасно ли выводить в окно данные посетителя?

Только через содержимое текстового узла, а не вставкой разметки. Иначе чужой текст становится кодом на вашей странице.

Смежное

Первоисточник