Новый шаблон sale.order.ajax: кастомизация

На реализацию этого функционала ушло порядка 30 часов рабочего времени (плюс время на самообразование).
За это время было отправлено 18 коммитов, написано 371 строк кода и осуществлено несколько попыток виртуального суицида :)

Основная задача

Создать группу свойств "Параметры доставки", которая будет зависеть от выбора типа доставки. Для курьера это "Адрес доставки", для "Транспортной компании" это выбор ТК из выпадающего списка, для доставки "Другая транспортная компания" - тоже текстовое поле (как и адрес доставки). Все эти поля являются обязательными, и отображаться должны не в блоке "Пользователь", а в блоке с доставками.
В новом шаблоне sale.order.ajax перенести поля в другой блок не так просто, как кажется на первый взгляд, а информации на эту тему буквально крупицы.


Кастомизация нового шаблона sale.order.ajax осуществляется полностью средставми JS. Для этого лучше не вносить изменения в сам файл order_ajax.js, а создать новый order_ajax_ext.js, как это советует Олег.
Теперь нам нужно будет переопределить часть методов. Я постараюсь прокомментировать подробно, насколько смогу.

Первая задача - перенести поле "Адрес доставки" из блока "Пользователь" в блок "Доставка".

Это можно сделать через событие OnSaleComponentOrderJsDataHandler. В этом событии мы можем поймать тот массив, который летит в ajax-обработчик для дальнейшего отображения и что-то поменять в нем.

function OnSaleComponentOrderJsDataHandler(&$arResult, &$arParams) {
    $groupParamsId = 5; //ID группы свойств с параметрами доставки
    foreach ($arResult['JS_DATA']['ORDER_PROP']['properties'] as $key => $prop) {
        if ($prop['PROPS_GROUP_ID']==$groupParamsId) {
            $arResult['JS_DATA']['DELIVERY_PROPS']['properties'][] = $arResult['JS_DATA']['ORDER_PROP']['properties'][$key];
            unset($arResult['JS_DATA']['ORDER_PROP']['properties'][$key]);
        }
    }
    foreach ($arResult['JS_DATA']['ORDER_PROP']['groups'] as $key => $group) {
        if ($group['ID']==$groupParamsId) {
            $arResult['JS_DATA']['DELIVERY_PROPS']['groups'][] = $arResult['JS_DATA']['ORDER_PROP']['groups'][$key];
            unset($arResult['JS_DATA']['ORDER_PROP']['groups'][$key]);
        }
    }
}

В данном случае мы создали дополнительный ключ массива JS_DATA для свойств, которые касаются именно доставки. Теперь они не будут появляться в блоке "Пользователь", но и в блоке с доставкой тоже не появятся - у нас всё еще впереди.

Задача 1.1 - вывести поля "Адрес доставки", убранные из блока "Пользователь", в блоке "Доставка".

Чтобы обращаться к нашим "новым" свойствам, создаем экземпляр "коллекции" и добавляем его в наш объект. Для этого наследуем метод initOptions, но весь его текст нам не нужен, поэтому вызовем родительский метод и добавим нужную нам строку в конце:

var initOptionsParent = BX.Sale.OrderAjaxComponent.initOptions;
BX.Sale.OrderAjaxComponentExt.initOptions = function() {
        initOptionsParent.apply(this, arguments);
        this.propertyDeliveryCollection = new BX.Sale.PropertyCollection(BX.merge({publicMode: true}, this.result.DELIVERY_PROPS));
    };

Наследуем метод editDeliveryInfo:

    BX.Sale.OrderAjaxComponentExt.editDeliveryInfo = function(deliveryNode) {
        editDeliveryInfoParent.apply(this, arguments); //вызываем родителя
        var deliveryInfoContainer = deliveryNode.querySelector('.bx-soa-pp-company-desc'); //находим блок с описанием службы доставки
        var group, property, groupIterator = this.propertyDeliveryCollection.getGroupIterator(), propsIterator, htmlAddress;
//используем коллекцию, инициализированную в предыдущем методе
        var deliveryItemsContainer = BX.create('DIV', {props: {className: 'col-sm-12 bx-soa-delivery'}}); //создаем контейнер для будущего поля
        while (group = groupIterator())
        {
            propsIterator =  group.getIterator();
            while (property = propsIterator())
            {
                if (property.getGroupId()==5) { //если это свойство является параметром доставки
                    this.getPropertyRowNode(property, deliveryItemsContainer, false); //вставляем свойство в подготовленный контейнер
                    deliveryInfoContainer.appendChild(deliveryItemsContainer); //контейнер вместе со свойством в нём добавляем в конце блока с описанием (deliveryInfoContainer)

                }
            }
        }
    };

