Статьи

Bubble.js: 1.6K решение общей проблемы

Одной из самых распространенных задач в веб-разработке является управление событиями. Наш JavaScript-код обычно прослушивает события, отправляемые элементами DOM.

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

В этой статье мы увидим реальную проблему и ее решение 1.6K.

Мой друг работает младшим разработчиком. Таким образом, он не имеет большого опыта работы с ванильным JavaScript; однако ему пришлось начать использовать такие фреймворки, как AngularJS и Ember, не имея фундаментального понимания отношений DOM-to-JavaScript. Во время его работы в качестве младшего разработчика он был назначен ответственным за небольшой проект: веб-сайты с одностраничной кампанией, в которых почти не использовался JavaScript. Он столкнулся с небольшой, но очень интересной проблемой, которая в конечном итоге заставила меня написать Bubble.js .

Представьте, что у нас есть всплывающее окно. Красиво стилизованный элемент <div> :

1
<div class=»popup»> … </div>

Вот код, который мы используем, чтобы показать сообщение:

1
2
3
4
5
6
7
var popup = document.querySelector(‘.popup’);
var showMessage = function(msg) {
    popup.style.display = ‘block’;
    popup.innerHTML = msg;
}
showMessage(‘Loading. Please wait.’);

У нас есть другая функция hideMessage которая изменяет свойство display на none и скрывает всплывающее окно. Подход может работать в самом общем случае, но все еще имеет некоторые проблемы.

Например, скажем, нам нужно реализовать дополнительную логику, если проблемы возникают одна за другой. Допустим, мы должны добавить две кнопки к содержимому всплывающего окна — Да и Нет .

1
2
3
4
var content = ‘Are you sure?<br />’;
content += ‘<a href=»#» class=»popup—yes»>Yes</a>’;
content += ‘<a href=»#» class=»popup—no»>No</a>’;
showMessage(content);

Итак, как мы узнаем, что пользователь нажимает на них? Мы должны добавить слушателей событий для каждой из ссылок.

Например:

01
02
03
04
05
06
07
08
09
10
var addListeners = function(yesCB, noCB) {
    popup.querySelector(‘.popup—yes’).addEventListener(‘click’, yesCB);
    popup.querySelector(‘.popup—no’).addEventListener(‘click’, noCB);
}
showMessage(content);
addListeners(function() {
    console.log(‘Yes button clicked’);
}, function() {
    console.log(‘No button clicked’);
});

Код выше работает во время первого запуска. Что если нам понадобится новая кнопка или, что еще хуже, что если нам понадобится кнопка другого типа? То есть, что если бы мы продолжали использовать элементы <a> но с разными именами классов? Мы не можем использовать одну и addListeners же функцию addListeners , и раздражает создание нового метода для каждого варианта всплывающего окна.

Вот где проблемы становятся видимыми:

  • Мы должны добавлять слушателей снова и снова. Фактически, мы должны делать это каждый раз, когда изменяется HTML в <div> всплывающего окна.
  • Мы можем прикрепить прослушиватели событий, только если содержимое всплывающего окна обновлено. Только после вызова showMessage . Мы должны постоянно думать об этом и синхронизировать два процесса.
  • Код, который добавляет слушателей, имеет одну жесткую зависимость — переменная popup . Нам нужно вызвать его функцию querySelector вместо document.querySelector . В противном случае мы можем выбрать неправильный элемент.
  • Как только мы изменим логику в сообщении, мы должны изменить селекторы и, вероятно, вызовы addEventListener . Это не СУХОЙ вообще.

Должен быть лучший способ сделать это.

Да, есть лучший подход. И нет, решение не в том, чтобы использовать фреймворк.

Прежде чем раскрыть ответ, давайте немного поговорим о событиях в дереве DOM.

События являются неотъемлемой частью веб-разработки. Они добавляют интерактивность нашим приложениям и служат мостом между бизнес-логикой и пользователем. Каждый элемент DOM может отправлять события. Все, что нам нужно сделать, это подписаться на эти события и обработать полученный объект Event .

