Интеграция main.ui.grid и main.ui.filter для работы с Highload-блоками в публичной части

Компонент main.ui.filter (классы Bitrix\Main\UI\Filter) выводит фильтр + поиск. Является системным компонентом.

В физической структуре его можно найти в папке /bitrix/components/bitrix/main.ui.filter

Архитектурный обзор

Системные компоненты Bitrix Framework main.ui.grid (табличное представление данных) и main.ui.filter (унифицированный интерфейс фильтрации) обеспечивают декларативный подход к реализации CRUD-интерфейсов. В связке они позволяют:

  1. Формировать динамические таблицы с сортировкой и пагинацией
  2. Реализовывать сложные фильтры с поддержкой типов данных
  3. Обрабатывать AJAX-запросы для обновления данных
  4. Интегрироваться с различными источниками данных (Highload-блоки, Инфоблоки, ORM)

Реализация компонента для Highload-блоков

1. Инициализация сущности Highload-блока

use Bitrix\Highloadblock\HighloadBlockTable;
use Bitrix\Main\Entity;

$hlblock = HighloadBlockTable::getById($arParams['BLOCK_ID'])->fetch();
$entity = HighloadBlockTable::compileEntity($hlblock);
$entityClass = $entity->getDataClass();

2. Обработка параметров фильтрации

use Bitrix\Main\UI\Filter\Options;

$filterOption = new Options($arParams['GRID_ID']);
$filterData = $filterOption->getFilter();

// Конвертация фильтра в формат D7
$runtimeFilter = [];
foreach ($filterData as $field => $value) {
    if ($field === 'FIND') {
        $runtimeFilter['%UF_NAME'] = $value;
    } elseif (isset($fields[$field])) {
        $runtimeFilter[$field] = $value;
    }
}

3. Конфигурация грида

$gridConfig = [
    'GRID_ID' => $arParams['GRID_ID'],
    'COLUMNS' => array_map(function($field) {
        return [
            'id' => $field['FIELD_NAME'],
            'name' => $field['EDIT_FORM_LABEL'],
            'sort' => $field['FIELD_NAME'],
            'type' => $this->getFieldType($field)
        ];
    }, $userFields),
    'NAV_OBJECT' => $nav,
    'AJAX_MODE' => 'Y',
    'AJAX_OPTION_HISTORY' => 'N'
];

Ключевые аспекты реализации

Обработка данных:

  • Использование Bitrix\Main\UI\PageNavigation для пагинации
  • Интеграция с CDBResult для совместимости с legacy-кодом
  • Оптимизация запросов через Entity\Query:

    $query = new Entity\Query($entity);
    $query->setSelect(['*'])
        ->setFilter($runtimeFilter)
        ->setLimit($nav->getLimit())
        ->setOffset($nav->getOffset());

Фильтрация:

  • Поддержка типов данных через Bitrix\Main\UI\Filter\Type
  • Кастомные обработчики для полей:

    \Bitrix\Main\UI\Filter\NumberType::getLogicFilter()
    \Bitrix\Main\UI\Filter\DateType::getLogicFilter()

Экспорт данных:

  • Интеграция с библиотекой SheetJS (xlsx):

    const workbook = XLSX.utils.book_new();
    const worksheet = XLSX.utils.aoa_to_sheet(data);
    XLSX.utils.book_append_sheet(workbook, worksheet, "Data");
    XLSX.writeFile(workbook, filename);

Пример интеграции с системным фильтром

$APPLICATION->IncludeComponent(
    'bitrix:main.ui.filter',
    '',
    [
        'FILTER_ID' => $gridId,
        'GRID_ID' => $gridId,
        'FILTER' => [
            [
                'id' => 'ID',
                'name' => 'ID',
                'type' => 'number',
                'default' => true
            ],
            [
                'id' => 'UF_DATE',
                'name' => 'Дата',
                'type' => 'date',
                'exclude' => [
                    DateType::NEXT_DAYS,
                    DateType::LAST_DAYS
                ]
            ]
        ],
        'ENABLE_LIVE_SEARCH' => true
    ]
);

