Битрикс24 кастомный тип пользовательского поля. Как добавить любое поле в Сделку и не только

Создадим кастомный тип пользовательского поля, который отображает HTML‑виджет прямо в карточке сделки.

Задача

Чтобы виджет появлялся сразу после добавления поля на форму через «Выбрать поле», и работал как обычное поле Bitrix24.

Что было изначально

У нас уже был готовый виджет «Услуги КРИА»:

  • JS подключался по URL страницы сделки.
  • Виджет монтировался в нужную секцию формы.
  • Данные брались и сохранялись через компонент iko4v:kria.service и HL‑блоки.

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

Свой тип пользовательского поля в Битрикс24

Логично выбрать готовый механизм и идти стандартным путём Bitrix24: создал кастомный тип пользовательского поля, который рендерится через компонент, а внутри шаблонов подключает нужный JS/CSS и маркер для монтирования.

Основная логика:

  • Пользователь добавляет на форму поле UF_KRIA_SERVICES_WIDGET.
  • Поле всегда рендерит маркер и подключает JS.
  • JS ищет контейнер поля и вставляет виджет внутрь него.

Шаг 1. Класс кастомного типа поля

Делаем класс на базе Bitrix\Main\UserField\Types\BaseType.

Файл: /local/php_interface/crm/kria_services_userfield.php

<?php

namespace Local\Crm\UserField;

use Bitrix\Main\UserField\Types\BaseType;

class KriaServicesWidget extends BaseType
{
    protected const USER_TYPE_ID = 'kria_services_widget';
    protected const RENDER_COMPONENT = 'iko4v:kria.userfield.widget';

    protected static function getDescription(): array
    {
        return [
            'DESCRIPTION' => 'Услуги КРИА (виджет)',
            'BASE_TYPE' => 'string',
        ];
    }

    public static function getDbColumnType(): string
    {
        return 'text';
    }
}

Шаг 2. Регистрация типа поля

Подключаем класс и регистрируем тип через OnUserTypeBuildList.

Файл: /public_html/local/php_interface/init.php

$eventManager->addEventHandler(
    'main',
    'OnUserTypeBuildList',
    'KriaServicesRegisterUserField'
);

function KriaServicesRegisterUserField(): array
{
    require_once __DIR__ . '/crm/kria_services_userfield.php';

    return \Local\Crm\UserField\KriaServicesWidget::getUserTypeDescription();
}

Шаг 3. Компонент рендера UF‑типа

Создаем компонент iko4v:kria.userfield.widget на базе BaseUfComponent, чтобы Bitrix рендерил поле в разных режимах:

  • main.edit - в режиме редактировани
  • main.view - в режиме просмотра
  • main.public_text - при экспорте

Файл: /local/components/iko4v/kria.userfield.widget/class.php

<?php

use Bitrix\Main\Component\BaseUfComponent;

if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) {
    die();
}

class KriaUserfieldWidgetComponent extends BaseUfComponent
{
    protected static function getUserTypeId(): string
    {
        return 'kria_services_widget';
    }
}

Шаблон main.edit

Файл: /local/components/iko4v/kria.userfield.widget/templates/main.edit/.default.php

<?php

use Bitrix\Main\Page\Asset;

if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) {
    die();
}

$userField = $arResult['userField'] ?? [];
$dealId = (int) ($userField['ENTITY_VALUE_ID'] ?? 0);

$asset = Asset::getInstance();
$asset->addCss('/local/js/kria-deal-services/kria-deal-services.css');
$asset->addJs('/local/js/kria-deal-services/kria-deal-services.js');
?>

<div data-kria-services-field="1" data-kria-deal-id="<?= $dealId ?>"></div>

Шаблоны main.view, main.public_text, .default

Сделал так, чтобы в режиме просмотра поле рендерило тот же маркер (иначе показывалось «не заполнено»). Логика такая же, как в main.edit, код в шаблонах в моем случае одинаковый.

Шаг 4. Миграция поля

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

Файл: /local/migrations/2026_02_10_create_kria_services_widget_userfield.php

<?php
// /local/migrations/2026_02_10_create_kria_services_widget_userfield.php

/**
 * Миграция: добавляет UF_KRIA_SERVICES_WIDGET в CRM_DEAL
 */
require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_before.php");

