原创

HTML number 的小数为什么不合法:stepMismatch 取决于步进基点

number输入框允许小数,却提示“请输入有效值”。先看step,再看它从哪个基点开始。step=0.5不是简单允许所有一位小数,而是要求值落在基点加整数倍步长的位置上。

基点可能来自min,也可能来自初始value属性;没有相应值时才采用该类型的默认基点。于是两个同样写step=0.5的输入框,可能接受不同的数列。只盯着用户刚输入的值,容易把这次差异误判成浮点问题。

下面做一份可切换基点的完整页面,分别显示stepMismatch、范围越界和必填状态。它只观察约束,不自动把用户输入四舍五入成另一个值。

同一个步长,两组合法位置

保存step-base.html,在现代浏览器打开。初始value属性是0.2,没有min;勾选以后加入min=0。

<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<title>number 步进基点</title>
<style>
body { max-width: 46rem; margin: 2rem auto; padding: 1rem; font: 16px sans-serif; }
input, button { font: inherit; }
table { border-collapse: collapse; margin-top: 16px; }
th, td { padding: 8px; border: 1px solid #789; }
pre { white-space: pre-wrap; }
</style>
<label>数量 <input id="amount" type="number" value="0.2" step="0.5" max="2" required></label>
<label><input id="minimum" type="checkbox">加入 min=0,优先以它作为基点</label>
<button id="inspect" type="button">检查当前值</button>
<pre id="report" role="status"></pre>
<table>
<caption>在当前约束下检查固定样本</caption>
<thead><tr><th>值</th><th>步进不符</th><th>超上限</th><th>缺失</th><th>有效</th></tr></thead>
<tbody id="rows"></tbody>
</table>
<script>
const input = document.querySelector('#amount');
function state() {
  return [input.validity.stepMismatch, input.validity.rangeOverflow,
          input.validity.valueMissing, input.validity.valid];
}
function render() {
  document.querySelector('#report').textContent = [
    `初始value属性=${input.getAttribute('value')};当前value=${JSON.stringify(input.value)}`,
    `min属性=${input.getAttribute('min')};step=${input.step}`,
    `当前[步进不符,超上限,缺失,有效]=${JSON.stringify(state())}`
  ].join('\n');
  const original = input.value;
  const body = document.querySelector('#rows');
  body.replaceChildren();
  for (const text of ['0.2', '0.5', '0.7', '1.2', '2.2', '']) {
    input.value = text;
    const row = document.createElement('tr');
    for (const value of [text || '空值', ...state()]) {
      const cell = document.createElement('td');
      cell.textContent = String(value);
      row.append(cell);
    }
    body.append(row);
  }
  input.value = original;
}
document.querySelector('#minimum').addEventListener('change', event => {
  if (event.target.checked) input.min = '0';
  else input.removeAttribute('min');
  render();
});
input.addEventListener('input', render);
document.querySelector('#inspect').addEventListener('click', render);
render();
</script>
</html>

初始合法步进包含0.2、0.7、1.2,0.5反而不在这组位置。加入min=0后,0.5符合步进,0.2、0.7和1.2则不符合。原始value属性仍是0.2,改变的是基点优先级。

2.2在第一种基点下可以符合步进,却超过max=2,因此整体仍然无效。空值因为required而无效,不应该把所有拒绝都显示成“步长不对”。

当前输入值,不是每次重新建立步进基点

用户把值从0.2编辑成0.7,改变的是当前value,不会因此把初始value属性改成0.7。代码每次测试样本也只修改运行时的input.value,不重写HTML的value内容属性,所以能在同一基点下比较多项输入。

如果模板系统重新渲染时又改了value属性或min,合法数列就可能随之改变。排查应同时查看当前值、内容属性和范围设置,不能只在控制台打印一个input.value就结束。

对重复使用的表单组件,明确min通常比让每份初始数据暗中决定步进起点更容易解释。比如数量必须是0.5的整数倍,就可以按业务设定相应基点;若需求本来是以某个偏移起步,也应把这份偏移明确放进模型。

step=any 放开的是步进,不是全部约束

把step改成any,可以不按固定步长验证,但max、min、required等限制仍然存在。它不是“接受任意字符串”,number类型仍然有自己的值语法与界面处理。

如果业务只是允许任意小数,不要求固定间隔,any可能更符合意图。若业务要求只能按半件、一刻钟或固定价格档位选择,就需要保留步长,并清楚说明基点。不能为了让一个失败样本通过,就把所有步进规则关闭。

number类型的默认step也不能忽略。未显式设置时,默认步进通常按整数尺度处理,这不代表键盘完全无法输入小数,而是某些小数可能无法通过约束验证。可输入、可保留在控件里与可提交是三个观察点。

不要用一次取模代替完整约束模型

JavaScript直接用valueAsNumber除以step再取模,还可能受到二进制浮点表示影响,而且容易漏掉基点偏移。浏览器约束验证有自己的定义,后端业务规则也应采用适合数值域的精确表示,不能把一条简单百分号表达式当成跨端协议。

金额、计量或时间槽的步进应该在后台重复验证。前端控件可以被脚本改写,用户也能直接发送请求;原生step只是交互与约束入口,不是数据可信保证。

若后端按最小单位整数表示,可以把允许的步长和偏移转换成相应整数关系,再验证范围;涉及任意十进制时,则应选择明确十进制解析。无论采用哪种,单位和舍入策略都需要与前端提示一致。

前端也不应在失焦时偷偷把不合法输入改成最近档位。0.7不符合某份基点,不代表用户同意把它保存为0.5或1.0。需要自动对齐时,应有明确交互与可恢复原值,而不是让约束错误变成静默改数。

空值解析也要单独处理。number控件的valueAsNumber在无法形成数字时可以是NaN,而直接对空字符串调用Number却可能得到0。若后台不允许缺失数量,不能把空框经普通数值转换悄悄变成零,再宣称满足步进。

同理,0是合法业务值时,也不能用if(!number)一口气拒绝零和缺失。应先判断字段是否提供、能否解析,再验证范围和步进;这条顺序与页面分开显示valueMissing的目的相同。

用具体 ValidityState 解释拒绝原因

页面把各项标志并排显示,是为了避免只看valid=false。一个值可以同时触发范围与步进问题,错误提示需要按产品策略选择清楚原因,但测试应保留所有关键标志。

程序通过脚本设置固定样本,验证的是值与约束,不声称已经验收每个平台数字键盘如何处理逗号、指数或临时输入状态。真实移动端交互仍应针对目标浏览器检查,并与后端数值语法对齐。

最后去掉min,再把HTML初始value属性改成0.1,重新加载页面。合法数列应跟着新的基点偏移,而不是保持原来的0.2序列。这个对照能直接证明问题来自步进起点,避免继续用“多保留几位小数”修错方向。

正文到此结束
评论插件初始化中...
Loading...