input, change и живой ввод
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Поле ввода разговаривает с кодом двумя голосами: input сообщает о каждом изменении значения прямо во время набора, change — только когда правка считается законченной. Разница в один момент времени, но именно она решает, будет ли форма живой или дёрганой.
input срабатывает после любого изменения значения: набрали символ, стёрли его, вставили текст мышью, надиктовали голосом, выбрали дату в календарике. Событие всегда приходит после того, как value уже обновилось, — читать новое значение можно сразу.
change ждёт, пока правка закончится. Для текстового поля это значит: пользователь ушёл с поля или нажал Enter. Для чекбокса, радиокнопки, select и загрузки файла «закончилась» правка мгновенно, поэтому change там прилетает сразу за input. Отсюда простое правило: живая реакция — на input, тяжёлая работа вроде запроса на сервер — на change.
| Событие | Когда срабатывает | Всплывает | Отменяемо |
|---|---|---|---|
| beforeinput | перед изменением значения | да | да |
| input | сразу после изменения | да | нет |
| change | когда правка завершена | да | нет |
| focus / blur | элемент получил / потерял фокус | нет | нет |
| focusin / focusout | то же самое, но всплывает | да | нет |
value из кода молчит: ни input, ни change, поэтому после него массив пуст. Это не баг — иначе обработчик, правящий значение, зациклил бы сам себя. Нужен эффект — событие рассылают руками через dispatchEvent.У чекбокса нечего «дописывать», поэтому клик даёт сразу оба события — сначала input, потом change. Метод click() устраивает настоящий клик, так что события здесь не поддельные.
value у любого поля — строка, даже у type="number". Числовое значение лежит отдельно, в valueAsNumber, и превращается в NaN, когда поле пустое или содержит мусор. У чекбокса и радиокнопки читать нужно не value (там лежит то, что уйдёт на сервер), а checked; у поля с файлами — files.
Атрибут и свойство живут врозь: атрибут value в разметке — это начальное значение, а свойство value — текущее. После правки они расходятся, и это удобно: исходный текст всегда можно достать из defaultValue, чтобы откатить поле.
Самый частый приём: в обработчике input перезаписать value очищенным вариантом. Работает с любым способом ввода — с клавиатуры, вставкой, автозаполнением, — потому что чинит уже готовое значение, а не отдельные нажатия. И, раз присваивание из кода событий не порождает, рекурсии не будет. В React тот же фильтр устроен иначе: value в обработчике не перезаписывают, а неподходящий ввод просто не пускают в состояние — это показывает статья «Управляемые поля: состояние вместо DOM».
У приёма есть цена: перезапись value сбрасывает каретку в конец строки. Пока пользователь печатает в конце, этого не видно, но при правке середины текста курсор будет прыгать. Лечится запоминанием selectionStart до правки и возвратом через setSelectionRange() после — либо переходом на beforeinput, который умеет не пускать символ вовсе.
beforeinput приходит до изменения значения и отменяется через preventDefault(). В event.data лежит то, что собираются вставить, в inputType — как именно (insertText, deleteContentBackward, insertFromPaste). Значение при этом не дёргается, и каретка остаётся на месте.
focus и blur — исключение из общего правила: они не всплывают, повесить их на форму и ловить все поля разом не выйдет. Для этого есть близнецы focusin и focusout с той же семантикой, но всплывающие как обычные события.
Кто сейчас в фокусе, знает document.activeElement — если никто, там окажется <body>. Фокус можно поставить методом focus() и снять через blur(), но не на любой элемент: disabled-поля и обычные <div> без tabindex его не принимают.
Настоящие input и change всплывают, поэтому вешать обработчик на каждое поле незачем: достаточно одного слушателя на форме и event.target, чтобы понять, кто изменился. Поля, добавленные позже, начнут работать сами собой. У событий, созданных руками, всплытие по умолчанию выключено — это единственное место, где нужен флаг bubbles.
Собрав такой обработчик, дальше несложно достроить привычное поведение формы: считать заполненные поля, включать кнопку отправки, показывать подсказки на 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.
Единственный обработчик на input: он придёт и на набор с клавиатуры, и на вставку мышью, и на удаление.
Обработчик читает about.value уже после изменения — событие input всегда приходит следом за обновлением значения, поэтому длина считается по свежему тексту. На change счётчик бы ожил только при уходе с поля.
Код из четырёх цифр
Поле должно принимать только цифры и не длиннее четырёх: всё лишнее вычищается прямо во время ввода. А когда пользователь уходит с поля, в соседний <span> попадает текст «нужно четыре цифры», если цифр набрано меньше, и пустая строка, если всё в порядке.
Чистка — на input, проверка — на blur: ругаться на неполный код во время набора было бы невежливо.
Присваивание value внутри обработчика input само событий не порождает, так что рекурсии не будет. Минус решения — каретка после перезаписи уезжает в конец строки; если поле правят из середины, стоит запомнить selectionStart и вернуть его через setSelectionRange() либо отсекать лишнее ещё в beforeinput.