Оптимизация производительности

  1. Кеширование метаданных Highload-блоков
  2. Пакетная обработка данных через Bitrix\Main\ORM\Query\Result::fetchCollection()
  3. Использование индексированных полей для фильтрации
  4. Ленивая загрузка данных через SHOW_MORE_BUTTON

Безопасность

  • Валидация параметров:

    if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $arParams['FILTER_NAME'])) {
      throw new SystemException('Invalid filter name');
    }
  • Экранирование выходных данных:

    $row[$field] = htmlspecialcharsbx($value, ENT_COMPAT, LANG_CHARSET);
  • Проверка прав доступа:

    if ($arParams['CHECK_PERMISSIONS'] === 'Y' && !$USER->CanDoOperation('edit_other_settings')) {
      ShowError('Access denied');
      return;
    }

Компонент

Пример вызова компонента

<?$APPLICATION->IncludeComponent(
    "yournamespace:leads.list",
    "",
    Array(
        "BLOCK_ID" => "1",
        "CHECK_PERMISSIONS" => "N",
        "DETAIL_URL" => "",
        "FILTER_NAME" => "arrFilterHL",
        "PAGEN_ID" => "page",
        "ROWS_PER_PAGE" => "12",
        "SORT_FIELD" => "ID",
        "SORT_ORDER" => "DESC",
        "GRID_ID" => "leads_list", // Опционально. Можно захардкодить внутри, если нужно.
    )
);?>

Файл component.php

<?php if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED!==true)die();

/**
 * @global object $APPLICATION
 * @var array $arResult
 * @var array $arParams
 */

$requiredModules = array('highloadblock');
foreach ($requiredModules as $requiredModule)
{
    if (!CModule::IncludeModule($requiredModule))
    {
        ShowError(GetMessage("F_NO_MODULE"));
        return 0;
    }
}

if (!$arParams['GRID_ID']) {
    ShowError($this->__name . ': GRID_ID не определен');
    return false;
}
$list_id = $arParams['GRID_ID'];

use Bitrix\Highloadblock as HL;
use Bitrix\Main\Entity;
use Bitrix\Main\Grid\Options as GridOptions;

use Bitrix\Main\Application;
$request = Application::getInstance()->getContext()->getRequest();

// hlblock info
$hlblock_id = $arParams['BLOCK_ID'];
if (empty($hlblock_id))
{
    ShowError(GetMessage('HLBLOCK_LIST_NO_ID'));
    return 0;
}
$hlblock = HL\HighloadBlockTable::getById($hlblock_id)->fetch();
if (empty($hlblock))
{
    ShowError(GetMessage('HLBLOCK_LIST_404'));
    return 0;
}

// check rights
if (isset($arParams['CHECK_PERMISSIONS']) && $arParams['CHECK_PERMISSIONS'] == 'Y' && !$USER->isAdmin())
{
    $operations = HL\HighloadBlockRightsTable::getOperationsName($hlblock_id);
    if (empty($operations))
    {
        ShowError(GetMessage('HLBLOCK_LIST_404'));
        return 0;
    }
}

$entity = HL\HighloadBlockTable::compileEntity($hlblock);

// page size
$gridOptions = new GridOptions($list_id);
$gridPageSize = $gridOptions->getCurrentOptions()['page_size'];

// Устанавливаем количество из объекта грида.
if (!empty($gridPageSize)) {
    $pageSize = (int)$gridPageSize;
} else {
    $pageSize = (int)($arParams['ROWS_PER_PAGE'] ?? 10);
}

// fields info
$fields = $GLOBALS['USER_FIELD_MANAGER']->GetUserFields('HLBLOCK_'.$hlblock['ID'], 0, LANGUAGE_ID);

// ui.filter
$stringFields = [];
foreach ($fields as $fieldName => $field)
{
    if ($field['USER_TYPE_ID'] === 'string')     {
        $stringFields[] = $fieldName;
    }
}