Определять, какая именно служба доставки выбрана, не нужно. Если у вас правильно настроены зависимости для свойств заказа (например, что "Адрес доставки" выводится только при доставке курьером, а "Выбор транспортной компании" только при доставке ТК), то поля будут отображаться только там, где нужно.

Задача 2 - добиться того, чтобы ошибки о незаполненных полях выводились в тех блоках, в которых нужно.

Если поля, которые мы переместили в блок с доставками, являются необязательными, то на этом можно и закончить. Но обычно наша задача не только вывести форму для оформления заказа, но и максимально помочь пользователю, например, не забыть заполнить важное поле "Адрес доставки".

Основное, что мы можем для этого сделать - это унаследовать метод initValidation:

    BX.Sale.OrderAjaxComponentExt.initValidation = function() {
        if (!this.result.ORDER_PROP || !this.result.ORDER_PROP.properties)
            return;

        var properties = this.result.ORDER_PROP.properties, 
            deliveryProps = this.result.DELIVERY_PROPS.properties,
            obj = {}, deliveryObj = {}, i;


        for (i in properties)
        {
            if (properties.hasOwnProperty(i))
                obj[properties[i].ID] = properties[i];
        }
        for (i in deliveryProps)
        {
            if (deliveryProps.hasOwnProperty(i))
                deliveryObj[deliveryProps[i].ID] = deliveryProps[i];
        }

        this.validation.properties = obj;
        this.validation.deliveryProperties = deliveryObj;
    };

Помимо массива ORDER_PROP у нас появился массив DELIVERY_PROPS, который мы должны показать компоненту при инициализации валидации. Записываем его в отдельное свойство нашего объекта - this.validation.deliveryProperties.

Теперь эти свойство надо где-то применить. Создаем свою функцию isValidDeliveryBlock: она будет создана по образу и подобию функции isValidPropertiesBlock (думаю, по их названиям понятно, для чего они предназначены).
BX.Sale.OrderAjaxComponentExt.isValidDeliveryBlock = function(excludeLocation) {
        if (!this.options.propertyValidation)
            return [];

        var props = this.orderBlockNode.querySelectorAll('.bx-soa-customer-field[data-property-id-row]'),
            propsErrors = [],
            id, propContainer, arProperty, data, i;
        for (i = 0; i < props.length; i++)
        {
            id = props[i].getAttribute('data-property-id-row');

            if (!!excludeLocation && this.locations[id])
                continue;

            propContainer = props[i].querySelector('.soa-property-container');
            if (propContainer)
            {
                arProperty = this.validation.deliveryProperties[id];
                data = this.getValidationData(arProperty, propContainer);
                propsErrors = propsErrors.concat(this.isValidProperty(data, true));
            }
        }
        return propsErrors;
    };

Эта функция возвращает массив с ошибками, касающимися полей доставки, используя для валидации список полей из нашего js-массива this.validation.deliveryProperties.

Теперь будем использовать её в методе editFadeDeliveryContent. Этот метод отвечает за содержимое блока "Доставка" в закрытом виде. В этом состоянии он должен выводить красный блок с описанием ошибки, при нажатии на который блок будет раскрываться. За отображение такого блока отвечает метод showError. В него мы отправляем this.deliveryBlockNode (блок, в котором нужно показывать ошибку) и validDeliveryErrors (переменная, которая получила ошибки с помощью нашего метода isValidDeliveryBlock).

    BX.Sale.OrderAjaxComponentExt.editFadeDeliveryContent = function(node) {
        editFadeDeliveryContentParent.apply(this, arguments);
        if (this.initialized.delivery) { //проверяем, была ли инициализирована доставка
            var validDeliveryErrors = this.isValidDeliveryBlock(); //вызываем наш метод
            if (validDeliveryErrors.length && BX.hasClass(BX.findParent(node),'bx-selected') == true) {
                this.showError(this.deliveryBlockNode, validDeliveryErrors);
            } else { //если ошибок нет и всё в порядке
                node.querySelector('.alert.alert-danger').style.display = 'none';

                var section = BX.findParent(node.querySelector('.alert.alert-danger'), {className: 'bx-soa-section'});

                node.setAttribute('data-visited', 'true');
                BX.removeClass(section, 'bx-step-error'); //убираем иконку, что есть ошибка в этом шаге
                BX.addClass(section, 'bx-step-completed'); //выставляем, что блок валиден и готов
            }
        }
    };