Существует термин « распространение событий», который стоит за пузырями событий и захватом событий, которые являются двумя способами обработки событий в DOM. Давайте используем следующую разметку и увидим разницу между ними.

1
2
3
<div class=»wrapper»>
    <a href=»#»>click me</a>
</div>

Мы прикрепим обработчики событий click к обоим элементам. Однако, поскольку они вложены друг в друга, они оба получат событие click .

1
2
3
4
5
6
document.querySelector(‘.wrapper’).addEventListener(‘click’, function(e) {
    console.log(‘.wrapper clicked’);
});
document.querySelector(‘a’).addEventListener(‘click’, function(e) {
    console.log(‘a clicked’);
});

Как только мы нажимаем на ссылку, мы видим следующий вывод в консоли:

1
2
a clicked
.wrapper clicked

Таким образом, оба элемента получают событие click . Сначала ссылка, а затем <div> . Это пузырьковый эффект. От самого глубокого возможного элемента его родителям. Есть способ прекратить пузыриться. Каждый обработчик получает объект события, у которого есть метод stopPropagation :

1
2
3
4
document.querySelector(‘a’).addEventListener(‘click’, function(e) {
    e.stopPropagation();
    console.log(‘a clicked’);
});

Используя функцию stopPropagation , мы указываем, что событие не следует отправлять родителям.

Иногда нам может понадобиться изменить порядок и получить событие, перехваченное внешним элементом. Чтобы достичь этого, мы должны использовать третий параметр в addEventListener . Если мы передадим true как значение, мы осуществим захват . Например:

1
2
3
4
5
6
document.querySelector(‘.wrapper’).addEventListener(‘click’, function(e) {
    console.log(‘.wrapper clicked’);
}, true);
document.querySelector(‘a’).addEventListener(‘click’, function(e) {
    console.log(‘a clicked’);
}, true);

Вот как наш браузер обрабатывает события, когда мы взаимодействуем со страницей.

Итак, почему мы потратили часть статьи, рассказывая о пузырях и захвате Мы упомянули их, потому что пузыри — это ответ на наши проблемы с всплывающим окном. Мы должны устанавливать прослушиватели событий не на ссылки, а непосредственно на <div> .

01
02
03
04
05
06
07
08
09
10
11
12
var content = ‘Are you sure?<br />’;
content += ‘<a href=»#» class=»popup—yes»>Yes</a>’;
content += ‘<a href=»#» class=»popup—no»>No</a>’;
 
var addListeners = function() {
    popup.addEventListener(‘click’, function(e) {
        var link = e.target;
    });
}
 
showMessage(content);
addListeners();

Следуя этому подходу, мы устраняем проблемы, перечисленные в начале.

  • Есть только один прослушиватель событий, и мы добавляем его один раз. Независимо от того, что мы помещаем во всплывающее окно, отлов событий будет происходить в их родителе.
  • Мы не связаны с дополнительным контентом. Другими словами, нам все равно, когда showMessage . Пока popup переменная жива, мы будем ловить события.
  • Поскольку мы вызываем addListeners один раз, мы используем переменную popup также один раз. Мы не должны хранить это или передавать его между методами.
  • Наш код стал гибким, потому что мы решили не заботиться о HTML, передаваемом в showMessage . У нас есть доступ к e.target якорю в том, что e.target указывает на нажатый элемент.

Приведенный выше код лучше, чем тот, с которого мы начали. Тем не менее, по-прежнему не работает так же. Как мы уже говорили, e.target указывает на e.target тег <a> . Итак, мы будем использовать это, чтобы различать кнопки Да и Нет .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
var addListeners = function(callbacks) {
    popup.addEventListener(‘click’, function(e) {
        var link = e.target;
        var buttonType = link.getAttribute(‘class’);
        if(callbacks[buttonType]) {
            callbacks[buttonType](e);
        }
    });
}
addListeners({
    ‘popup—yes’: function() {
        console.log(‘Yes’);
    },
    ‘popup—no’: function() {
        console.log(‘No’);
    }
});

