Гриды в Битрикс24: теперь не нужно искать  сторонние решения

Всем привет! Меня зовут Илья, я разработчик в Битрикс24. В последнее время наша команда стремится быть прозрачнее и делиться изменениями в продукте. Мы хотим, чтобы разработчики, использующие Битрикс24, быстрее узнавали об обновлениях и имели на руках актуальную документацию. Это поможет меньше велосипедить и искать решения на стороне.

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

Об одном из них, простом и полезном, расскажу сегодня. Ранее у нас не было хорошо задокументированного коробочного решения по гридам. Если стояла задача вывести в публичной части информацию в виде таблиц, мы вручную пилили шаблоны для элементов и искали костыли для сортировки данных. Проблема возникала часто: например, если нужно было вывести список товаров, сделок или клиентов, а еще лучше — интерактивные списки.

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

Как выглядят гриды в Битрикс24

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

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

Список компаний из CRM

Список компаний из CRM

Раньше разработка гридов выполнялась на базе Bootstrap или других UI-библиотек и сопровождалась кучей кода — чтобы реализовать пагинацию, сортировку и фильтры. Сейчас можно не писать дополнительные страницы и контроллеры и не возиться с шаблонами для правильного отображения строк.

Пошаговый гайд по гридам: от простого к сложному

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

Шаг 1. Создаем класс грида

На этом шаге выполняем минимальные действия по подключению к API Битрикс24 и выводу грида. Для пользователей существует таблет UserTable, грид будем делать на его основе. Первым делом создаем класс самого грида:

Шаг 2. Выводим грид

Просто так вывалить код в статический файлик из архитектурных соображений нельзя. Добавим кастомный компонент user.grid со следующим содержимым:

 'user-grid',
    	]);

    	$grid = new UserGrid($settings);

    	return $grid;
	}

    protected function getTablet(): string
	{
    		return UserTable::class;
	}
}

Сам шаблон компонента (templates/.default/template.php) будет выглядеть так:

IncludeComponent(
	'bitrix:main.ui.grid',
	'',
	ComponentParams::get(
    	$arResult['GRID']
	)
);

Размещаем компонент на нужной странице и любуемся результатом:

7b5696d00a068a9c6b7f4bea98ccbb2f.png

Шаг 3. Работаем со списком отображаемых столбцов

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

Давайте в классе грида переопределим метод createColumns, отвечающий за создание столбцов:

getEntity()
    	);
	}
}

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

Шаг 4. Управляем отображением грида

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

При отображении грида будут показаны только столбцы из аргумента defaultFields, но через шестеренку мы сможем при желании также выбрать и настроить другие столбцы:

dce51a2b05a02fe5f295ce36475a41ce.png

Если нужно скрыть все столбцы таблета, можно использовать третий параметр isDefaultShow:

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

Шаг 5. Привязываем пользовательские поля и работаем с ассемблерами

Помимо обычных полей, к таблетам (и множеству других сущностей) можно привязывать «пользовательские поля». Дополним грид такими полями, добавив к коллекции столбцов провайдер UfColumnsProvider:

getEntity()
        	),
        	new UfColumnsProvider(
            	$this->getEntity()->getUfId(),
        	),
    	);
	}
}

Так в гриде появятся все пользовательские поля, настроенные для отображения в списке. Обратите внимание, что у них в настройках не должно стоять галочки «Не показывать в списке»:

6a4cebf373af68e9c5c1276cd7934484.png

С точки зрения настроек провайдер UfColumnsProvider поддерживает те же аргументы, что и TabletColumnsProvider: selectFields, defaultFields и isDefaultShow.

Коллекция столбцов Columns отвечает только за формирование списка столбцов. За отображение отвечают классы-сборщики RowAssembler и FieldAssembler. Задача этих классов заключается в сборке сырых данных и трансформации в пригодный для вывода вид.

ВАЖНО: сборщики — это необязательный элемент грида, если стандартные типы позволяют выводить информацию корректно.

Шаг 6. Добавляем UF

В примере с гридом сотрудников можно заметить, что поле «Подразделения» выводит не список подразделений, а строку Array. Чтобы UF поля выводились правильно, нужно добавить сборщик UfFieldAssembler. По аналогии со столбцами добавим класс UserRows и укажем нужный сборщик:

getUfId(),
			),
		);

		parent::__construct($rowAssembler);
	}
}

Затем используем созданный класс в гриде:

getVisibleColumnsIds(),
			$this->getEntity()
		);
	}
}

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

