GitHub - navigatorsvs1232009/custom_deal_field · GitHub
Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

Кастомный тип UF-поля с HTML-виджетом в Bitrix24

Руководство описывает, как создать собственный тип пользовательского поля, который монтирует произвольный HTML/JS-виджет прямо в карточке сделки CRM. Поле ведёт себя как родное поле Bitrix24: добавляется через «Выбрать поле», отображается в режимах просмотра и редактирования, не появляется там, где не нужно.


Содержание

  1. Как это работает
  2. Структура файлов
  3. Шаг 1 — Класс UF-типа
  4. Шаг 2 — Регистрация типа
  5. Шаг 3 — Компонент рендера
  6. Шаг 4 — Шаблоны компонента
  7. Шаг 5 — Создание UF-поля
  8. Шаг 6 — Подключение JS на странице сделки
  9. Шаг 7 — Монтирование виджета в контейнер поля
  10. Упаковка в модуль
  11. Частые ошибки

Как это работает

Пользователь добавляет поле на форму («Выбрать поле»)
        │
        ▼
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/

Шаг 1 — Класс UF-типа

Наследуемся от 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 — оба подходят.


Шаг 2 — Регистрация типа

Тип регистрируется через событие 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 подключает автоматически при загрузке модуля.


Шаг 3 — Компонент рендера

Класс компонента наследует 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 автоматически выбирает нужный шаблон в зависимости от режима отображения формы.


Шаг 4 — Шаблоны компонента

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>

Шаблон main.edit

<?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.view

Код идентичен 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';

Шаблон main.public_text

При экспорте виджет не нужен — оставляем файл пустым:

<?php
// templates/main.public_text/.default.php

if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) {
    die();
}
// Виджет не отображается в текстовом контексте

Шаг 5 — Создание UF-поля

Вариант A: через установщик модуля

Метод 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;
}

Вариант B: отдельная миграция

Если используется система миграций, создайте файл вида YYYY_MM_DD_create_uf_field.php и запускайте его вручную или через CI. Шаблон миграции включён в архив модуля.

Вариант C: вручную через интерфейс

  1. Перейдите: Настройки → Пользовательские поля → Сделки CRM.
  2. Нажмите «Добавить поле».
  3. Выберите тип «Услуги КРИА (виджет)» — он появится в списке после установки модуля.
  4. Укажите символьный код UF_KRIA_SERVICES_WIDGET.
  5. Сохраните.

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

В режиме просмотра карточки 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.


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

Основная логика 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));

Как Bitrix именует блоки поля

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.

Минимальный install/index.php

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;
    }
    // ...
}

Порядок установки

  1. Распаковать архив в /bitrix/modules/ks.dream/.
  2. Установить через Marketplace → Установленные решения или программно:
    \Bitrix\Main\ModuleManager::isModuleInstalled('ks.dream')
        ?: (new ks_dream())->DoInstall();
  3. Добавить поле UF_KRIA_SERVICES_WIDGET на форму сделки через «Выбрать поле».

Частые ошибки

Виджет не отображается в режиме просмотра

Причина: JS не подключён на странице сделки вне шаблона компонента. Решение: добавить подключение JS через Asset в init.php для URL /crm/deal/details/ (см. Шаг 6).


Bitrix показывает «не заполнено» поверх виджета

Причина: 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'));

Виджет монтируется дважды при SPA-навигации

Причина: mount() вызывается повторно без проверки. Решение: в начале mount() проверить, смонтирован ли виджет:

if (block.querySelector('.kria-services-widget')) { return; }

Ошибка «Class BaseUfComponent not found»

Причина: устаревшая версия Bitrix (класс появился в версии 21.0). Решение: проверить версию модуля main, обновить ядро или реализовать компонент без наследования от BaseUfComponent, управляя шаблонами вручную.


Адаптация для других сущностей

Логика полностью переносима на любую сущность Bitrix24, у которой есть UF-поля:

Сущность ENTITY_ID URL страницы
Сделка CRM_DEAL /crm/deal/details/
Лид CRM_LEAD /crm/lead/details/
Контакт CRM_CONTACT /crm/contact/details/
Компания CRM_COMPANY /crm/company/details/
Смарт-процесс CRM_... зависит от ID процесса

Меняется только ENTITY_ID при создании поля и URL для подключения JS. Класс типа, компонент и шаблоны остаются теми же.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors