EN
oekintaro
oekintaro
18 марта 2016

Здравствуйте, я новичок в BEM. Помогите разобраться, пожалуйста.

Я подключил bem-components в проект dist-способом

<link rel="stylesheet" href="https://yastatic.net/bem-components/2.4.0/desktop/bem-components.css">
<script src="https://yastatic.net/bem-components/2.4.0/desktop/bem-components.js+bemhtml.js"></script>

Также у меня есть форма с несколькими select-блоками из bem-components, html которых скопировал со странички описания:

<form class='filter-form i-bem' data-bem='{ "filter-form": {} }'>
    <div class="select select_mode_radio select_theme_islands select_size_m i-bem" data-bem='{"select":{"name":"city"}}'>
        ...
    </div>
    <div class="select select_mode_radio select_theme_islands select_size_m i-bem" data-bem='{"select":{"name":"region"}}'>
        ...
    </div>
</form>

Мне нужно при изменении значения определенного select, а именно city, выполнить некоторые действия (если интересно, то нужно сделать post-ajax и выполнить submit формы).

Вроде надо что-то типа этого:

modules.define('filter-form', ['i-bem__dom'], function(provide, BEMDOM) {
    provide(BEMDOM.decl(this.name, {
        onSetMod: {
            js: {
                'inited': function() {
                    var sity_select = findBlockInside('cityselect');
                    city_select.on(this.domElem, 'change', this._onCityChanged, this);
                }
            }
        },
        _onCityChanged: function() {  ...  }
    }))
});

Как правильно указать событие и указать конкретный select?

tadatuta
#tadatuta
18 марта 2016

Привет!

Нужно смиксовать каждый select с соответствующими элементами блока filter-form, тогда у findBlockInside() можно будет передать контекст:

// первый аргумент — это имя элемента текущего блока, т.е. filter-form__city
var citySelect = this.findBlockInside('city', 'select')
    .on('change', this._onCityChanged, this); // замечу, что передавать контекст в on() в данном случае не имеет смысла — мы вешаемся на событие на уже найденном экземпляре блока

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

modules.define('filter-form', ['i-bem__dom', 'select'], function(provide, BEMDOM, Select) {
    provide(BEMDOM.decl(this.name, {
        onSetMod: {
            js: {
                'inited': function() {
                    // подписываемся на все селекты, но первым аргументом передаем ограничивающий контекст — элемент filter-form__city
                    Select.on(this.elem('city'), 'change', this._onCityChanged, this);
                    // в результате событие вешается не на конкретный экземпляр селекта, а на делегируется родителю, поэтому селект на самом деле может создаваться динамически в рантайме уже после подписке, но она продолжит работать
                }
            }
        },
        _onCityChanged: function() {  ...  }
    }))
});
oekintaro
#oekintaro
18 марта 2016

Спасибо за подробный ответ!
Странно, сначала работало, теперь перестало после нескольких перезагрузок страницы
Может js неправильно подключаю?
Я делаю так:

<script src="https://yastatic.net/bem-components/2.4.0/desktop/bem-components.js+bemhtml.js"></script>
<script type="text/javascript" src="static/js/filter-form.js"></script>

В filter-form.js только modules.define('filter-form'...). Может его надо обрамлять в

$("document").ready(
    modules.define('filter-form'...)
})

Хотя тоже не помогает.
filter-form почему-то не js-inited

tadatuta
#tadatuta
18 марта 2016

@oekintaro
у меня для такого HTML

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>test</title>
    <link rel="stylesheet" href="https://yastatic.net/bem-components/2.4.0/desktop/bem-components.css">