Задача 2.1 - скорректировать вывод ошибок при отправке формы.

Все эти проверки касаются ситуации, когда проверки происходят "на лету". Но еще есть ситуация, когда мы нажимаем кнопку "Подтвердить заказ" и все поля снова проверяются не только на стороне клиента, но и на стороне сервера. В стандартном оформлении заказа, если какое-то обязательное поле не заполнено, js даже не отправляет запроса к серверу, а показывает ошибку сразу. В нашей ситуации, если мы заполнили все поля, кроме поля "Адрес доставки", запрос всё-таки на сервер уйдет, а сервер уже покажет нам ошибку. Но ошибку он покажет не в том блоке, который нам нужен, а опять в блоке "Пользователь", потому в методе saveOrder, который выполняется при нажатии кнопки оформления заказа, нам тоже нужно переопределить место показа для блока ошибки.
Лирическое отступление. Определить, какая именно ошибка показывается, очень просто. Языковые сообщения здесь почему-то не унифицированы, и когда происходит проверка на стороне клиента, текст ошибки звучит как "Поле "Адрес доставки" обязательно для заполнения".
Если же мы получили ошибку от сервера, ошибка выводится без слова "поле", название поля без кавычек. Примерно так: "Адрес доставки обязательно для заполнения".
Может, конечно, это когда-то пофиксят, но сейчас у меня стоит последняя версия модуля sale (даже бета), и там всё так.

Но не суть. Метод saveOrder будем переопределять целиком, так как там изменения будут в середине кода внутри условия else.
Этот метод получает объект result, внутри которого есть order.ERROR.PROPERTY, куда он и складывает по умолчанию все ошибки, связанные с пользовательскими свойствами. К сожалению, мне не удалось (в адекватные сроки) найти место, где формируется объект result, который туда попадает. Поэтому пришлось просто перенести ошибку из свойства PROPERTY в свойство DELIVERY. В данном случае я сделала допущение, что ошибки типа PROPERTY обычно валидируются в форме "на лету" и мне не удалось ни разу воспроизвести ситуацию, когда в метод saveOrder попадали ошибки, кроме ошибки, связанной с незаполненным адресом доставки.

    BX.Sale.OrderAjaxComponentExt.saveOrder = function(result) {
        var res = BX.parseJSON(result), redirected = false;
        if (res && res.order)
        {
            result = res.order;
            this.result.SHOW_AUTH = result.SHOW_AUTH;
            this.result.AUTH = result.AUTH;

            if (this.result.SHOW_AUTH)
            {
                this.editAuthBlock();
                this.showAuthBlock();
                this.animateScrollTo(this.authBlockNode);
            }
            else
            {
                if (result.REDIRECT_URL && result.REDIRECT_URL.length)
                {
                    if (this.params.USE_ENHANCED_ECOMMERCE === 'Y')
                    {
                        this.setAnalyticsDataLayer('purchase', result.ID);
                    }

                    redirected = true;
                    document.location.href = result.REDIRECT_URL;
                }
                if (result.ERROR.hasOwnProperty('PROPERTY')) {
                    result.ERROR['DELIVERY'] = result.ERROR.PROPERTY;
                    delete result.ERROR.PROPERTY;
                }
                this.showErrors(result.ERROR, true, true);
            }
        }

        if (!redirected)
        {
            this.endLoader();
            this.disallowOrderSave();
        }
    };

Задача 3 - запретить битриксу выбирать доставку по умолчанию.