3e5454737540371b74253009e691b7ba.png

Из коробки у нас также имеются следующие сборщики:

  1. HtmlFieldAssembler.

  2. UserFieldAssembler.

  3. StringFieldAssembler.

  4. NumberFieldAssembler.

  5. ListFieldAssembler.

  6. UfFieldAssembler.

Шаг 7. Идем к контекстным действиям

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

Поговорим про контекстные действия — они отображаются возле строки в «гамбургере». За их работу отвечает интерфейс и класс, который содержит в себе типовой код формирования кнопки в меню. Для сотрудников мы можем реализовать два противоположных действия: «уволить» и «принять обратно». Реализуем оба действия (пока без логики обработки):

Далее добавим провайдер наших действий:

Используем провайдер в уже созданном классе строк UserRows:

getUfId(),
			),
		);

		parent::__construct(
			$rowAssembler,
			new UserContextActionDataProvider(),
		);
	}
}

Мы видим, что возле строк появился виджет «гамбургера», нажав на который можно увидеть оба действия:

c5ac6b1f0eceba953421114567bed648.png

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

Наблюдательный читатель наверняка уже заметил проблему в коде: если столбец ACTIVE не будет отображаться (скрыт настройками), то также он не будет содержаться в аргументе $rawFields. Чтобы этого не происходило, нужно отметить столбец ACTIVE обязательным. Тогда он будет выбираться из базы данных всегда, независимо от того, отображается он сейчас или нет. Добьемся этого, добавив метод UserColumns: prepareColumns с таким содержимым:

getId() === 'ACTIVE')
			{
				$column->setNecessary(true);
			}
		}

		return $columns;
	}
}

Наконец, реализуем логику для обоих действий в два этапа:

  1. создаём JS-обработчик нажатия на кнопку для фронтенда;

  2. создаём PHP-обработчик для бэкенде.

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

onclick = new SendRowActionOnclick($this, [
			'id' => $rawFields['ID'],
		]);

		return parent::getControl($rawFields);
	}
}

Обработчик на бэкенде должен принять эти данные и обработать по своей логике. За это отвечает метод processRequest:

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'] и не забываем сделать его обязательным:

getId(), $necessaryColumns))
			{
				$column->setNecessary(true);
			}
		}

		return $columns;
	}
}

В случае, если обработчик возвращает объект результата, то на фронтенд будет отправлены данные из него (актуально для кастомных обработчиков), либо отобразится поп-ап с ошибками (актуально для базовых).

690da3e89142cb20f8dcb534972740e2.png

Шаг 8. Прикручиваем групповые действия с несколькими элементами

Это актуально, когда мы хотим выделить несколько элементов и применить  общее действие (в нашем случае это — массовый найм сотрудников или увольнение). Тут тоже есть тонкости. 

Помимо действий в конкретной строке можно также добавить и действия в нижнюю панель грида, так называемые групповые действия. За работу с ними в панели отвечает класс Bitrix\Main\Grid\Panel\Panel. Механика аналогична действиям в строке, сначала необходимо добавить класс-действие. Но есть нюансы, о которых сейчас расскажу :)

Редактирование

Начнем с типового, но необычного действия «Редактирование». Необычное оно потому, что это встроенное действие грида, и нам практически не нужно писать код.

Создадим класс-действие (как обычно, без обработчика):

Создаем провайдер:

И, наконец, сделаем сам объект панели грида:

Затем добавляем панель в грид:

Можно заметить, что возле каждой строки появились галочки, а внизу грида панель:

a43e391e5853c64f3eef246903d4760e.png

Можно выделить несколько строк, нажать кнопку «редактировать» и увидеть, что грид перестроится под редактирование:

d9a956e18aaff9ef77734959f2461988.png

В этом особенность такого действия. Оно встроенное, так что не нужно озадачиваться его отображением или JS логикой, нужно лишь реализовать его обработку на бэкенде:

getRequestRows($request);
		foreach ($rows as $row)
		{
			$id = $row['ID'];
			unset($row['ID']);

			$user = new CUser();
			$user->Update($id, $row);
			if ($user->LAST_ERROR)
			{
				$result->addError(
					new Error($user->LAST_ERROR)
				);
			}
		}

		return $result;
	}
}

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

Аналогичное действие, доступное из коробки, — это удаление. Для него также существует базовый класс Bitrix\Main\Grid\Panel\Action\RemoveAction, а остальное происходит идентично.

Увольнение/прием на работу

