Руководство описывает, как создать собственный тип пользовательского поля, который монтирует произвольный HTML/JS-виджет прямо в карточке сделки CRM. Поле ведёт себя как родное поле Bitrix24: добавляется через «Выбрать поле», отображается в режимах просмотра и редактирования, не появляется там, где не нужно.
- Как это работает
- Структура файлов
- Шаг 1 — Класс UF-типа
- Шаг 2 — Регистрация типа
- Шаг 3 — Компонент рендера
- Шаг 4 — Шаблоны компонента
- Шаг 5 — Создание UF-поля
- Шаг 6 — Подключение JS на странице сделки
- Шаг 7 — Монтирование виджета в контейнер поля
- Упаковка в модуль
- Частые ошибки
Пользователь добавляет поле на форму («Выбрать поле»)
│
▼
Bitrix вызывает компонент рендера (BaseUfComponent)
│
▼
Шаблон компонента выводит <div data-kria-services-field="1">
и подключает JS/CSS через Asset
│
▼
JS ищет блок поля по data-cid="UF_KRIA_SERVICES_WIDGET"
│
▼
JS убирает классы «пустого» состояния
и монтирует виджет внутрь .ui-entity-editor-content-block
Ключевая идея: данные виджета хранятся в HL-блоке или любом другом источнике — значение самого UF-поля не используется. Поле служит только маркером присутствия виджета на форме.
/bitrix/modules/ks.dream/
├── include.php ← автозагрузка + регистрация события
├── version.php
├── lib/
│ └── userfield/
│ └── kriaserviceswidget.php ← класс UF-типа
└── install/
├── index.php ← установщик модуля
├── version.php
├── components/ks/kria.userfield.widget/
│ ├── class.php
│ └── templates/
│ ├── _widget.php ← общий фрагмент разметки
│ ├── main.edit/.default.php
│ ├── main.view/.default.php
│ ├── main.public_text/.default.php
│ └── .default/.default.php
├── js/kria-deal-services/
│ ├── kria-deal-services.js
│ └── kria-deal-services.css
└── migrations/
└── 2026_02_10_create_kria_services_widget_userfield.php
После установки модуля компонент и JS/CSS копируются в /local/:
/local/components/ks/kria.userfield.widget/
/local/js/kria-deal-services/
Наследуемся от Bitrix\Main\UserField\Types\BaseType. Минимум три вещи:
USER_TYPE_ID— строковый идентификатор, совпадает со значениемUSER_TYPE_IDв БД;RENDER_COMPONENT— пространство имён и имя компонента рендера;getDescription()— человекочитаемое название типа и базовый тип (string).
<?php
// /bitrix/modules/ks.dream/lib/userfield/kriaserviceswidget.php
namespace Ks\Dream\UserField;
use Bitrix\Main\UserField\Types\BaseType;
class KriaServicesWidget extends BaseType
{
protected const USER_TYPE_ID = 'kria_services_widget';
protected const RENDER_COMPONENT = 'ks:kria.userfield.widget';
protected static function getDescription(): array
{
return [
'DESCRIPTION' => 'Услуги КРИА (виджет)',
'BASE_TYPE' => 'string',
];
}
public static function getDbColumnType(): string
{
// Колонка в БД нужна Bitrix, даже если значение не используется
return 'text';
}
}Почему
BASE_TYPE = 'string'? Bitrix требует базовый тип для формирования колонки в таблице сущности. Для виджета-маркераstringилиtext— оба подходят.
Тип регистрируется через событие OnUserTypeBuildList. Метод getUserTypeDescription() унаследован от BaseType и возвращает массив, который ядро добавляет в список доступных типов.
<?php
// /bitrix/modules/ks.dream/include.php
use Bitrix\Main\Loader;
use Bitrix\Main\EventManager;
// Автозагрузка класса
Loader::registerAutoLoadClasses('ks.dream', [
'Ks\\Dream\\UserField\\KriaServicesWidget' =>
'lib/userfield/kriaserviceswidget.php',
]);
// Регистрация UF-типа
EventManager::getInstance()->addEventHandler(
'main',
'OnUserTypeBuildList',
['Ks\\Dream\\UserField\\KriaServicesWidget', 'getUserTypeDescription']
);Этот файл Bitrix подключает автоматически при загрузке модуля.
Класс компонента наследует BaseUfComponent. Единственное, что нужно определить — метод getUserTypeId(), возвращающий тот же идентификатор, что и USER_TYPE_ID в классе типа.
<?php
// install/components/ks/kria.userfield.widget/class.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';
}
}BaseUfComponent автоматически выбирает нужный шаблон в зависимости от режима отображения формы.
Bitrix вызывает шаблоны по следующим режимам:
| Шаблон | Когда используется |
|---|---|
main.edit |
Форма редактирования |
main.view |
Режим просмотра |
main.public_text |
Экспорт, публичный текст |
.default |
Запасной вариант |
Чтобы не дублировать код, выносим разметку в общий файл _widget.php:
<?php
// install/components/ks/kria.userfield.widget/templates/_widget.php
use Bitrix\Main\Page\Asset;
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) {
die();
}
$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="<?= (int) $dealId ?>"
></div><?php
// templates/main.edit/.default.php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) {
die();
}
$userField = $arResult['userField'] ?? [];
$dealId = (int) ($userField['ENTITY_VALUE_ID'] ?? 0);
include __DIR__ . '/../_widget.php';Код идентичен main.edit. Важно: если не создать этот шаблон или оставить его пустым, в режиме просмотра Bitrix покажет «не заполнено» и виджет отобразится только после клика по полю (то есть только в режиме редактирования).
<?php
// templates/main.view/.default.php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) {
die();
}
$userField = $arResult['userField'] ?? [];
$dealId = (int) ($userField['ENTITY_VALUE_ID'] ?? 0);
include __DIR__ . '/../_widget.php';При экспорте виджет не нужен — оставляем файл пустым:
<?php
// templates/main.public_text/.default.php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) {
die();
}
// Виджет не отображается в текстовом контекстеМетод InstallUserFields() в install/index.php создаёт поле при установке модуля и удаляет при деинсталляции:
public function InstallUserFields(): bool
{
\Bitrix\Main\Loader::includeModule('main');
$entityId = 'CRM_DEAL';
$fieldName = 'UF_KRIA_SERVICES_WIDGET';
// Идемпотентность: не создаём поле повторно
$exists = CUserTypeEntity::GetList([], [
'ENTITY_ID' => $entityId,
'FIELD_NAME' => $fieldName,
])->Fetch();
if ($exists) {
return true;
}
$ufe = new CUserTypeEntity();
$ufe->Add([
'ENTITY_ID' => $entityId,
'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' => 'KRIA Services'],
]);
return true;
}Если используется система миграций, создайте файл вида YYYY_MM_DD_create_uf_field.php и запускайте его вручную или через CI. Шаблон миграции включён в архив модуля.
- Перейдите: Настройки → Пользовательские поля → Сделки CRM.
- Нажмите «Добавить поле».
- Выберите тип «Услуги КРИА (виджет)» — он появится в списке после установки модуля.
- Укажите символьный код
UF_KRIA_SERVICES_WIDGET. - Сохраните.
В режиме просмотра карточки Bitrix не вызывает шаблон компонента заново — страница уже загружена. Поэтому JS нужно подключить отдельно для URL страницы сделки.
Добавьте в файл, который подключается из init.php (например, local/php_interface/crm/js.php):
use Bitrix\Main\Page\Asset;
if (CSite::InDir('/crm/deal/details/')) {
$asset = Asset::getInstance();
$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>'
);
}Зачем это нужно? Без этого шага при открытии карточки сделки в режиме просмотра виджет не отобразится. JS сработает только когда пользователь кликнет по полю и перейдёт в режим редактирования, потому что именно тогда Bitrix вызывает шаблон
main.editи подключает скрипт черезAsset.
Основная логика JS: найти DOM-блок поля, убрать классы «пустого» состояния и вставить виджет.
(function (window, document) {
'use strict';
var FIELD_CID = 'UF_KRIA_SERVICES_WIDGET';
var EMPTY_BLOCK_CLS = 'ui-entity-editor-content-block-click-empty';
var EMPTY_STATE_CLS = 'ui-entity-editor-content-block-empty';
var CONTENT_BLOCK = 'ui-entity-editor-content-block';
function mount() {
var block = document.querySelector('[data-cid="' + FIELD_CID + '"]');
if (!block) { return; }
// Не монтируем повторно
if (block.querySelector('.kria-services-widget')) { return; }
var marker = block.querySelector('[data-kria-services-field="1"]');
if (!marker) { return; }
var dealId = parseInt(marker.getAttribute('data-kria-deal-id'), 10) || 0;
var content = block.querySelector('.' + CONTENT_BLOCK);
// Убираем состояние «не заполнено»
block.classList.remove(EMPTY_BLOCK_CLS);
block.classList.remove(EMPTY_STATE_CLS);
// Создаём корень виджета
var root = document.createElement('div');
root.className = 'kria-services-widget';
if (content) {
content.appendChild(root);
} else {
marker.parentNode.insertBefore(root, marker.nextSibling);
}
// TODO: инициализируйте здесь свой React/Vue-компонент
// ReactDOM.render(React.createElement(MyWidget, { dealId }), root);
root.textContent = 'Виджет сделки #' + dealId;
}
// Запуск при загрузке страницы
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', mount);
} else {
mount();
}
// Повторный монт при SPA-навигации Bitrix
if (window.BX && window.BX.addCustomEvent) {
BX.addCustomEvent('SPA:pageDataReceived', mount);
BX.addCustomEvent('onAfterEpilogLoad', mount);
}
}(window, document));| CSS-класс / data-атрибут | Значение |
|---|---|
[data-cid="UF_KRIA_SERVICES_WIDGET"] |
Корневой блок поля в карточке |
.ui-entity-editor-content-block |
Контейнер содержимого поля |
.ui-entity-editor-content-block-empty |
Добавляется, если значение пустое |
.ui-entity-editor-content-block-click-empty |
Добавляется для кликабельного пустого состояния |
Оформление в виде модуля даёт полный контроль над жизненным циклом: установка, обновление, удаление через стандартный интерфейс Bitrix24.
class ks_dream extends CModule
{
public string $MODULE_ID = 'ks.dream';
public function DoInstall(): bool
{
$this->InstallFiles(); // копирует компонент и JS/CSS в /local/
$this->InstallUserFields(); // создаёт UF-поле
RegisterModule($this->MODULE_ID);
return true;
}
public function DoUninstall(): bool
{
$this->UnInstallFiles(); // удаляет файлы из /local/
$this->UnInstallUserFields(); // удаляет UF-поле
UnRegisterModule($this->MODULE_ID);
return true;
}
public function InstallFiles(): bool
{
$src = __DIR__ . '/install';
$root = $_SERVER['DOCUMENT_ROOT'];
CopyDirFiles($src . '/components', $root . '/local/components', true, true);
CopyDirFiles($src . '/js', $root . '/local/js', true, true);
return true;
}
// ...
}- Распаковать архив в
/bitrix/modules/ks.dream/. - Установить через Marketplace → Установленные решения или программно:
\Bitrix\Main\ModuleManager::isModuleInstalled('ks.dream') ?: (new ks_dream())->DoInstall();
- Добавить поле
UF_KRIA_SERVICES_WIDGETна форму сделки через «Выбрать поле».
Причина: JS не подключён на странице сделки вне шаблона компонента.
Решение: добавить подключение JS через Asset в init.php для URL /crm/deal/details/ (см. Шаг 6).
Причина: JS не убирает CSS-классы пустого состояния. Решение: перед монтированием вызвать:
block.classList.remove('ui-entity-editor-content-block-click-empty');
block.classList.remove('ui-entity-editor-content-block-empty');Причина 1: модуль не установлен или include.php не подключается.
Причина 2: USER_TYPE_ID в классе не совпадает со значением, которое возвращает getUserTypeDescription().
Проверка:
$types = CUserTypeManager::GetUserTypeList();
var_dump(array_column($types, 'USER_TYPE_ID'));Причина: mount() вызывается повторно без проверки.
Решение: в начале mount() проверить, смонтирован ли виджет:
if (block.querySelector('.kria-services-widget')) { return; }Причина: устаревшая версия Bitrix (класс появился в версии 21.0).
Решение: проверить версию модуля main, обновить ядро или реализовать компонент без наследования от BaseUfComponent, управляя шаблонами вручную.
Логика полностью переносима на любую сущность Bitrix24, у которой есть UF-поля:
Меняется только ENTITY_ID при создании поля и URL для подключения JS. Класс типа, компонент и шаблоны остаются теми же.