Мы получили значение атрибута class и используем его в качестве ключа. Разные классы указывают на разные обратные вызовы.

Однако не рекомендуется использовать атрибут class . Он зарезервирован для применения визуальных стилей к элементу, и его значение может измениться в любое время. Как разработчики JavaScript, мы должны использовать атрибуты data .

1
2
3
var content = ‘Are you sure?<br />’;
content += ‘<a href=»#» data-action=»yes» class=»popup—yes»>Yes</a>’;
content += ‘<a href=»#» data-action=»no» class=»popup—no»>No</a>’;

Наш код тоже становится немного лучше. Мы можем удалить кавычки, используемые в функции addListeners :

1
2
3
4
5
6
7
8
addListeners({
    yes: function() {
        console.log(‘Yes’);
    },
    no: function() {
        console.log(‘No’);
    }
});

Результат можно увидеть в этом JSBin .

Я применил решение выше в нескольких проектах, так что имело смысл обернуть его библиотекой. Он называется Bubble.js и доступен в GitHub . Это файл 1.6K, который делает именно то, что мы сделали выше.

Давайте преобразуем наш всплывающий пример для использования Bubble.js . Первое, что мы должны изменить — это используемая разметка:

1
2
3
var content = ‘Are you sure?<br />’;
content += ‘<a href=»#» data-bubble-action=»yes» class=»popup—yes»>Yes</a>’;
content += ‘<a href=»#» data-bubble-action=»no» class=»popup—no»>No</a>’;

Вместо data-action мы должны использовать data-bubble-action .

Как только мы bubble.min.js на нашу страницу, у нас появится глобальная функция bubble . Он принимает селектор элемента DOM и возвращает API библиотеки. Метод on это тот, который добавляет слушателей:

1
2
3
4
5
6
7
bubble(‘.popup’)
.on(‘yes’, function() {
    console.log(‘Yes’);
})
.on(‘no’, function() {
    console.log(‘No’);
});

Существует также альтернативный синтаксис:

1
2
3
4
5
6
7
8
bubble(‘.popup’).on({
    yes: function() {
        console.log(‘Yes’);
    },
    no: function() {
        console.log(‘No’);
    }
});

По умолчанию Bubble.js прослушивает события click , но есть возможность изменить это. Давайте добавим поле ввода и прослушиваем его событие keyup :

1
<input type=»text» data-bubble-action=»keyup:input»/>

Обработчик JavaScript все еще получает объект Event . Итак, в этом случае мы можем показать текст поля:

1
2
3
4
5
6
bubble(‘.popup’).on({
    …
    input: function(e) {
        console.log(‘New value: ‘ + e.target.value);
    }
});

Иногда нам нужно поймать не одно, а много событий, отправляемых одним и тем же элементом. data-bubble-action принимает несколько значений, разделенных запятой:

1
<input type=»text» data-bubble-action=»keyup:input, blur:inputBlurred»/>

Найдите окончательный вариант в JSBin здесь .

Решение, представленное в этой статье, полностью зависит от всплытия событий. В некоторых случаях e.target может не указывать на элемент, который нам нужен.

Например:

1
2
3
<div class=»wrapper»>
    <a href=»#»>Please, <span>choose
</div>

Если мы поместим указатель мыши на «выбрать» и нажмем кнопку, элемент, отправляющий событие, будет не тегом <a> а элементом span .

Следует признать, что связь с DOM является неотъемлемой частью разработки наших приложений, но мы часто используем фреймворки, чтобы обойти это взаимодействие.

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

Bubble.js — это всего лишь один результат нескольких часов чтения и одного часа кодирования — это наше решение 1.6K одной из самых распространенных проблем.