$filterD = [];
if ($request->isAjaxRequest()
    && $request->getQuery("grid_id") !== null
    && $request->getQuery("grid_id") === $list_id)
{
    $filterOption = new Bitrix\Main\UI\Filter\Options($list_id);
    $filterData = $filterOption->getFilter([]);

    /*foreach ($filterData as $k => $v) {
        // Тут разбор массива $filterData из формата, в котором его формирует main.ui.filter в формат,
        // который подойдет для вашей выборки.
        // Обратите внимание на поле "FIND", скорее всего его вы и захотите засунуть в фильтр по NAME и еще паре полей.

        //$filter['UF_LEAD_NAME'] = "%".$filterData['FIND']."%";
    }*/

    // Число.
    $numberFilter = \Bitrix\Main\UI\Filter\NumberType::getLogicFilter($filterData, ['ID']);
    if (!empty($numberFilter)) {
        $filterD[] = $numberFilter;
    }

    // Дата.
    $dateFilter = \Bitrix\Main\UI\Filter\DateType::getLogicFilter($filterData, []);
    if (!empty($dateFilter)) {
        $filterD[] = $dateFilter;
    }

    // Обрабатываем поисковую строку. Ищем по всем полям с типом 'string'.
    if (!empty($filterData['FIND']) && !empty($stringFields)) {
        $searchValue = '%' . $filterData['FIND'] . '%';

        $conditions = [];

        foreach ($stringFields as $field) {
            $conditions[] = [
                '=%' . $field => $searchValue
            ];
        }

        // Собираем все условия в OR-группу.
        $filterD = [
            [
                'LOGIC' => 'OR',
                ...$conditions // Распаковываем массив условий.
            ]
        ];
    }
}

// sort
$sortId = 'ID';
$sortType = 'DESC';
if (isset($arParams['SORT_FIELD']) && isset($fields[$arParams['SORT_FIELD']]))
{
    $sortId = $arParams['SORT_FIELD'];
}
// for compatibility
elseif (isset($_GET['sort_id']) && isset($fields[$_GET['sort_id']]))
{
    $sortId = $_GET['sort_id'];
}
if (isset($arParams['SORT_ORDER']) && in_array($arParams['SORT_ORDER'], array('ASC', 'DESC'), true))
{
    $sortType = $arParams['SORT_ORDER'];
}
// for compatibility
if (isset($_GET['sort_type']) && in_array($_GET['sort_type'], array('ASC', 'DESC'), true))
{
    $sortType = $_GET['sort_type'];
}
// for main.ui.grid
if (isset($_GET['grid_action'])) {
    if (isset($_GET['by'])) {
        $sortId = $_GET['by'];
    }

    if (isset($_GET['order']) && in_array($_GET['order'], array('ASC', 'DESC', 'asc', 'desc'), true)) {
        $sortType = $_GET['order'];
    }
}

// pagen
if ($pageSize>0)
{
    $pagenId = isset($arParams['PAGEN_ID']) && trim($arParams['PAGEN_ID']) != '' ? trim($arParams['PAGEN_ID']) : 'page';
    //$perPage = intval($arParams['ROWS_PER_PAGE']);
    $perPage = $pageSize;
    $nav = new \Bitrix\Main\UI\PageNavigation($pagenId);
    $nav->allowAllRecords(true)
        ->setPageSize($perPage)
        ->initFromUri();
}
else
{
    $arParams['ROWS_PER_PAGE'] = 0;
}

// start a query
$mainQuery = new Entity\Query($entity);
$mainQuery->setSelect(array('*'));
$mainQuery->setOrder(array($sortId => $sortType));

// filter
if (
    isset($arParams['FILTER_NAME']) &&
    !empty($arParams['FILTER_NAME']) &&
    preg_match('/^[A-Za-z_][A-Za-z01-9_]*$/', $arParams['FILTER_NAME']))
{
    global ${$arParams['FILTER_NAME']};
    $filter = ${$arParams['FILTER_NAME']};
    if (is_array($filter))
    {
        $mainQuery->setFilter($filter);
    }
}

if (!empty($filterD)) {
    $mainQuery->setFilter($filterD);
}

// pagen
if ($perPage > 0)
{
    $mainQueryCnt = $mainQuery;
    $result = $mainQueryCnt->exec();
    $result = new CDBResult($result);
    $nav->setRecordCount($result->selectedRowsCount());
    $arResult['nav_object'] = $nav;
    unset($mainQueryCnt, $result);

    $mainQuery->setLimit($nav->getLimit());
    $mainQuery->setOffset($nav->getOffset());
}

