我知道有很多类似的问题,但我还没有找到确切的情况。
使用 vanilla JS 有一个
input
控件,用户可以在其中键入内容。以日期为例(但可以是电话号码或任何其他具有固定格式的内容)。使用元素的 change
事件验证输入数据。因此,当用户完成(通过按 enter
或离开控件或提交等)时,会进行验证,如果出现问题,则会显示错误消息。
对于良好的用户体验,一旦用户开始再次输入(即尝试编辑错误),验证错误就会被清除。这是必要的,这样用户在输入数据时就不会因为尚未完成而混淆数据“无效”。当他完成输入时,数据会再次重新验证。我们没有进行实时验证,因为它看起来很混乱(“我输入的数据已经无效了吗?”)。
例如,输入未完成的日期,例如
12.12
(没有年份)将导致验证错误。当用户再次开始输入时,错误将被清除,直到用户完成为止。
现在考虑一个案例:
12.12
;enter
;12.12
;enter
;input
元素的值没有变化,因此没有 change
事件。所以问题是,如何使
input
元素相信数据实际上已更改,以便在用户完成编辑时再次触发该事件?
不确定模拟
change
事件是否是一个好主意(例如,通过在 blur
或 keypress=enter
或类似的内容中手动调度它)。
我正在寻找类似
input
的“优化”标志之类的东西,当禁用时,将强制它调度 change
事件,而不管实际更改的值如何。或者类似 invalidateElementValue
的东西可以在元素的 input
事件内部调用。
使用多个事件进行主动验证
问题询问如何在更改后或用户按 ENTER 时验证输入,但这也会在编辑时抑制烦人的消息。
用户有时可能会发现这种即时反馈很有帮助。例如,查看选定日期有哪些座位可用或快速了解用户名是否可用。一般来说,用户在继续之前可能想知道的任何信息。
我们可以通过触发对 change 和 keydown 事件的验证来完成此工作。并且 input 事件可用于在编辑期间禁用验证。
虽然可以添加额外的调整,但效果很好。例如,只允许按下 Enter 时提交按钮提交表单。
所需的最低代码
form.addEventListener('change', (e) => {
validation(e);
});
form.addEventListener('keydown', (e) => {
if (e.code === 'Enter') validation(e);
})
form.addEventListener('input', (e) => {
e.target.setCustomValidity('');
});
function validation(e) {
// if e.target.value invalid
e.target.setCustomValidity('some message');
e.target.reportValidity();
}
当用户按 Enter 或 Tab 键进入下一个输入时,验证将显示浏览器生成的弹出窗口。
演示片段
document.querySelectorAll('form').forEach(form => {
// Valdate input on change
form.addEventListener('change', (e) => {
if (e.target.classList.contains('date')) {
dateValidation(e);
}
});
// Validate input on Enter key
form.addEventListener('keydown', (e) => {
if (e.code === 'Enter') {
if (e.target.classList.contains('date')) {
dateValidation(e);
}
// Prevents non-submit buttons from triggering form submit
if (e.target.type !== 'submit') {
e.preventDefault();
}
}
})
// Clear to suppress messages while editing
form.addEventListener('input', (e) => {
e.target.setCustomValidity('');
});
// Simulate form submit
form.addEventListener('submit', (e) => {
e.preventDefault();
console.log('form submitted');
});
});
function dateValidation(e) {
let date = new Date(e.target.value),
min = new Date(e.target.getAttribute('min')),
max = new Date(e.target.getAttribute('max')),
msg = '';
if (isNaN(date)) {
msg = 'The date is invalid';
} else if (!isNaN(min) && min > date) {
msg = `Date minimum is ${min.toDateString()}`;
} else if (!isNaN(max) && max < date) {
msg = `Date maximum is ${max.toDateString()}`;
}
if (msg) {
e.target.setCustomValidity(msg);
e.target.reportValidity();
}
}
label {
font-family: sans-serif;
display: block;
margin-bottom: 0.5rem;
}
<form>
<label>
<input type="text"
name="arrive"
class="date"
min="2023-11-01T00:00:00"
max="2023-12-31T00:00:00"
required>
Arrival Date
</label>
<label>
<input type="date"
name="depart"
class="date"
max="2023-12-31T00:00:00"
required>
Departure Date
</label>
<label>
<input type="text" name="comment">
Comments
</label>
<input type="submit">
</form>
来自一些OP和我的上述评论......
良好的用户体验要么对(表单)数据提交进行验证,要么默默地对每个发生的
事件进行不令人厌烦的后台验证。关于验证,input
事件几乎总是没有什么用处。 – 彼得·塞利格change
@PeterSeliger 在输入过程中进行后台验证并不是一个好的用户体验,因为它会让用户感到困惑,因为他现在输入的数据已经无效。在这种情况下,您建议举办什么活动? – 卡斯博拉特·库马霍夫
正如我提议/所述,验证发生在(表单)数据
submit
或每个 input
事件,甚至两种事件类型。良好的用户体验取决于干扰用户期望的种类和方式。因此,为了在需要时以最有支持性且最不烦人的方式提供正确的信息,人们必须想出一些复杂的事件和数据处理。但这不会改变建议的事件类型。
function handleInvalidatedRepetition(validationOutput) {
validationOutput.classList.add('warning');
validationOutput.value = 'This value already 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 ?? '';
if (eventType && validationType) {
// console.log({ eventType, validationType });
control.addEventListener(
eventType, validationLookup[validationType]
);
}
control.addEventListener('focus', clearValidationStates);
});
elmForm.addEventListener('submit', handleFormSubmit);
}
main();
fieldset { padding: 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" palceholder="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>