提问人:Kasbolat Kumakhov 提问时间:11/12/2023 更新时间:11/14/2023 访问量:109
当值被重新键入但未更改时,如何强制JS输入的“change”事件?
How to force JS input's 'change' event when value was retyped but not changed?
问:
我知道有很多类似的问题,但我还没有找到确切的情况。
使用vanilla JS,有一个用户在其中键入内容的控件。以日期为例(但可以是电话号码或其他任何具有固定格式的内容)。使用元素的事件验证输入数据。因此,当用户完成(通过按下或离开控件或提交等)时,将进行验证,如果出现错误,则会显示错误消息。input
change
enter
为了获得良好的用户体验,一旦用户再次开始键入(即尝试编辑错误),验证错误就会被清除。这是必需的,这样用户就不会在键入数据时因为尚未完成而混淆数据是“无效”的。当他完成输入时,数据将再次重新验证。我们没有进行实时验证,因为它看起来很混乱(“我输入的数据是否已经无效?”)。
例如,键入未完成的日期(没有年份)将导致验证错误。当用户再次开始键入时,错误将被清除,直到用户完成。12.12
现在考虑一个案例:
- 用户类型
12.12
; - 印刷机
enter
; - 验证开始并导致错误;
- 用户清除输入并再次键入;
12.12
- 印刷机
enter
; - 不会发生验证,因为元素的值没有变化,因此没有事件。
input
change
那么问题来了,如何让元素相信数据实际上已经改变,以便在用户完成编辑时再次触发事件?input
不确定模拟事件是否是一个好主意(例如,通过手动调度它或类似的东西)。change
blur
keypress=enter
我正在寻找类似“优化”标志的东西,当禁用时,无论实际更改的值如何,都会强制它调度事件。或者类似的东西可以称为 inside element 的事件。input
change
invalidateElementValue
input
答:
从一些 OP 和我上面的评论......
良好的用户体验要么对(表单)数据提交进行验证,要么对每个发生的
输入
事件进行无提示的后台验证。关于验证,-event 几乎总是没有用处。– 彼得·塞利格change
@PeterSeliger打字过程中的后台验证不是一个好的用户体验,因为它会让用户混淆他现在输入的数据已经无效。在这种情况下,您可以建议哪个事件?– 卡斯博拉特·库马霍夫
正如我所建议/陈述的,验证发生在(表单)数据或每个事件上,甚至在两种事件类型上。良好的用户体验取决于如何干扰用户的期望。因此,为了在需要时以最支持和最不令人讨厌的方式提供正确的信息,必须提出一些复杂的事件和数据处理。但这不会改变建议的事件类型。submit
input
注意
下面发布的代码并不是关于如何解决 OP 问题和需要进行验证的建议。它只是一个演示者,以显示收集正确数据时所需的复杂程度,这些数据将依据这些数据做出所有用户体验决策。
function handleInvalidatedRepetition(validationOutput) {
validationOutput.classList.add('warning');
validationOutput.value = 'This value has been invalidated before.'
}
function handleFailedValidation(validationRoot, control/*, validationOutput*/) {
validationRoot.classList.add('validation-failed');
// validationOutput.value = 'This is an invalid value.';
control.blur();
}
function clearInvalidatedRepetition(control, validationOutput) {
const invalidationsLookup = controlRegistry.get(control);
if (
invalidationsLookup &&
!invalidationsLookup.has(control.value) &&
validationOutput.classList.contains('warning')
) {
validationOutput.classList.remove('warning');
validationOutput.value = '';
}
}
function clearValidationStates({ currentTarget: control }) {
const validationRoot = control.closest('label[data-validation]');
const validationOutput = validationRoot.querySelector('output');
const invalidationsLookup = controlRegistry.get(control);
if (validationRoot.classList.contains('validation-failed')) {
validationRoot.classList.remove('validation-failed');
control.value = '';
}
clearInvalidatedRepetition(control, validationOutput);
}
function assureNoDotChainedNumbers(evtOrControl) {
let result;
const isEvent = ('currentTarget' in evtOrControl);
const control = isEvent && evtOrControl.currentTarget || evtOrControl;
const invalidationsLookup = controlRegistry.get(control);
if (invalidationsLookup) {
const { value } = control;
const isValid = !(/\d*(?:\.\d+)+/g).test(value);
const validationRoot = control.closest('label[data-validation]');
const validationOutput = validationRoot.querySelector('output');
clearInvalidatedRepetition(control, validationOutput);
if (!isEvent) {
if (!isValid) {
invalidationsLookup.add(value);
handleFailedValidation(validationRoot, control, validationOutput);
}
result = isValid;
} else if (!isValid && invalidationsLookup.has(value)) {
handleInvalidatedRepetition(validationOutput);
}
}
return result;
}
function validateFormData(elmForm) {
return [...elmForm.elements]
.filter(control =>
!(/^(?:fieldset|output)$/).test(control.tagName.toLowerCase())
)
.every(control => {
const validationType =
control.closest('label[data-validation]')?.dataset.validation ?? '';
if (!controlRegistry.has(control)) {
controlRegistry.set(control, new Set);
}
return validationLookup[validationType]?.(control) ?? true;
});
}
function handleFormSubmit(evt) {
const success = validateFormData(evt.currentTarget);
if (!success) {
evt.preventDefault();
}
return success;
}
const validationLookup = {
'no-dot-chained-numbers': assureNoDotChainedNumbers,
};
const eventTypeLookup = {
'input-text': 'input',
}
const controlRegistry = new WeakMap;
function main() {
const elmForm = document.querySelector('form');
[...elmForm.elements]
.filter(control =>
!(/^(?:fieldset|output)$/.test(control.tagName.toLowerCase()))
)
.forEach(control => {
const controlName = control.tagName.toLowerCase();
const controlType = control.type && `-${ control.type }` || '';
const eventType =
eventTypeLookup[`${ controlName }${ controlType }`] ?? '';
const validationType =
control.closest('label[data-validation]')?.dataset.validation ?? '';
const validationHandler = validationLookup[validationType];
if (eventType && validationHandler) {
control.addEventListener(eventType, validationHandler);
control.addEventListener('focus', clearValidationStates);
}
});
elmForm.addEventListener('submit', handleFormSubmit);
}
main();
body { margin: 0; }
ul { margin: 4px 0 0 0; }
fieldset { padding: 12px 16px 16px 16px; }
label { padding: 8px 12px 10px 12px; }
code { background-color: #eee; }
.validation-failed {
outline: 1px dashed red;
background-color: rgb(255 0 0 / 25%);
}
.warning { color: #ff9000; }
<form>
<fieldset>
<legend>No dot chained numbers</legend>
<label data-validation="no-dot-chained-numbers">
<span class="label">No dot chained numbers</span>
<input type="text" placeholder="No dot chained numbers" />
<output></output>
</label>
</fieldset>
</form>
<ul>
<li>E.g. do type <code>12.45</code>.</li>
<li>Press <code><Enter></code>.</li>
<li>Focus the <code>input</code> element again.</li>
<li>Type e.g. another dot chained number sequence.</li>
<li>
Maybe repeat the above task sequence by pressing <code><Enter></code> again.
</li>
<li>Do type input <code>12.45</code> again.</li>
<li>... Try other stuff; play around ...</li>
</ul>
评论
enter
12.45
enter
tabbing
上一个:为什么验证失败时不显示错误消息?
评论
个好主意“:为什么首先使用
变更
事件?根据您的描述,这似乎不是正确的选择。您确定它是如何工作的以及您希望接受哪些条件作为“最终确定”状态吗?输入
事件进行静默的后台验证。关于验证,-event 几乎总是没有用处。change
blur
enter
change