// execute query
//  ->setGroup($group)
//  ->setOptions($options);
$result = $mainQuery->exec();
$result = new CDBResult($result);

// build results
$rows = array();
$tableColumns = array();
while ($row = $result->fetch())
{
    foreach ($row as $k => $v)
    {
        $arUserField = $fields[$k];

        if ($k == 'ID')
        {
            $tableColumns['ID'] = true;
            continue;
        }
        if ($arUserField['SHOW_IN_LIST'] != 'Y')
        {
            continue;
        }

        $html = call_user_func_array(
            array($arUserField['USER_TYPE']['CLASS_NAME'], 'getadminlistviewhtml'),
            array(
                $arUserField,
                array(
                    'NAME' => 'FIELDS['.$row['ID'].']['.$arUserField['FIELD_NAME'].']',
                    'VALUE' => htmlspecialcharsbx(is_array($v) ? implode(', ', $v) : $v)
                )
            )
        );

        $tableColumns[$k] = true;
        $row[$k] = $html;
    }

    $rows[] = $row;
}

$arResult['rows'] = $rows;
$arResult['fields'] = $fields;
$arResult['tableColumns'] = $tableColumns;
$arResult['sort_id'] = $sortId;
$arResult['sort_type'] = $sortType;

// for compatibility
$arResult['NAV_STRING'] = '';
$arResult['NAV_PARAMS'] = '';
$arResult['NAV_NUM'] = 0;

$this->IncludeComponentTemplate();

Данные получены, теперь начнем с ними работать.

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

В Битрикс такую верстку и функционал (кнопка с иконкой "шестерёнка", выпадающее меню с пунктом "Excel" и вызовом BX.adminList.ShowMenu) формирует админский компонент для списков — это компонент bitrix:main.interface.grid (или старый административный список, основанный на CAdminList).

Поэтому заморачиваться не будем и воспользуемся навыками "гугления", забираем первую попавшуюся либу из поисковыой выдачи. У меня это xlsx.full.min.js


Файл template.php

<?php
if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED !== true) {
    die();
}

/** @global CMain $APPLICATION */
/** @var array $arParams */
/** @var array $arResult */

if (!empty($arResult['ERROR'])) {
    echo $arResult['ERROR'];

    return false;
}

/**
 * Документация по main.ui.grid.
 *
 * D7
 * https://dev.1c-bitrix.ru/api_d7/bitrix/main/systemcomponents/gridandfilter/mainuigrid/index.php
 *
 * Из Битрикс24 (здесь описано больше параметров).
 * https://bx24devbook.website.yandexcloud.net/Razrabotka/UI/Tablicy/Obzor.html
 *
 * Фильтр
 * https://dev.1c-bitrix.ru/api_d7/bitrix/main/systemcomponents/gridandfilter/mainuifilter.php
 */

$list_id = $arParams['GRID_ID'];

$columns = [];
$list = [];

$columns[] = [
    'id' => 'ID',
    'name' => 'ID',
    'sort' => 'ID',
    'default' => true,
    'width' => 100,
    'first_order' => 'desc',
];

foreach ($arResult['fields'] as $field) {
    $columns[] = [
        'id' => $field['FIELD_NAME'],
        'name' => $field['EDIT_FORM_LABEL'],
        'sort' => $field['FIELD_NAME'],
        // Определяет должна ли отображаться колонка по умолчанию в гриде. По умолчанию колонки не отображаются.
        'default' => true,
        'first_order' => 'desc',
    ];
}

foreach ($arResult['rows'] as $row) {
    $list[] = [
        'data' => $row,
    ];
}

$pageSizes = [
    ['NAME' => '10', 'VALUE' => '10'],
    ['NAME' => '20', 'VALUE' => '20'],
    ['NAME' => '50', 'VALUE' => '50'],
    ['NAME' => '100', 'VALUE' => '100'],
    ['NAME' => '200', 'VALUE' => '200'],
    ['NAME' => '500', 'VALUE' => '500'],
    ['NAME' => '1000', 'VALUE' => '1000'],
];