В случае, если у пользователя есть сохраненный профиль, ему автоматически выберется последняя выбранная им доставка, но битрикс ничего не знает о том, что у нас там еще и обязательные поля. Поэтому убираем дефолтный выбор доставки в обработчике OnSaleComponentOrderJsDataHandler. Он у нас уже есть, дописываем в него:
if (isset($arResult['JS_DATA']['LAST_ORDER_DATA']['DELIVERY'])
 && $arResult['JS_DATA']['LAST_ORDER_DATA']['DELIVERY']!='') {
    $arResult['JS_DATA']['LAST_ORDER_DATA']['DELIVERY'] = '';}

В данном случае блок с доставками всегда будет открыт, и пользователь сразу обратит внимание на необходимость заполнения полей. Но! Если пользователь в кабинете удаляет профиль, поле с местоположением будет у него незаполнено, и после его заполнения блок с доставками автоматически закроется без возможности его отредактировать (пропадет кнопка "Изменить"). Это очень трудно пофиксить, чтобы не посыпалось всё остальное, поэтому мы приняли решение убрать возможность редактирования профилей в кабинете пользователя (делается снятием галочки в настройках компонента личного кабинета).

На данный момент у меня всё. Конечно, этот код был написан для конкретного проекта и с определенными допущениями. Но надеюсь, что данная заметка оказалась вам полезной и наведет вас на путь истинный при решении вашей задачи. Ибо документации по методам класса OrderAjaxComponent нет и не будет.
Если Вам есть что добавить или поправить - буду рада комментариям.

Комментарии

  1. Добрый день, был опыт работы с кастомизацией компонента. Собственно все переписал в js шаблоне с разбором, не самая приятная задача, но все вполне себе работает и корректно отрабатывает.

    Пример кастомизации можно посмотреть на сайте http://postel.aliterax.com/personal/cart/

    Если будет интересно можете стукнуть мне на почту - backend собака aliterax.com, вышлю исходник js файла для разбора, комментарии не особо подробные, но все блоки, которые я затрагивал комментированы.

    ОтветитьУдалить
  2. Добрый день, скажите, а как сделать чтобы "Адрес доставки" передавался в службу доставки?

    ОтветитьУдалить
    Ответы
    1. Алекс, такой задачей не занималась. Надо смотреть, экспериментировать) скорее всего, многое зависит от самого обработчика службы доставки. По идее "адрес доставки" это стандартное поле битрикса, значит, обработчик службы в идеале же сам его как-то подтягивать или в нём это должно настраиваться. Копайте там, в файлах самого обработчика доставки.

      Удалить
    2. Спасибо за ответ, уже все перекопал не могу найти (( в доставку передается массив $arOrder, но в нем из адреса только LOCATION_TO, а дополнительных полей нет (у меня это улица и дом), а найти где формируется сам массив $arOrder не смог, печаль и дергающий глаз :(

      Удалить
    3. Попробуйте спросить в телеграмм-чате разработчиков, может кто подскажет: @bitrixfordevelopers

      Удалить
    4. Скажите при кастомизации вы оставили файл order_ajax.js, просто еще подключили дополнительный order_ajax_ext.js?

      Удалить
  3. Этот комментарий был удален автором.

    ОтветитьУдалить
  4. Добрый день!
    Подскажите, пожалуйста, по шагу "Задача 1.1 - вывести поля "Адрес доставки", убранные из блока "Пользователь", в блоке "Доставка"."

    Куда нужно вставлять данный код?

    Если вставить его в order_ajax.js (или в order_ajax_ext.js) - получаю ошибку в консоль:
    "Uncaught TypeError: Cannot set property 'initOptions' of undefined"

    ОтветитьУдалить
  5. Добрый день!
    Можно как нибудь сделать чтобы поля выбора способа доставки, оплаты были всегда открыты?
    Пока не щелкнишь далее, после выбора доставки, он не предложит выбрать способ оплаты. Или пока кнопку изменить не щелкнуть.

    ОтветитьУдалить

Отправить комментарий

Популярные сообщения из этого блога

Вывод пользовательского свойства раздела в компонентах catalog.section и news.list

Если при полной выгрузке из 1С в битрикс товары и разделы приходят неактивными