Всем привет! Меня зовут Илья, я разработчик в Битрикс24. В последнее время наша команда стремится быть прозрачнее и делиться изменениями в продукте. Мы хотим, чтобы разработчики, использующие Битрикс24, быстрее узнавали об обновлениях и имели на руках актуальную документацию. Это поможет меньше велосипедить и искать решения на стороне.
Знаю, раньше и Битрикс, и его блоги собирали много хейта. Да что говорить, я сам одно время работал на партнеров и не всем был доволен. Даже писал критические посты. Однако всё это время продукт не стоял на месте, Битрикс24 времен 2014 и сейчас — две разных вселенных. До сих пор не всё идеально, но постоянно происходят улучшения.
Об одном из них, простом и полезном, расскажу сегодня. Ранее у нас не было хорошо задокументированного коробочного решения по гридам. Если стояла задача вывести в публичной части информацию в виде таблиц, мы вручную пилили шаблоны для элементов и искали костыли для сортировки данных. Проблема возникала часто: например, если нужно было вывести список товаров, сделок или клиентов, а еще лучше — интерактивные списки.
Впереди мало слов и много кода. Если останутся вопросы или замечания, жду вас в комментах.
Как выглядят гриды в Битрикс24
Гриды — элементы интерфейса в Битрикс24, отображаемые как списки. Это может быть список складских документов, перечень сделок, список кандидатов для отбора персонала и многое другое.
Мы уже используем встроенные гриды во внутренней разработке: это проще и эстетичнее, чем сторонние решения. Вот как выглядит стандартный грид:
Раньше разработка гридов выполнялась на базе Bootstrap или других UI-библиотек и сопровождалась кучей кода — чтобы реализовать пагинацию, сортировку и фильтры. Сейчас можно не писать дополнительные страницы и контроллеры и не возиться с шаблонами для правильного отображения строк.
Пошаговый гайд по гридам: от простого к сложному
Покажу на примере, как шаг за шагом создавать и совершенствовать грид, накидывая функционал. Возьмем в качестве иллюстрации список сотрудников.
Шаг 1. Создаем класс грида
На этом шаге выполняем минимальные действия по подключению к API Битрикс24 и выводу грида. Для пользователей существует таблет UserTable, грид будем делать на его основе. Первым делом создаем класс самого грида:
<?php
use Bitrix\Main\Grid\TabletGrid;
use Bitrix\Main\UserTable;
final class UserGrid extends TabletGrid
{
protected function getTabletClass(): string
{
return UserTable::class;
}
}
Шаг 2. Выводим грид
Просто так вывалить код в статический файлик из архитектурных соображений нельзя. Добавим кастомный компонент user.grid со следующим содержимым:
<?php
use Bitrix\Main\Grid\Component\GridComponent;
use Bitrix\Main\Grid\Grid;
use Bitrix\Main\Grid\Settings;
use Bitrix\Main\UserTable;
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true)
{
die();
}
final class UserGridComponent extends TabletGridComponent
{
protected function createGrid(): Grid
{
$settings = new Settings([
'ID' => 'user-grid',
]);
$grid = new UserGrid($settings);
return $grid;
}
protected function getTablet(): string
{
return UserTable::class;
}
}
Сам шаблон компонента (templates/.default/template.php) будет выглядеть так:
<?php
use Bitrix\Main\Grid\Component\ComponentParams;
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true)
{
die();
}
/**
* @var array $arParams
* @var array $arResult
* @var CMain $APPLICATION
* @var CBitrixComponent $component
* @var CBitrixComponentTemplate $this
*/
$APPLICATION->IncludeComponent(
'bitrix:main.ui.grid',
'',
ComponentParams::get(
$arResult['GRID']
)
);
Размещаем компонент на нужной странице и любуемся результатом:
Шаг 3. Работаем со списком отображаемых столбцов
По умолчанию выводятся все не приватные и скалярные поля таблета. За это отвечает провайдер TabletColumnsProvider, создающий список столбцов. Если нужно его изменить, вы можете ограничить список отображаемых столбцов. Создаем класс UserColumns, который будет отвечать за формирование столбцов:
<?php
use Bitrix\Main\Grid\Column\Columns;
use Bitrix\Main\Grid\Column\DataProvider\TabletColumnsProvider;
use Bitrix\Main\Grid\Column\DataProvider\UfColumnsProvider;
use Bitrix\Main\ORM\Entity;
final class UserColumns extends Columns
{
public function __construct(Entity $entity)
{
parent::__construct(
new TabletColumnsProvider(
$entity,
[
'ID',
'ACTIVE',
'LOGIN',
'EMAIL',
'NAME',
'SECOND_NAME',
'LAST_NAME',
'DATE_REGISTER',
'LAST_LOGIN',
],
),
);
}
}
Давайте в классе грида переопределим метод createColumns, отвечающий за создание столбцов:
<?php
use Bitrix\Main\Grid\TabletGrid;
final class UserGrid extends TabletGrid
{
// ...
protected function createColumns(): Columns
{
return new UserColumns(
$this->getEntity()
);
}
}
В данном случае используется базовый класс Columns. В качестве аргументов конструктор этого класса принимает список провайдеров, формирующих список столбцов. По умолчанию провайдер TabletColumnsProvider выводит все поля таблета, чтобы это изменить, достаточно передать вторым аргументом массив со списком полей.
Шаг 4. Управляем отображением грида
Виджет грида позволяет управлять отображением гридов по умолчанию. Если, например, мы не хотим по умолчанию показывать столбцы с датой последнего входа, ID и отчеством, нужно перечислить остальные поля третьим параметром у провайдера TabletColumnsProvider:
<?php
use Bitrix\Main\Grid\Column\Columns;
use Bitrix\Main\Grid\Column\DataProvider\TabletColumnsProvider;
use Bitrix\Main\Grid\Column\DataProvider\UfColumnsProvider;
use Bitrix\Main\ORM\Entity;
final class UserColumns extends Columns
{
public function __construct(Entity $entity)
{
parent::__construct(
new TabletColumnsProvider(
$entity,
selectFields: [
'ID',
'ACTIVE',
'LOGIN',
'EMAIL',
'NAME',
'SECOND_NAME',
'LAST_NAME',
'DATE_REGISTER',
'LAST_LOGIN',
],
defaultFields: [
'ACTIVE',
'LOGIN',
'EMAIL',
'NAME',
'LAST_NAME',
'DATE_REGISTER',
],
),
);
}
}
При отображении грида будут показаны только столбцы из аргумента defaultFields, но через шестеренку мы сможем при желании также выбрать и настроить другие столбцы:
Если нужно скрыть все столбцы таблета, можно использовать третий параметр isDefaultShow:
<?php
use Bitrix\Main\Grid\Column\Columns;
use Bitrix\Main\Grid\Column\DataProvider\TabletColumnsProvider;
use Bitrix\Main\ORM\Entity;
final class UserColumns extends Columns
{
public function __construct(Entity $entity)
{
parent::__construct(
new TabletColumnsProvider(
$entity,
selectFields: [
'ID',
'ACTIVE',
'LOGIN',
'EMAIL',
'NAME',
'SECOND_NAME',
'LAST_NAME',
'DATE_REGISTER',
'LAST_LOGIN',
],
isDefaultShow: false,
),
);
}
}
Тогда все столбцы из провайдера не будут отображаться сразу, а их можно будет только выбрать через шестеренку.
Шаг 5. Привязываем пользовательские поля и работаем с ассемблерами
Помимо обычных полей, к таблетам (и множеству других сущностей) можно привязывать «пользовательские поля». Дополним грид такими полями, добавив к коллекции столбцов провайдер UfColumnsProvider:
<?php
use Bitrix\Main\Grid\Column\Columns;
use Bitrix\Main\Grid\Column\DataProvider\TabletColumnsProvider;
use Bitrix\Main\Grid\Column\DataProvider\UfColumnsProvider;
use Bitrix\Main\ORM\Entity;
final class UserColumns extends Columns
{
public function __construct(Entity $entity)
{
parent::__construct(
new TabletColumnsProvider(
$this->getEntity()
),
new UfColumnsProvider(
$this->getEntity()->getUfId(),
),
);
}
}
Так в гриде появятся все пользовательские поля, настроенные для отображения в списке. Обратите внимание, что у них в настройках не должно стоять галочки «Не показывать в списке»:
С точки зрения настроек провайдер UfColumnsProvider поддерживает те же аргументы, что и TabletColumnsProvider: selectFields, defaultFields и isDefaultShow.
Коллекция столбцов Columns отвечает только за формирование списка столбцов. За отображение отвечают классы-сборщики RowAssembler и FieldAssembler. Задача этих классов заключается в сборке сырых данных и трансформации в пригодный для вывода вид.
ВАЖНО: сборщики — это необязательный элемент грида, если стандартные типы позволяют выводить информацию корректно.
Шаг 6. Добавляем UF
В примере с гридом сотрудников можно заметить, что поле «Подразделения» выводит не список подразделений, а строку Array. Чтобы UF поля выводились правильно, нужно добавить сборщик UfFieldAssembler. По аналогии со столбцами добавим класс UserRows и укажем нужный сборщик:
<?php
use Bitrix\Main\Grid\Row\Assembler\DynamicRowAssembler;
use Bitrix\Main\Grid\Row\Assembler\Field\UfFieldAssembler;
use Bitrix\Main\Grid\Row\Rows;
use Bitrix\Main\ORM\Entity;
final class UserRows extends Rows
{
public function __construct(array $visibleColumnIds, Entity $entity)
{
$rowAssembler = new DynamicRowAssembler(
$visibleColumnIds,
new UfFieldAssembler(
$entity->getUfId(),
),
);
parent::__construct($rowAssembler);
}
}
Затем используем созданный класс в гриде:
<?php
use Bitrix\Main\Grid\Row\Rows;
use Bitrix\Main\Grid\TabletGrid;
final class UserGrid extends TabletGrid
{
// ...
protected function createRows(): Rows
{
return new UserRows(
$this->getVisibleColumnsIds(),
$this->getEntity()
);
}
}
Важно обратить внимание на использование метода getVisibleColumnsIds. Для оптимальной работы сборщиков необходимо передавать только отображаемые столбцы и не подготавливать вывод для тех столбцов, которые выводить необязательно. Так мы получим корректное отображение всех используемых UF полей:
Из коробки у нас также имеются следующие сборщики:
HtmlFieldAssembler.
UserFieldAssembler.
StringFieldAssembler.
NumberFieldAssembler.
ListFieldAssembler.
Шаг 7. Идем к контекстным действиям
Ок, у нас есть строчка по сотруднику, а если мы хотим добавить к ней конкретные типовые действия внутри? Допустим, уволить сотрудника или принять обратно. Добавим два асимметричных действия, чтобы по клику выводилась либо скрывалась нужная информация.
Поговорим про контекстные действия — они отображаются возле строки в «гамбургере». За их работу отвечает интерфейс и класс, который содержит в себе типовой код формирования кнопки в меню. Для сотрудников мы можем реализовать два противоположных действия: «уволить» и «принять обратно». Реализуем оба действия (пока без логики обработки):
<?php
use Bitrix\Main\Grid\Row\Action\BaseAction;
use Bitrix\Main\HttpRequest;
use Bitrix\Main\Result;
final class HireAction extends BaseAction
{
public static function getId(): ?string
{
return 'hire';
}
protected function getText(): string
{
return 'принять обратно';
}
public function processRequest(HttpRequest $request): ?Result
{
return null;
}
}
final class FireAction extends BaseAction
{
public static function getId(): ?string
{
return 'fire';
}
protected function getText(): string
{
return 'уволить';
}
public function processRequest(HttpRequest $request): ?Result
{
return null;
}
}
Далее добавим провайдер наших действий:
<?php
use Bitrix\Main\Grid\Row\Action\DataProvider;
final class UserContextActionDataProvider extends DataProvider
{
public function prepareActions(): array
{
return [
new FireAction(),
new HireAction(),
];
}
}
Используем провайдер в уже созданном классе строк UserRows:
<?php
use Bitrix\Main\Grid\Row\Assembler\DynamicRowAssembler;
use Bitrix\Main\Grid\Row\Assembler\Field\UfFieldAssembler;
use Bitrix\Main\Grid\Row\Rows;
use Bitrix\Main\ORM\Entity;
final class UserRows extends Rows
{
public function __construct(array $visibleColumnIds, Entity $entity)
{
$rowAssembler = new DynamicRowAssembler(
$visibleColumnIds,
new UfFieldAssembler(
$entity->getUfId(),
),
);
parent::__construct(
$rowAssembler,
new UserContextActionDataProvider(),
);
}
}
Мы видим, что возле строк появился виджет «гамбургера», нажав на который можно увидеть оба действия:
Логично предположить, что отображать обе кнопки одновременно — бессмысленно. Давайте доработаем код так, чтобы отображалась одна из кнопок, в зависимости от текущего статуса сотрудника. Для этого переопределим метод getControl у обоих действий, который отвечает за вывод контрола. Если данный метод вернёт null, в меню ничего не будет отображаться:
<?php
final class HireAction extends BaseAction
{
// ...
public function getControl(array $rawFields): ?array
{
if ($rawFields['ACTIVE'] === 'Y')
{
return null;
}
return parent::getControl($rawFields);
}
}
Наблюдательный читатель наверняка уже заметил проблему в коде: если столбец ACTIVE не будет отображаться (скрыт настройками), то также он не будет содержаться в аргументе $rawFields. Чтобы этого не происходило, нужно отметить столбец ACTIVE обязательным. Тогда он будет выбираться из базы данных всегда, независимо от того, отображается он сейчас или нет. Добьемся этого, добавив метод UserColumns::prepareColumns с таким содержимым:
<?php
final class UserColumns extends Columns
{
// ...
protected function prepareColumns(array $columns): array
{
foreach ($columns as $column)
{
if ($column->getId() === 'ACTIVE')
{
$column->setNecessary(true);
}
}
return $columns;
}
}
Наконец, реализуем логику для обоих действий в два этапа:
создаём JS-обработчик нажатия на кнопку для фронтенда;
создаём PHP-обработчик для бэкенде.
В подавляющем большинстве случаев для реализации JS-обработчика достаточно использовать класс SendRowActionOnclick. Он первым аргументом принимает само действие, а вторым — полезную нагрузку, которая отправится на бэкенд. Остальные случаи — это сложная логика, которая обрабатывается на фронтенде. Ей, возможно, и бэкенд-обработчик не нужен. За логику отвечает свойство onclick:
<?php
use Bitrix\Main\Grid\Row\Action\BaseAction;
use Bitrix\Main\Grid\Row\Action\Control\SendRowActionOnclick;
final class HireAction extends BaseAction
{
// ...
public function getControl(array $rawFields): ?array
{
if ($rawFields['ACTIVE'] === 'Y')
{
return null;
}
$this->onclick = new SendRowActionOnclick($this, [
'id' => $rawFields['ID'],
]);
return parent::getControl($rawFields);
}
}
Обработчик на бэкенде должен принять эти данные и обработать по своей логике. За это отвечает метод processRequest:
<?php
use Bitrix\Main\Error;
use Bitrix\Main\Grid\Row\Action\BaseAction;
use Bitrix\Main\HttpRequest;
use Bitrix\Main\Result;
final class HireAction extends BaseAction
{
// ...
public function processRequest(HttpRequest $request): ?Result
{
$result = null;
$userId = (int)$request->get('id');
if ($userId > 0)
{
$user = new CUser();
$user->Update($userId, [
'ACTIVE' => 'Y',
]);
if ($user->LAST_ERROR)
{
$result = new Result();
$result->addError(
new Error($user->LAST_ERROR)
);
}
}
return $result;
}
}
Опять же, обращаем внимание на использование $rawFields['ID'] и не забываем сделать его обязательным:
<?php
final class UserColumns extends Columns
{
// ...
protected function prepareColumns(array $columns): array
{
$necessaryColumns = [
'ID',
'ACTIVE',
];
foreach ($columns as $column)
{
if (in_array($column->getId(), $necessaryColumns))
{
$column->setNecessary(true);
}
}
return $columns;
}
}
В случае, если обработчик возвращает объект результата, то на фронтенд будет отправлены данные из него (актуально для кастомных обработчиков), либо отобразится поп-ап с ошибками (актуально для базовых).