$filterOption = new Bitrix\Main\UI\Filter\Options($list_id);
$filterData = $filterOption->getFilter([]);
$filter = [];

foreach ($filterData as $k => $v) {
    // Тут разбор массива $filterData из формата, в котором его формирует main.ui.filter в формат, который подойдет для вашей выборки.
    // Обратите внимание на поле "FIND", скорее всего его вы и захотите засунуть в фильтр по NAME и еще паре полей
    $filter['UF_LEAD_NAME'] = "%".$filterData['FIND']."%";
    $filter['ID'] = $filterData['ID_numsel'];
}

$ui_filter = [
    ['id' => 'ID', 'name' => 'ID', 'type' => 'number', 'default' => true],
    ['id' => 'UF_LEAD_NAME', 'name' => 'Имя лида', 'type' => 'text', 'default' => true],
    [
        'id' => 'UF_CREATED_BY',
        'name' => 'Дата создания',
        'type' => 'date',
        'default' => true,
        "exclude" => array(
            \Bitrix\Main\UI\Filter\DateType::NEXT_WEEK,
            \Bitrix\Main\UI\Filter\DateType::NEXT_MONTH,
            \Bitrix\Main\UI\Filter\DateType::TOMORROW,
            \Bitrix\Main\UI\Filter\DateType::LAST_90_DAYS
        )
    ],
];
?>
<h3 class="my-2">Лиды</h3>
<div class="leads-list-content">
    <div class="leads-list__top">
        <div class="leads-list__filter">
            <?$APPLICATION->IncludeComponent(
                'bitrix:main.ui.filter',
                '',
                [
                    'FILTER_ID' => $list_id,
                    'GRID_ID' => $list_id,
                    'FILTER' => $ui_filter,
                    'ENABLE_LIVE_SEARCH' => true,
                    'ENABLE_LABEL' => true
                ]
            );?>
        </div>
        <div class="leads-list__cta">
            <div>
                <button id="download-excel" class="ui-btn ui-btn-success">Скачать Excel</button>
            </div>
        </div>
    </div>

    <div class="leads-list__table">
        <? $APPLICATION->IncludeComponent(
            'bitrix:main.ui.grid',
            '',
            [
                'GRID_ID' => $list_id,
                'COLUMNS' => $columns,
                'ROWS' => $list,
                'NAV_OBJECT' => $arResult["nav_object"], // Объект постраничной навигации.
                'AJAX_MODE' => 'Y', // Всегда должен быть 'Y'.
                'AJAX_ID' => \CAjax::getComponentID('bitrix:main.ui.grid', '.default', ''),
                'AJAX_OPTION_JUMP' => 'N',
                'AJAX_OPTION_HISTORY' => 'N',
                // По сколько показывать.
                'SHOW_PAGESIZE' => true,
                'PAGE_SIZES' => $pageSizes,
                'SHOW_PAGINATION' => true,
                'ALLOW_SORT' => true,
                'SHOW_CHECK_ALL_CHECKBOXES' => false,
                'SHOW_ROW_CHECKBOXES' => false,
                'SHOW_SELECTED_COUNTER' => false,
                // Для кнопки "Загрузить еще".
                'CURRENT_PAGE' => $arResult['nav_object']->getCurrentPage(),
                'ENABLE_NEXT_PAGE' => true,
                'SHOW_MORE_BUTTON' => false, // true чтобы включить.
                'NAV_PARAM_NAME' => 'page',
                'SHOW_GROUP_EDIT_BUTTON' => false,
                'SHOW_GROUP_DELETE_BUTTON' => false,
                // Сообщения.
                'HANDLE_RESPONSE_ERRORS' => true,
                // Общее количество.
                'SHOW_NAVIGATION_PANEL' => true,
                'SHOW_TOTAL_COUNTER' => true,
                'TOTAL_ROWS_COUNT' => $arResult['nav_object']->getRecordCount(),
                'DEFAULT_PAGE_SIZE' => (int)$arParams['ROWS_PER_PAGE']
            ]
        );
        ?>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<script>
    document.querySelector('#download-excel').addEventListener('click', function() {
        // Проверяем наличие грида.
        let grid = BX.Main.gridManager.getById('<?=$list_id?>');
        if (!grid || !grid.instance) {
            alert('Грид не найден!');
            return;
        }

        let data = [];

        // Получаем заголовки из DOM.
        let headers = [];
        let headerCells = document.querySelectorAll('.main-grid-cell-head');

        headerCells.forEach(function(cell, index) {
            // Пропускаем первую ячейку, так как пустая.
            if (index > 0) {
                headers.push(cell.textContent.trim());
            }

        });
        data.push(headers);

        // Получаем данные строк из DOM.
        let rows = document.querySelectorAll('.main-grid-row');
        rows.forEach(function(row) {
            let rowData = [];
            let cells = row.querySelectorAll('.main-grid-cell');
            cells.forEach(function(cell, index) {
                // Пропускаем первую ячейку, так как пустая.
                if (index > 0) {
                    rowData.push(cell.textContent.trim());
                }
            });
            data.push(rowData);
        });

        // Формируем имя файла: список_ID + текущая дата и время.
        let fileName = generateFileNameForDownload('<?=$list_id?>')

        // Генерируем Excel-файл.
        let ws = XLSX.utils.aoa_to_sheet(data);
        let wb = XLSX.utils.book_new();
        XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
        XLSX.writeFile(wb, fileName);
    });

    function generateFileNameForDownload(id, ext = 'xlsx') {
        let now = new Date();
        let dateStr = now.getFullYear() +
            '-' + ('0' + (now.getMonth() + 1)).slice(-2) +
            '-' + ('0' + now.getDate()).slice(-2) +
            '_' + ('0' + now.getHours()).slice(-2) +
            '-' + ('0' + now.getMinutes()).slice(-2) +
            '-' + ('0' + now.getSeconds()).slice(-2);

        return id + '_' + dateStr + '.' + ext;
    }