</head>
<body>
    <form class='filter-form i-bem' data-bem='{ "filter-form": {} }'>
        <div class="select filter-form__city select_mode_radio select_theme_islands select_size_m i-bem" data-bem='{"select":{"name":"city"}}'>
            <input class="select__control" type="hidden" name="city" value="2" />
            <button class="button button_size_m button_theme_islands button__control select__button i-bem" data-bem='{"button":{}}' role="listbox" type="button" aria-owns="uniq14583308821051 uniq14583308821051 uniq14583308821051" aria-labelledby="uniq14583308821052">
                <span class="button__text" id="uniq14583308821052">Мастер-класс</span>
                <span class="icon select__tick"></span>
            </button>
            <div class="popup popup_target_anchor popup_theme_islands popup_autoclosable i-bem" data-bem='{"popup":{"directions":["bottom-left","bottom-right","top-left","top-right"]}}' aria-hidden="true">
                <div class="menu menu_size_m menu_theme_islands menu_mode_radio menu__control select__menu i-bem" data-bem='{"menu":{}}'>
                    <div class="menu-item menu-item_theme_islands i-bem" data-bem='{"menu-item":{"val":1}}' role="option" id="uniq14583308821053" aria-checked="false">Доклад</div>
                    <div class="menu-item menu-item_checked menu-item_theme_islands i-bem" data-bem='{"menu-item":{"val":2}}' role="option" id="uniq14583308821054" aria-checked="true">Мастер-класс</div>
                    <div class="menu-item menu-item_theme_islands i-bem" data-bem='{"menu-item":{"val":3}}' role="option" id="uniq14583308821055" aria-checked="false">Круглый стол</div>
                </div>
            </div>
        </div>
        <div class="select select_mode_radio select_theme_islands select_size_m i-bem" data-bem='{"select":{"name":"region"}}'>
            ...
        </div>
    </form>
    <script src="https://yastatic.net/bem-components/2.4.0/desktop/bem-components.js+bemhtml.js"></script>
    <script src="script.js"></script>
</body>
</html>

и такого JS работает как ожидается:

modules.define('filter-form', ['i-bem__dom'], function(provide, BEMDOM) {

provide(BEMDOM.decl(this.name, {
    onSetMod: {
        js: {
            inited: function() {
                var citySelect = this.findBlockInside('city', 'select')
                    .on('change', this._onCityChanged, this);
            }
        }
    },
    _onCityChanged: function() {
        console.log('da')
    }
}));

});
oekintaro
#oekintaro
19 марта 2016

Спасибо! Все работает!

oekintaro
#oekintaro
22 марта 2016

@tadatuta
Все же не работает) точнее не всегда работает.

Почему-то блок filter-form не всегда инициализируется. Вроде бы инициализация должна произойти, когда загрузится полностью DOM.
Вот сам сайт - m2center.ru
static/outsite/js/filter-form.js - здесь js код filter-form
кнопка выбора города - это элемент filter-form__city

Уже голову сломал. Если посмотрите, буду очень благодарен.

tadatuta
#tadatuta
22 марта 2016

Проблему понял, она связана с тем, что инициализация блоков на странице иногда происходит до того, как успевает произойти декларация filter-form.

Я добавлю возможность самостоятельно управлять моментом инициализации.

Пока в качестве быстрого хака могу предложить такую схему:

  1. Взять https://yastatic.net/bem-components/2.4.0/desktop/bem-components.dev.js+bemhtml.js
  2. Фрагмент
modules.require(
    ['i-bem__dom_init', 'jquery', 'next-tick'],
    function(init, $, nextTick) {

$(function() {
    nextTick(init);
});

});

перенести из него в свой проектный код и вызывать после всех вызовов modules.define().

oekintaro
#oekintaro
23 марта 2016

Спасибо! Теперь точно работает))

sermonis
#sermonis
13 июля 2017

Здравствуйте! У меня по сути тот же самый вопрос, но по bem-components 6.0.0.
Не удается отловить событие 'change' селектора (select).

Привожу содержимое ads-list-form-search.js:

/**
* @module ads-list-form-search
*/
modules.define('ads-list-form-search',  ['i-bem-dom', 'select'],  function(provide, bemDom, Select)
{
    /**
    * @exports
    * @class ads-list-form-search
    * @bem
    */
    provide(bemDom.declBlock(this.name, /** @lends ads-list-form-search.prototype */{

        /**
        * On modifier set
        * callback functions
        */
        onSetMod: {

            'js': {

                /**
                * @param modName
                * @param modVal
                * @param currentModVal
                */
                'inited': function()
                {
                    console.log('ads-list-form-search inited');

                    this._events(Select)
                        .on(this._elem('control-brand'), 'change', this._onBrandChanged, this);

                },

            },

        },

        _onBrandChanged: function()
        {
            console.log('Brand Changed');
        },

    }, /** @lends ads-list-form-search */{

        lazyInit: false,
        onInit: function()
        {
            this.__base.apply(this, arguments);
        },

    }));
});

Выдержка из ads-list-form-search.bemhtml.js (только элемент control-brand):

// Brand control
{
    block: 'select',
    mods: {

        mode: 'radio-check',
        theme: $_theme,
        size: $_size,
        width: 'available',

    },
    mix: [

        {
            block: this.ctx.block,
            elem: 'control-brand',        
        },

    ],
    name: 'brand_id',
    text: '— Все марки',
    options: [

        {
            val: 1,
            text: 'Audi',
        },
        {
            val: 2,
            text: 'BMW',
        },
        {
            val: 3,
            text: 'Chevrolet',
        },

    ],

},

Подключение зависимостей в ads-list-form-search.deps.js:

({

    mustDeps: [

        'select',
        'i-bem-dom',

    ],

    shouldDeps: [],

    noDeps: [ ],

})

Реакции на событие событие 'change' не наблюдается. Подскажите пожалуйста, где тут ошибка.
Спасибо!

tadatuta
#tadatuta
16 июля 2017

@sermonis

Ошибка собственно в подписке на событие (this._events(Select).on(this._elem('control-brand'), 'change', this._onBrandChanged, this);).

Если по какой-то причине ленивая инициализация действительно не нужна, то должно сработать такое:

this._elem('control-brand')._events(Select).on('change', this._onBrandChanged);

А для обеспечения ленивой инициализации стоит создать отдельную декларацию для самого элемента control-brand, в нем подписаться на селект и бросить собственное событие, которое потом ловить из родительского блока.

sermonis
#sermonis
16 июля 2017

@tadatuta

Все работает. Большое спасибо за помощь!
Создал отдельную декларацию для элемента control-brand. На всякий случай приведу здесь итоговый пример кода для bem-components 6.0.1., может кому-то пригодится.

Выдержка из ads-list-form-search.bemhtml.js (родительский блок):

// Brand control
{
    block: 'select',
    mods: {
        mode: 'radio-check',
        theme: 'islands',
        size: 'l',    
    },
    mix: [
        {
            block: 'block_name',
            elem: 'control-brand',
            js: true,
        },
    ],
    name: 'brand_id',
    text: '— Все марки',
    options: [        
        {
            val: 1,
            text: 'Audi',
        },
        {
            val: 2,
            text: 'BMW',
        },
        {
            val: 3,
            text: 'Chevrolet',
        },
    ],
},

Содержимое ads-list-form-search__control-brand.deps.js:

(
    {
        mustDeps: ['i-bem-dom'],
        shouldDeps: ['select'],
    }
);

Содержимое ads-list-form-search__control-brand.js:

/**
* @module ads-list-form-search__control-brand
*/
modules.define('ads-list-form-search',
['i-bem-dom', 'events', 'BEMHTML', 'select'],
function(provide, bemDom, events, BEMHTML, Select) {

    /**
    * @exports
    * @class ads-list-form-search__control-brand
    * @bem
    */
    provide(bemDom.declElem('ads-list-form-search', 'control-brand',
    /** @lends ads-list-form-search__control-brand.prototype */{

        _onBrandChanged: function(e) {

            console.log('Brand value changed: ' + e.target.getVal());
            this._emit('change');

        },

    }, /** @lends ads-list-form-search__control-brand */{

        lazyInit: true,
        onInit: function() {

            this._events(Select).on('change', this.prototype._onBrandChanged);

        },

    }));

});