Перейти к содержанию

Кастомизация crm функционала на декораторах

Декоратор можно использовать для кастомизации функционала crm без правок ядра.

В общем виде декоратор выглядит так:

const bxCrmEntityMethodDecorator = function (func, context) {
  return function (...params) {
    return func.apply(context, params);
  }
}

BX.Crm.Entity.method = bxCrmEntityMethodDecorator(BX.Crm.Entity.method, BX.Crm.Entity);

Идея - переопределить стандартный битровый метод, который внутри себя вызывает оригинал. Параметр context используется чтоб сохранить this внутри метода.

Варианты определения декораторов

  • использование call если известно количество передаваемых в функцию параметров
    const fnDecorator = (fn, context) => {
      return (param1, param2) => {
        return fn.call(context, param1, param2);
      }
    }
    
  • использвание apply и arguments для любого количества параметров
    const fnDecorator = (fn, context) => {
      return () => {
        return fn.apply(context, arguments);
      }
    }
    
  • использвание apply и spread синтаксиса
    const fnDecorator = (fn, context) => {
      return (...params) => {
        return fn.apply(context, params);
      }
    }
    

Декорирование поведения объекта

Декоратор применяется к существующему объекту. Для начала объект надо где-то взять. Некоторые классы бросают событие при создании либо инициализации. Можно подписаться на такое событие, в обработчике навесить декоратор.

Декорирование создания объекта

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

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

За функционал переключения стадий отвечает класс BX.CrmProgressControl. При переключении стадий он бросает событие перед самым ajax'ом в тот момент, когда уже сработала анимация переключения. Нам такое не подходит. События на создание объект не бросает. Организуем сами.

Для создания объекта используется метод BX.CrmProgressControl.create:

(function () {
    /**
     * Декоратор конструктора объекта
     *
     * Декоратор вызывает оригинальный метод для создания объекта.
     * Далее бросает событие, куда параметром пробрасывает созданный объект.
     * Так как оригинальный метод возвращает объект через return, то сделаем то же самое чтоб повторить поведение
     *
     * @param func
     * @param context
     * @return {function(*, *): *}
     */
    const crmProgressControlCreateDecorator = function (func, context) {
        return function (id, settings) {
            const crmProgressControl = func.call(context, id, settings);  // вызываем оригинальный метод create
            BX.onCustomEvent(crmProgressControl, 'CrmProgressControlCreate', [crmProgressControl]); // бросаем кастомное событие, куда передаём созданный объект

            return crmProgressControl;
        };
    }

    /**
     * Декоратор обработчика смены стадии
     *
     * Декоратор вызывает диалог подтверждения.
     * В случае подтверждения вызывается стандартный обработчик.
     * Иначе ничего не происходит
     *
     * @param func
     * @param context
     * @return {(function(*): void)|*}
     */
    const setCurrentStepDecorator = function (func, context) {
        return function (step) {
            BX.UI.Dialogs.MessageBox.confirm(
                'Вы точно хотите сменить стадию?',
                'Смена стадии',
                function (messageBox) {
                    func.call(context, step);   // вызов оригинального метода-обработчика
                    messageBox.close();
                },
                'Да',
            );
        }
    }

    // подменяем конструктор на декоратор
    BX.CrmProgressControl.create = crmProgressControlCreateDecorator(BX.CrmProgressControl.create, BX.CrmProgressControl);

    BX.addCustomEvent('CrmProgressControlCreate', function (crmProgressControl) {
        // событие срабатывает в тот момент, когда объект уже создан. Можно декорировать методы объекта
        crmProgressControl.setCurrentStep = setCurrentStepDecorator(crmProgressControl.setCurrentStep, crmProgressControl);
    });
})();

Замечания

Декораторы можно подключать как BX JS расширения \Bitrix\Main\UI\Extension::load(). В таком случае классы уже будут описаны, а вот объекты ещё не созданы.