</script>

Еще примеры

Работа с инфоблоком

<?php
include($_SERVER["DOCUMENT_ROOT"] . "/bitrix/header.php");
$APPLICATION->SetTitle("Тестовый список");

use \Bitrix\Iblock\PropertyEnumerationTable;
use Bitrix\Main\Grid\Options as GridOptions;
use Bitrix\Main\UI\PageNavigation;

CModule::IncludeModule("iblock");

$list_id = 'example_list';

$grid_options = new GridOptions($list_id);
$sort = $grid_options->GetSorting(['sort' => ['DATE_CREATE' => 'DESC'], 'vars' => ['by' => 'by', 'order' => 'order']]);
$nav_params = $grid_options->GetNavParams();

$nav = new PageNavigation($list_id);
$nav->allowAllRecords(true)
    ->setPageSize($nav_params['nPageSize'])
    ->initFromUri();
if ($nav->allRecordsShown()) {
    $nav_params = false;
} else {
    $nav_params['iNumPage'] = $nav->getCurrentPage();
}

$ui_filter = [
    ['id' => 'NAME', 'name' => 'Название', 'type'=>'text', 'default' => true],
    ['id' => 'DATE_CREATE', 'name' => 'Дата создания', 'type'=>'date', 'default' => true],
];
?>
    <h2>Фильтр</h2>
    <div>
        <?$APPLICATION->IncludeComponent('bitrix:main.ui.filter', '', [
            'FILTER_ID' => $list_id,
            'GRID_ID' => $list_id,
            'FILTER' => $ui_filter,
            'ENABLE_LIVE_SEARCH' => true,
            'ENABLE_LABEL' => true
        ]);?>
    </div>
    <div style="clear: both;"></div>

    <hr>

    <h2>Таблица</h2>
<?php
$filterOption = new Bitrix\Main\UI\Filter\Options($list_id);
$filterData = $filterOption->getFilter([]);

foreach ($filterData as $k => $v) {
    // Тут разбор массива $filterData из формата, в котором его формирует main.ui.filter в формат, который подойдет для вашей выборки.
    // Обратите внимание на поле "FIND", скорее всего его вы и захотите засунуть в фильтр по NAME и еще паре полей
    $filterData['NAME'] = "%".$filterData['FIND']."%";
}