use Bitrix\Main\Loader;
use Bitrix\Main\SystemException;

define('NO_KEEP_STATISTIC', true);
define('NOT_CHECK_PERMISSIONS', true);

header('Content-Type: text/html; charset=utf-8');
echo "<pre>";

try {
    if (!Loader::includeModule('main')) {
        throw new SystemException('Module main is not installed');
    }

    $entityId = 'CRM_DEAL';
    $fieldName = 'UF_KRIA_SERVICES_WIDGET';

    $filter = [
        'ENTITY_ID' => $entityId,
        'FIELD_NAME' => $fieldName,
    ];
    $exists = CUserTypeEntity::GetList([], $filter)->Fetch();
    if ($exists) {
        echo "UF поле {$fieldName} уже существует (ID={$exists['ID']}).\n";
        echo "Готово.\n";
        echo "</pre>";
        return;
    }

    $fieldDef = [
        'FIELD_NAME' => $fieldName,
        'USER_TYPE_ID' => 'kria_services_widget',
        'XML_ID' => $fieldName,
        'SORT' => 500,
        'MULTIPLE' => 'N',
        'MANDATORY' => 'N',
        'SHOW_FILTER' => 'N',
        'SHOW_IN_LIST' => 'N',
        'EDIT_IN_LIST' => 'N',
        'IS_SEARCHABLE' => 'N',
        'SETTINGS' => [],
        'EDIT_FORM_LABEL' => [
            'ru' => 'Услуги КРИА',
            'en' => 'Show KRIA services',
        ],
        'LIST_COLUMN_LABEL' => [
            'ru' => 'Услуги КРИА',
            'en' => 'Show KRIA services',
        ],
        'LIST_FILTER_LABEL' => [
            'ru' => 'Услуги КРИА',
            'en' => 'Show KRIA services',
        ],
    ];

    $ufe = new CUserTypeEntity();
    $fieldDef['ENTITY_ID'] = $entityId;

    $id = (int) $ufe->Add($fieldDef);
    if (!$id) {
        global $APPLICATION;
        $e = $APPLICATION->GetException();
        $msg = $e ? $e->GetString() : 'unknown';
        throw new SystemException("Fail add UF {$fieldName}: {$msg}");
    }

    echo "Добавлено UF поле {$fieldName} (ID={$id}).\n";
    echo "Готово.\n";
} catch (Throwable $e) {
    echo "ERROR: " . $e->getMessage() . "\n";
}

echo "</pre>";

Шаг 5. Подключение JS на странице сделки

Чтобы JS запускался в режиме просмотра, подключаем его в общем файле crm/js.php для страницы сделки (этот файл подключается в init.php):

if (CSite::InDir('/crm/deal/details/')) {
    $asset->addCss('/local/js/kria-deal-services/kria-deal-services.css');
    $asset->addString('<script src="/local/js/kria-deal-services/kria-deal-services.js"></script>');
}

Иначе при загрузке карточки Сделки наш виджет не отобразится. Будет заглушка с «не заполнено» и виджет отобразится только когда кликнем по полю. То есть перейдем в режим редактирования.

Шаг 6. Монтирование виджета в контейнер поля

Основная проблема была в том, что Bitrix в режиме просмотра показывал «не заполнено». Как и упомянул выше, решаем так:

  • JS ищет блок поля по data-cid="UF_KRIA_SERVICES_WIDGET".
  • Вставляет виджет внутрь .ui-entity-editor-content-block этого поля.
  • Убирает классы ui-entity-editor-content-block-click-empty и ...-empty.

Фрагмент:

const block = document.querySelector('[data-cid="UF_KRIA_SERVICES_WIDGET"]');
const content = block ? block.querySelector('.ui-entity-editor-content-block') : null;

if (content) {
    block.classList.remove('ui-entity-editor-content-block-click-empty');
    block.classList.remove('ui-entity-editor-content-block-empty');
    content.appendChild(widget);
}

Итог

Теперь поле работает как родное поле Bitrix24:

  • Добавляем его через «Выбрать поле».
  • Виджет сразу отображается в режиме просмотра.
  • Данные сохраняются в HL‑блоках через существующий компонент.

Если вам нужно реализовать похожий виджет в другой сущности, логика будет ровно такой же: кастомный тип UF + компонент рендера + JS, который монтирует виджет в контейнер поля.