Введение в браузерные события

Для реакции на действия посетителя и внутреннего взаимодействия скриптов существуют события.

Событие – это сигнал от браузера о том, что что-то произошло. Существует много видов событий. Посмотрим список самых часто используемых, пока просто для ознакомления:

События мыши:

  • click – происходит, когда кликнули на элемент левой кнопкой мыши
  • contextmenu – происходит, когда кликнули на элемент правой кнопкой мыши
  • mouseover – возникает, когда на элемент наводится мышь
  • mousedown и mouseup – когда кнопку мыши нажали или отжали
  • mousemove – при движении мыши

События на элементах управления:

  • submit – посетитель отправил форму <form>
  • focus – посетитель фокусируется на элементе, например нажимает на <input>

Клавиатурные события:

  • keydown – когда посетитель нажимает клавишу
  • keyup – когда посетитель отпускает клавишу

События документа:

  • DOMContentLoaded – когда HTML загружен и обработан, DOM документа полностью построен и доступен.

События CSS:

  • transitionend – когда CSS-анимация завершена.

Также есть и много других событий.

Назначение обработчиков событий

Событию можно назначить обработчик, то есть функцию, которая сработает, как только событие произошло.

Именно благодаря обработчикам JavaScript-код может реагировать на действия посетителя.

Есть несколько способов назначить событию обработчик. Сейчас мы их рассмотрим, начиная от самого простого.

Использование атрибута HTML

Обработчик может быть назначен прямо в разметке, в атрибуте, который называется on<событие>.

Например, чтобы прикрепить click-событие к input кнопке, можно присвоить обработчик onclick, вот так:

<input value="Нажми меня" onclick="alert('Клик!')" type="button">

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

В действии:

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

Частая ошибка новичков в том, что они забывают, что код находится внутри атрибута. Запись вида onclick="alert("Клик!")", с двойными кавычки внутри, не будет работать. Если вам действительно нужно использовать именно двойные кавычки, то это можно сделать, заменив их на &quot;, то есть так: onclick="alert(&quot;Клик!&quot;)".

Однако, обычно этого не требуется, так как прямо в разметке пишутся только очень простые обработчики. Если нужно сделать что-то сложное, то имеет смысл описать это в функции, и в обработчике вызвать уже её.

Следующий пример по клику запускает функцию countRabbits().

<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">

  <script>
    function countRabbits() {
      for(var i=1; i<=3; i++) {
        alert("Кролик номер " + i);
      }
    }
  </script>
</head>
<body>
  <input type="button" onclick="countRabbits()" value="Считать кроликов!"/>
</body>
</html>

Как мы помним, атрибут HTML-тега не чувствителен к регистру, поэтому ONCLICK будет работать так же, как onClick или onCLICK… Но, как правило, атрибуты пишут в нижнем регистре: onclick.

Использование свойства DOM-объекта

Можно назначать обработчик, используя свойство DOM-элемента on<событие>.

Пример установки обработчика click:

<input id="elem" type="button" value="Нажми меня" />
<script>
  elem.onclick = function() {
    alert( 'Спасибо' );
  };
</script>

Если обработчик задан через атрибут, то браузер читает HTML-разметку, создаёт новую функцию из содержимого атрибута и записывает в свойство onclick.

Этот способ, по сути, аналогичен предыдущему.

Обработчик хранится именно в DOM-свойстве, а атрибут – лишь один из способов его инициализации.

Эти два примера кода работают одинаково:

  1. Только HTML:

    <input type="button" onclick="alert('Клик!')" value="Кнопка"/>
  2. HTML + JS:

    <input type="button" id="button" value="Кнопка" />
    <script>
      button.onclick = function() {
        alert( 'Клик!' );
      };
    </script>

Так как DOM-свойство onclick, в итоге, одно, то назначить более одного обработчика так нельзя.

В примере ниже назначение через JavaScript перезапишет обработчик из атрибута:

<input type="button" id="elem" onclick="alert('До')" value="Нажми меня" />
<script>
  elem.onclick = function() { // перезапишет существующий обработчик
    alert( 'После' ); // выведется только это
  };
</script>

Кстати, обработчиком можно назначить и уже существующую функцию:

function sayThanks() {
  alert( 'Спасибо!' );
}