$filterData['IBLOCK_ID'] = 68;
$filterData['ACTIVE'] = "Y";

$columns = [];
$columns[] = ['id' => 'ID', 'name' => 'ID', 'sort' => 'ID', 'default' => true];
$columns[] = ['id' => 'NAME', 'name' => 'Название', 'sort' => 'NAME', 'default' => true];
$columns[] = ['id' => 'DATE_CREATE', 'name' => 'Создано', 'sort' => 'DATE_CREATE', 'default' => true];

$res = \CIBlockElement::GetList($sort['sort'], $filterData, false, $nav_params,
    ["ID", "IBLOCK_ID", "NAME", "CODE", "PROPERTY_MANAGER", "PROPERTY_AIM_OF_REQUEST", "DATE_CREATE",
        "PROPERTY_LAST_NAME", "PROPERTY_E_MAIL", "PROPERTY_FIRST_NAME", "PROPERTY_STATUS_OF_REQUEST"]
);
$nav->setRecordCount($res->selectedRowsCount());
while($row = $res->GetNext()) {
    $list[] = [
        'data' => [
            "ID" => $row['ID'],
            "NAME" => $row['NAME'],
            "DATE_CREATE" => $row['DATE_CREATE'],
        ],
        'actions' => [
            [
                'text'    => 'Просмотр',
                'default' => true,
                'onclick' => 'document.location.href="?op=view&id='.$row['ID'].'"'
            ], [
                'text'    => 'Удалить',
                'default' => true,
                'onclick' => 'if(confirm("Точно?")){document.location.href="?op=delete&id='.$row['ID'].'"}'
            ]
        ]
    ];
}

$APPLICATION->IncludeComponent('bitrix:main.ui.grid', '', [
    'GRID_ID' => $list_id,
    'COLUMNS' => $columns,
    'ROWS' => $list,
    'SHOW_ROW_CHECKBOXES' => false,
    'NAV_OBJECT' => $nav,
    'AJAX_MODE' => 'Y',
    'AJAX_ID' => \CAjax::getComponentID('bitrix:main.ui.grid', '.default', ''),
    'PAGE_SIZES' =>  [
        ['NAME' => '20', 'VALUE' => '20'],
        ['NAME' => '50', 'VALUE' => '50'],
        ['NAME' => '100', 'VALUE' => '100']
    ],
    'AJAX_OPTION_JUMP'          => 'N',
    'SHOW_CHECK_ALL_CHECKBOXES' => false,
    'SHOW_ROW_ACTIONS_MENU'     => true,
    'SHOW_GRID_SETTINGS_MENU'   => true,
    'SHOW_NAVIGATION_PANEL'     => true,
    'SHOW_PAGINATION'           => true,
    'SHOW_SELECTED_COUNTER'     => true,
    'SHOW_TOTAL_COUNTER'        => true,
    'SHOW_PAGESIZE'             => true,
    'SHOW_ACTION_PANEL'         => true,
    'ALLOW_COLUMNS_SORT'        => true,
    'ALLOW_COLUMNS_RESIZE'      => true,
    'ALLOW_HORIZONTAL_SCROLL'   => true,
    'ALLOW_SORT'                => true,
    'ALLOW_PIN_HEADER'          => true,
    'AJAX_OPTION_HISTORY'       => 'N'
]);
?>
<?require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php");?>

Работа с Битрикс ORM

<?php
include($_SERVER["DOCUMENT_ROOT"] . "/bitrix/header.php");
$APPLICATION->SetTitle("Тестовый список");

use \Bitrix\Iblock\PropertyEnumerationTable;
use Bitrix\Main\Grid\Options as GridOptions;
use Bitrix\Main\UI\PageNavigation;

$list_id = 'example_list';

$grid_options = new GridOptions($list_id);
$sort = $grid_options->GetSorting(['sort' => ['ID' => 'DESC'], 'vars' => ['by' => 'by', 'order' => 'order']]);
$nav_params = $grid_options->GetNavParams();

$nav = new PageNavigation('request_list');
$nav->allowAllRecords(true)
    ->setPageSize($nav_params['nPageSize'])
    ->initFromUri();