Теперь добавим кастомное действие, которого нет в коробке грида. Это снова будут увольнение и приём на работу. Мы не будет делать отдельную кнопку, а используем селектор для выбора действия. Нужно добавить сразу два. Но прежде начнем с добавления класс-наследника Bitrix\Main\Grid\Panel\Action\Group\GroupChildAction. Он будет выступать элементом групповых действий:

Замечу, что мы пока что добавили класс без реализации логики обработки запроса, а также с заглушкой метода getOnchange. Об этом поговорим далее. Давайте добавим сам элемент, отвечающий за групповое действие. В данном случае просто наследуемся от базового класса \Bitrix\Main\Grid\Panel\Action\GroupAction и перечисляем доступные действия:

Добавляем полученный класс в провайдер панели:

Вжух, в нижней панели грида у нас появился селектор с действиями:

a007a0fb08fd1eef9eab1a6585e0429a.png

Теперь поговорим о логике работы селектора в момент, когда мы выбираем элемент списка. При его выборе срабатывает событие onchange. Оно выполняет действия, указанные в методе getOnchange для класса нашего действия:

 Actions::RESET_CONTROLS,
        	],
        	// создаем кнопку
        	[
            	'ACTION' => Actions::CREATE,
            	'DATA' => [
                	// добавляем кнопку, которая выполняет JS код, который отправляет выбранные строки грида
                	(new Snippet)->getApplyButton([
                    	'ONCHANGE' => [
                        	[
                            	'ACTION' => Actions::CALLBACK,
                            	'DATA' => [
                                	[
                                    	'JS' => 'CurrentGrid.sendSelected()',
                                	]
                            	],
                        	],
                    	],
                	]),
            	],
        	],
    	]);
	}
}

Далее добавляем обработчик запроса в действии:

getValue() ?? [];
		}
		else
		{
			$filter = [
				'@ID' => $this->getRequestRows($request),
			];
		}

		$rows = UserTable::getList([
			'select' => [
				'ID',
			],
			'filter' => $filter,
		]);
		foreach ($rows as $row)
		{
			$user = new CUser();
			$user->Update($row['ID'], [
				'ACTIVE' => 'N',
			]);
			if ($user->LAST_ERROR)
			{
				$result->addError(
					new Error($user->LAST_ERROR)
				);
			}
		}

		return $result;
	}
}

При передаче аргумента $isSelectedAllRows важно обязательно использовать фильтр, если он передан. Если такой галочки не стоит, то можно воспользоваться методом базового класса Bitrix\Main\Grid\Panel\Action\Group\GroupChildAction: getRequestRows. Он вытащит из запроса выбранные строки.

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

Шаг 9. Добавляем фильтр

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

Пока добавим класс для фильтра с провайдером полей на базе таблета:

 $gridId,
				]),
				$entity,
				selectFields: [
					'ID',
					'ACTIVE',
					'LOGIN',
					'EMAIL',
					'NAME',
					'SECOND_NAME',
					'LAST_NAME',
					'DATE_REGISTER',
					'LAST_LOGIN',
				],
				defaultFields: [
					'ID',
					'ACTIVE',
					'LOGIN',
					'EMAIL',
					'NAME',
					'LAST_NAME',
					'DATE_REGISTER',
				],
			)
		);
	}
}

Обратите внимание, что TabletDataProvider идентичен по аргументам с TabletColumnProvider, и можно сразу указать поля, которые участвуют в фильтре. Первым параметром обязательно передаются настройки фильтра. Создадим фильтр в классе грида:

getId(),
			$this->getEntity(),
		);
	}
}

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

IncludeComponent(
			'bitrix:main.ui.filter',
			'',
			$componentParams,
		);
	}
}

$APPLICATION->IncludeComponent(
	'bitrix:main.ui.grid',
	'',
	\Bitrix\Main\Grid\Component\ComponentParams::get(
		$arResult['~GRID']
	)
);

Смотрим на страницу и видим, что вверху над гридом появился фильтр:

ce558c79b1ae61e50759100cdc99ed30.png

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

Работали с гридами ранее? Как сейчас их реализуете?  

Если вы регулярно работаете с Битрикс24, как разработчик, для нашей команды будет ценной обратная связь. Поделитесь, подходит ли формат подобных статей, что хочется в него добавить, или каких обновлений в продукте вам не хватает.

Описанный функционал выйдет совсем скоро в свежем апдейте модуля main, не забудьте обновиться ;-)

© Habrahabr.ru