elem.onclick = sayThanks;

Если обработчик надоел – его всегда можно убрать назначением elem.onclick = null.

Доступ к элементу через this

Внутри обработчика события this ссылается на текущий элемент, то есть на тот, на котором он сработал.

Это можно использовать, чтобы получить свойства или изменить элемент.

В коде ниже button выводит свое содержимое, используя this.innerHTML:

<button onclick="alert(this.innerHTML)">Нажми меня</button>

В действии:

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

Если вы только начинаете работать с событиями – обратите внимание на следующие особенности.

Функция должна быть присвоена как sayThanks, а не sayThanks().

button.onclick = sayThanks;

Если добавить скобки, то sayThanks() – будет уже результат выполнения функции (а так как в ней нет return, то в onclick попадёт undefined). Нам же нужна именно функция.

…А вот в разметке как раз скобки нужны:

<input type="button" id="button" onclick="sayThanks()" />

Это различие просто объяснить. При создании обработчика браузером из атрибута, он автоматически создает функцию из его содержимого. Поэтому последний пример – фактически то же самое, что:

button.onclick = function() {
  sayThanks(); // содержимое атрибута
};

Используйте именно функции, а не строки.

Назначение обработчика строкой elem.onclick = "alert(1)" можно иногда увидеть в древнем коде. Это будет работать, но не рекомендуется, могут быть проблемы при сжатии JavaScript. Да и вообще, передавать код в виде строки по меньшей мере странно в языке, который поддерживает Function Expressions. Это возможно лишь по соображениям совместимости, не делайте так.

Не используйте setAttribute.

Такой вызов работать не будет:

// при нажатии на body будут ошибки
// потому что при назначении в атрибут функция будет преобразована в строку
document.body.setAttribute('onclick', function() { alert(1) });

Регистр DOM-свойства имеет значение.

При назначении через DOM нужно использовать свойство onclick, а не ONCLICK.

Недостаток назначения через свойство

Фундаментальный недостаток описанных выше способов назначения обработчика – невозможность повесить несколько обработчиков на одно событие.

Например, одна часть кода хочет при клике на кнопку делать ее подсвеченной, а другая – выдавать сообщение. Нужно в разных местах два обработчика повесить.

При этом новый обработчик будет затирать предыдущий. Например, следующий код на самом деле назначает один обработчик – последний:

input.onclick = function() { alert(1); }
// ...
input.onclick = function() { alert(2); } // заменит предыдущий обработчик

Разработчики стандартов достаточно давно это поняли и предложили альтернативный способ назначения обработчиков при помощи специальных методов, которые свободны от указанного недостатка.

addEventListener и removeEventListener

Методы addEventListener и removeEventListener являются современным способом назначить или удалить обработчик, и при этом позволяют использовать сколько угодно любых обработчиков.

Назначение обработчика осуществляется вызовом addEventListener с тремя аргументами:

element.addEventListener(event, handler[, phase]);
event
Имя события, например click
handler
Ссылка на функцию, которую надо поставить обработчиком.
phase
Необязательный аргумент, «фаза», на которой обработчик должен сработать. Этот аргумент редко нужен, мы его рассмотрим позже.

Удаление обработчика осуществляется вызовом removeEventListener:

// передать те же аргументы, что были у addEventListener
element.removeEventListener(event, handler[, phase]);
Удаление требует именно ту же функцию

Для удаления нужно передать именно ту функцию-обработчик которая была назначена.

Вот так removeEventListener не сработает:

elem.addEventListener( "click" , function() {alert('Спасибо!')});
// ....
elem.removeEventListener( "click", function() {alert('Спасибо!')});

В removeEventListener передана не та же функция, а другая, с одинаковым кодом, но это не важно.

Вот так правильно:

function handler() {
  alert( 'Спасибо!' );
}

input.addEventListener("click", handler);
// ....
input.removeEventListener("click", handler);

Обратим внимание – если функцию не сохранить где-либо, а просто передать в addEventListener, как в предыдущем коде, то потом получить её обратно, чтобы снять обработчик, будет невозможно. Нет метода, который позволяет считать обработчики событий, назначенные через addEventListener.