$filterOption = new Bitrix\Main\UI\Filter\Options($list_id);
$filterData = $filterOption->getFilter([]);
$filter = [];

foreach ($filterData as $k => $v) {
    // Тут разбор массива $filterData из формата, в котором его формирует main.ui.filter в формат, который подойдет для вашей выборки.
    // Обратите внимание на поле "FIND", скорее всего его вы и захотите засунуть в фильтр по NAME и еще паре полей
    $filter['NAME'] = "%".$filterData['FIND']."%";
}

$res = ExampleTable::getList([
    'filter' => $filter,
    'select' => [
        "*",
    ],
    'offset'      => $nav->getOffset(),
    'limit'       => $nav->getLimit(),
    'order'       => $sort['sort']
]);

$ui_filter = [
    ['id' => 'NAME', 'name' => 'Название', 'type'=>'text', 'default' => true],
    ['id' => 'DATE_CREATE', 'name' => 'Дата создания', 'type'=>'date', 'default' => true],
];
?>
    <h2>Фильтр</h2>
    <div>
        <?$APPLICATION->IncludeComponent('bitrix:main.ui.filter', '', [
            'FILTER_ID' => $list_id,
            'GRID_ID' => $list_id,
            'FILTER' => $ui_filter,
            'ENABLE_LIVE_SEARCH' => true,
            'ENABLE_LABEL' => true
        ]);?>
    </div>
    <div style="clear: both;"></div>

    <hr>

    <h2>Таблица</h2>
<?php
$columns = [];
$columns[] = ['id' => 'ID', 'name' => 'ID', 'sort' => 'ID', 'default' => true];
$columns[] = ['id' => 'NAME', 'name' => 'Название', 'sort' => 'NAME', 'default' => true];
$columns[] = ['id' => 'DATE_CREATE', 'name' => 'Создано', 'sort' => 'DATE_CREATE', 'default' => true];

foreach ($res->fetchAll() as $row) {
    $list[] = [
        'data' => [
            "ID" => $row['ID'],
            "NAME" => $row['NAME'],
            "DATE_CREATE" => $row['DATE_CREATE'],
        ],
        'actions' => [
            [
                'text'    => 'Просмотр',
                'default' => true,
                'onclick' => 'document.location.href="?op=view&id='.$row['ID'].'"'
            ], [
                'text'    => 'Удалить',
                'default' => true,
                'onclick' => 'if(confirm("Точно?")){document.location.href="?op=delete&id='.$row['ID'].'"}'
            ]
        ]
    ];
}

$APPLICATION->IncludeComponent('bitrix:main.ui.grid', '', [
    'GRID_ID' => $list_id,
    'COLUMNS' => $columns,
    'ROWS' => $list,
    'SHOW_ROW_CHECKBOXES' => false,
    'NAV_OBJECT' => $nav,
    'AJAX_MODE' => 'Y',
    'AJAX_ID' => \CAjax::getComponentID('bitrix:main.ui.grid', '.default', ''),
    'PAGE_SIZES' =>  [
        ['NAME' => '20', 'VALUE' => '20'],
        ['NAME' => '50', 'VALUE' => '50'],
        ['NAME' => '100', 'VALUE' => '100']
    ],
    'AJAX_OPTION_JUMP'          => 'N',
    'SHOW_CHECK_ALL_CHECKBOXES' => false,
    'SHOW_ROW_ACTIONS_MENU'     => true,
    'SHOW_GRID_SETTINGS_MENU'   => true,
    'SHOW_NAVIGATION_PANEL'     => true,
    'SHOW_PAGINATION'           => true,
    'SHOW_SELECTED_COUNTER'     => true,
    'SHOW_TOTAL_COUNTER'        => true,
    'SHOW_PAGESIZE'             => true,
    'SHOW_ACTION_PANEL'         => true,
    'ALLOW_COLUMNS_SORT'        => true,
    'ALLOW_COLUMNS_RESIZE'      => true,
    'ALLOW_HORIZONTAL_SCROLL'   => true,
    'ALLOW_SORT'                => true,
    'ALLOW_PIN_HEADER'          => true,
    'AJAX_OPTION_HISTORY'       => 'N'
]);
?>
<?require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php");?>