input, change и живой ввод

ввод≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Поле ввода разговаривает с кодом двумя голосами: input сообщает о каждом изменении значения прямо во время набора, change — только когда правка считается законченной. Разница в один момент времени, но именно она решает, будет ли форма живой или дёрганой.

Разделы темы:·

input срабатывает после любого изменения значения: набрали символ, стёрли его, вставили текст мышью, надиктовали голосом, выбрали дату в календарике. Событие всегда приходит после того, как value уже обновилось, — читать новое значение можно сразу.

change ждёт, пока правка закончится. Для текстового поля это значит: пользователь ушёл с поля или нажал Enter. Для чекбокса, радиокнопки, select и загрузки файла «закончилась» правка мгновенно, поэтому change там прилетает сразу за input. Отсюда простое правило: живая реакция — на input, тяжёлая работа вроде запроса на сервер — на change.

СобытиеКогда срабатываетВсплываетОтменяемо
beforeinputперед изменением значениядада
inputсразу после измененияданет
changeкогда правка завершенаданет
focus / blurэлемент получил / потерял фокуснетнет
focusin / focusoutто же самое, но всплываетданет
js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
Присваивание value из кода молчит: ни input, ни change, поэтому после него массив пуст. Это не баг — иначе обработчик, правящий значение, зациклил бы сам себя. Нужен эффект — событие рассылают руками через dispatchEvent.

У чекбокса нечего «дописывать», поэтому клик даёт сразу оба события — сначала input, потом change. Метод click() устраивает настоящий клик, так что события здесь не поддельные.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

value у любого поля — строка, даже у type="number". Числовое значение лежит отдельно, в valueAsNumber, и превращается в NaN, когда поле пустое или содержит мусор. У чекбокса и радиокнопки читать нужно не value (там лежит то, что уйдёт на сервер), а checked; у поля с файлами — files.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Атрибут и свойство живут врозь: атрибут value в разметке — это начальное значение, а свойство value — текущее. После правки они расходятся, и это удобно: исходный текст всегда можно достать из defaultValue, чтобы откатить поле.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Самый частый приём: в обработчике input перезаписать value очищенным вариантом. Работает с любым способом ввода — с клавиатуры, вставкой, автозаполнением, — потому что чинит уже готовое значение, а не отдельные нажатия. И, раз присваивание из кода событий не порождает, рекурсии не будет. В React тот же фильтр устроен иначе: value в обработчике не перезаписывают, а неподходящий ввод просто не пускают в состояние — это показывает статья «Управляемые поля: состояние вместо DOM».

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

У приёма есть цена: перезапись value сбрасывает каретку в конец строки. Пока пользователь печатает в конце, этого не видно, но при правке середины текста курсор будет прыгать. Лечится запоминанием selectionStart до правки и возвратом через setSelectionRange() после — либо переходом на beforeinput, который умеет не пускать символ вовсе.

beforeinput приходит до изменения значения и отменяется через preventDefault(). В event.data лежит то, что собираются вставить, в inputType — как именно (insertText, deleteContentBackward, insertFromPaste). Значение при этом не дёргается, и каретка остаётся на месте.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Кто сейчас в фокусе, знает document.activeElement — если никто, там окажется <body>. Фокус можно поставить методом focus() и снять через blur(), но не на любой элемент: disabled-поля и обычные <div> без tabindex его не принимают.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Настоящие input и change всплывают, поэтому вешать обработчик на каждое поле незачем: достаточно одного слушателя на форме и event.target, чтобы понять, кто изменился. Поля, добавленные позже, начнут работать сами собой. У событий, созданных руками, всплытие по умолчанию выключено — это единственное место, где нужен флаг bubbles.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Собрав такой обработчик, дальше несложно достроить привычное поведение формы: считать заполненные поля, включать кнопку отправки, показывать подсказки на input и запускать проверку на focusout — когда пользователь уже дописал и ушёл дальше.

  • input срабатывает после каждого изменения значения, change — когда правка завершена: у текстового поля при уходе с него или по Enter, у чекбокса, radio и select — сразу.
  • Присваивание value из кода не порождает ни input, ни change; нужно событие — рассылайте его сами через dispatchEvent.
  • value всегда строка: число берут из valueAsNumber, состояние флажка — из checked, исходный текст — из defaultValue.
  • Ограничение ввода проще всего делать перезаписью value в обработчике input (помня про сброс каретки) или отменой в beforeinput.
  • focus и blur не всплывают — для делегирования есть focusin и focusout, а текущий элемент лежит в document.activeElement.

1/3К полю привязан обработчик input. Дальше код делает field.value = "текст". Сколько раз вызовется обработчик?

2/3Какие из событий фокуса всплывают?

3/3Пользователь набрал в текстовом поле пять символов и нажал Tab. Что произошло?

Счётчик оставшихся символов

Рядом с полем есть <span id="left"> с числом 100. Сделайте так, чтобы при каждом изменении текста в нём оказывалось, сколько символов ещё можно ввести. Дописывать проверку длины не нужно — за ней следит атрибут maxlength.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Единственный обработчик на input: он придёт и на набор с клавиатуры, и на вставку мышью, и на удаление.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Обработчик читает about.value уже после изменения — событие input всегда приходит следом за обновлением значения, поэтому длина считается по свежему тексту. На change счётчик бы ожил только при уходе с поля.

Код из четырёх цифр

Поле должно принимать только цифры и не длиннее четырёх: всё лишнее вычищается прямо во время ввода. А когда пользователь уходит с поля, в соседний <span> попадает текст «нужно четыре цифры», если цифр набрано меньше, и пустая строка, если всё в порядке.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Чистка — на input, проверка — на blur: ругаться на неполный код во время набора было бы невежливо.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Присваивание value внутри обработчика input само событий не порождает, так что рекурсии не будет. Минус решения — каретка после перезаписи уезжает в конец строки; если поле правят из середины, стоит запомнить selectionStart и вернуть его через setSelectionRange() либо отсекать лишнее ещё в beforeinput.