Метод addEventListener позволяет добавлять несколько обработчиков на одно событие одного элемента, например:

<input id="elem" type="button" value="Нажми меня"/>

<script>
  function handler1() {
    alert('Спасибо!');
  };

  function handler2() {
    alert('Спасибо ещё раз!');
  }

  elem.onclick = function() { alert("Привет"); };
  elem.addEventListener("click", handler1); // Спасибо!
  elem.addEventListener("click", handler2); // Спасибо ещё раз!
</script>

Как видно из примера выше, можно одновременно назначать обработчики и через DOM-свойство и через addEventListener. Однако, во избежание путаницы, рекомендуется выбрать один способ.

addEventListener работает всегда, а DOM-свойство – нет

У специальных методов есть ещё одно преимущество перед DOM-свойствами.

Есть некоторые события, которые нельзя назначить через DOM-свойство, но можно через addEventListener.

Например, таково событие transitionend, то есть окончание CSS-анимации. В большинстве браузеров оно требует назначения через addEventListener.

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

<style>
  button {
    transition: width 1s;
    width: 100px;
  }

  .wide {
    width: 300px;
  }
</style>

<button id="elem" onclick="this.classList.toggle('wide');">
  Нажми меня
</button>

<script>
  elem.ontransitionend = function() {
    alert( "ontransitionend" ); // не сработает
  };

  elem.addEventListener("transitionend", function() {
    alert( "addEventListener" ); // сработает по окончании анимации
  });
</script>

Отличия IE8-

При работе с событиями в IE8- есть много отличий. Как правило, они формальны – некое свойство или метод называются по-другому. Начиная с версии 9, также работают и стандартные свойства и методы.

В IE8- вместо addEventListener/removeEventListener используются свои методы.

Назначение обработчика осуществляется вызовом attachEvent:

element.attachEvent("on" + event, handler);

Удаление обработчика – вызовом detachEvent:

element.detachEvent("on" + event, handler);

Например:

function handler() {
  alert( 'Спасибо!' );
}
button.attachEvent("onclick", handler) // Назначение обработчика
  // ....
button.detachEvent("onclick", handler) // Удаление обработчика

Как видите, почти то же самое, только событие должно включать префикс on.

У обработчиков, назначенных с attachEvent, нет this

Обработчики, назначенные с attachEvent не получают this!

Это важная особенность и подводный камень старых IE.

Чтобы ваш код работал в старом IE, нужно либо использовать DOM-свойства, то есть onclick, либо подключить полифилл для современных методов, например такой или с сервиса polyfill.io или какой-то другой.

Итого

Есть три способа назначения обработчиков событий:

  1. Атрибут HTML: onclick="...".
  2. Свойство: elem.onclick = function.
  3. Специальные методы:
  • Современные: elem.addEventListener( событие, handler[, phase]), удаление через removeEventListener.
  • Для старых IE8-: elem.attachEvent( on+событие, handler ), удаление через detachEvent.

Сравнение addEventListener и onclick:

Достоинства
  • Некоторые события можно назначить только через addEventListener.
  • Метод addEventListener позволяет назначить много обработчиков на одно событие.
Недостатки
  • Обработчик, назначенный через onclick, проще удалить или заменить.
  • Метод onclick кросс-браузерный.

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

Задачи

важность: 5

Используя JavaScript, сделайте так, чтобы при клике на кнопку исчезал элемент с id="text".

Демо:

Открыть песочницу для задачи.

важность: 5

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

Как эта:

Решение задачи заключается в использовании this в обработчике.

<input type="button" onclick="this.style.display='none'" value="Нажми, чтобы меня спрятать" />
важность: 5

В переменной button находится кнопка.

Изначально обработчиков на ней нет.

Что будет выведено при клике после выполнения кода?

button.addEventListener("click", function() { alert("1"); });

button.removeEventListener("click", function() { alert("1"); });

button.onclick = function() { alert(2); };

Ответ: будет выведено 1 и 2.

Первый обработчик сработает, так как он не убран вызовом removeEventListener. Для удаления обработчика нужно передать в точности ту же функцию (ссылку на нее), что была назначена, а в коде передается такая же с виду функция, но, тем не менее, это другой объект.

Для того, чтобы удалить функцию-обработчик, нужно где-то сохранить ссылку на неё, например так:

function handler() {
  alert( "1" );
}

button.addEventListener("click", handler);
button.removeEventListener("click", handler);

Обработчик button.onclick сработает независимо и в дополнение к назначенному в addEventListener.

важность: 5

Создайте меню, которое раскрывается/сворачивается при клике:

P.S. HTML/CSS исходного документа понадобится изменить.

Открыть песочницу для задачи.

Для начала, зададим структуру HTML/CSS.

Меню является отдельным графическим компонентом, его лучше поместить в единый DOM-элемент.

Элементы меню с точки зрения семантики являются списком UL/LI. Заголовок должен быть отдельным кликабельным элементом.

Получаем структуру:

<div class="menu">
  <span class="title">Сладости (нажми меня)!</span>
  <ul>
    <li>Пирог</li>
    <li>Пончик</li>
    <li>Мед</li>
  </ul>
</div>

Для заголовка лучше использовать именно SPAN, а не DIV, так как DIV постарается занять 100% ширины, и мы не сможем ловить click только на тексте:

<div style="border: solid red 1px">[Сладости (нажми меня)!]</div>

…А SPAN – это элемент с display: inline, поэтому он занимает ровно столько места, сколько занимает текст внутри него:

<span style="border: solid red 1px">[Сладости (нажми меня)!]</span>

Раскрытие/закрытие сделаем путём добавления/удаления класса .menu-open к меню, которые отвечает за стрелочку и отображение UL.

Обычно меню будет закрыто:

.menu ul {
  margin: 0;
  list-style: none;
  padding-left: 20px;
  display: none;
}

.menu .title::before {
  content: '▶ ';
  font-size: 80%;
  color: green;
}

Если же меню раскрыто, то есть имеет класс .menu-open, то стрелочка слева заголовка меняется и список детей показывается:

.menu.open .title::before {
  content: '▼ ';
}

.menu.open ul {
  display: block;
}

Для JavaScript остался минимум работы – только добавить/удалить класс при клике.

Открыть решение в песочнице.

важность: 5

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

Результат:

P.S. Как лучше отобразить кнопку справа-сверху: через position:absolute или float:right? Почему?

Открыть песочницу для задачи.

  1. Изменим HTML/CSS, чтобы кнопка была в нужном месте сообщения. Кнопка – это тег <button>, поэтому понадобится несколько стилей.

    Расположить кнопку справа можно при помощи position:relative для pane, а для кнопки position:absolute + right/top. Так как position:absolute вынимает элемент из потока, то кнопка может частично оказаться «сверху» текста заголовка, перекрыв его конец. Чтобы этого не произошло, можно добавить padding-right к заголовку.

    Если использовать float:right, то кнопка никогда не перекроет текст. Это, пожалуй хорошо.

    С другой стороны, потенциальным преимуществом способа с position по сравнению с float в данном случае является возможность поместить элемент кнопки в HTML после текста, а не до него.

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

Открыть решение в песочнице.

важность: 4

Напишите «Карусель» – ленту изображений, которую можно листать влево-вправо нажатием на стрелочки.

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

P.S. В этой задаче разработка HTML/CSS-структуры составляет 90% решения.

Открыть песочницу для задачи.

Лента изображений в разметке должна быть представлена как список <ul> тегов <img>.

Нужно расположить его внутри <div> фиксированного размера, так чтобы в один момент была видна только нужная часть списка:

Чтобы список был длинный и элементы не переходили вниз, ему ставится width: 9999px, а элементам <li>, соответственно, float:left, либо для элементов используется display: inline-block, как в этом решении.

Главное – не использовать display:inline, так как такие элементы имеют дополнительные отступы снизу для возможных «хвостов букв».

В частности, для <img> нужно поставить в стилях явно display:block, чтобы пространства под ними не оставалось.

Для «прокрутки» будем сдвигать <ul>. Это можно делать по-разному, например, назначением CSS-свойства transform: translateX() или margin-left:

У внешнего <div> фиксированная ширина, поэтому «лишние» изображения обрезаются.

Снаружи окошка находятся стрелки и внешний контейнер.

Открыть решение в песочнице.

Карта учебника

Комментарии

перед тем как писать…
  • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.