Numeric Input
NumericInput is the single-value numeric field the rest of the web application embeds. It has no
route and no Control-Tree node of its own: it is reached only through the editors that host it,
and those span the equipment and mission trees, the Tool House tool editors, the transformer and
geometry editors and the Preference menu. The rule for carrying NaN and
the infinities across the JSON boundary is stated once in
Numeric Input/Output; this page is the widget that implements
the client end of it.
The Binding Contract
The widget owns no value. It holds the raw text the user is typing and nothing else, and every committed number is handed straight back to the host, which decides what to persist and when.
| Prop | Meaning |
|---|---|
modelValue |
number, null or undefined — the value to show |
label |
the field's label, inside the outline; floats to the top of it once the field has content |
hint |
text below the field |
unit |
a display-only suffix inside the field |
readonly / disable |
passed through to the underlying field |
min / max |
inclusive bounds, checked on commit |
integer |
whole numbers only: a fraction, Infinity, -Infinity or NaN is refused on commit; off by default |
allowEmpty |
whether a blank field commits null; defaults to on |
hideBottomSpace |
stop holding space below the field; a hint or message still renders when there is one |
rules |
extra validation functions over the raw text |
commitOnInput |
also commit as the text is typed, as Commit Semantics describes; off by default |
resetToken |
when it changes and the field does not hold the focus, a text that does not show modelValue — a refused one, with its message — is dropped and the field shows modelValue again |
Two events leave the widget.
update:modelValuecarries a number ornull.nullis a real outcome rather than an error signal, so a host that must not receive one either turnsallowEmptyoff or filters what it gets: the cutter section panel returns early onnullso that clearing a field cannot write a zero into the profile, and the graphic-cache menu rejectsnulland every non-finite value before it calls the server.parseErrorcarries the raw text that failed to parse. No shipped host listens for it, so a parse failure is visible only as the message under the field.
Commit Semantics
Typing changes nothing but the text in the box, unless the host sets commitOnInput. The commit runs
on blur and on Enter — and, under commitOnInput, on a keystroke as well — and on nothing else: there
is no debounce and no timer. The Enter is taken on
keydown, and the one that confirms an IME conversion is not a commit
(Internationalization). The underlying control is
a plain text field rather than a browser number input, so there is no spinner, no step and no
keystroke-level filtering: any text at all may sit in the box until the field is committed.
Under commitOnInput, a keystroke that leaves a finite number the blur would accept, or a blank box
where allowEmpty is on, emits that value at once, so a host's draft holds a typed value before the
field loses focus. Any other text stays as typed, puts the model back to the value it had at focus-in
or that a change from outside set since — never a prefix that parsed on the way, the 1 of 1,500 —
and is left to the blur, which commits or refuses it as below; a text that no further typing can turn
into a number shows its Invalid number: "…" message at once.
A commit parses the text once and then takes one of three exits. On success the error strip is
cleared, update:modelValue is emitted, and the box is rewritten from the parsed number. On text
that does not parse, parseError is emitted and a message appears. On a number outside the bounds,
or one that is not whole on an integer field, a message appears. Neither failing exit emits
update:modelValue, and neither rewrites the box.
Enter commits without moving focus, so the field is committed again when focus finally leaves it.
That second commit parses to the number the host already holds and stops inside the widget: after
the bounds check the parsed value is compared against modelValue (Object.is, so a NaN entry
matches a NaN model) and update:modelValue is emitted only on a difference — the same guard the
vector and matrix editors carry. A blur that changed nothing therefore never reaches the host:
focus moving to a button in the field's own append slot, or through a field the user only clicked,
is not an edit. That matters where a commit means more than a write — the Cutter panel's Shank Mass
field turns any commit into a held value, and its return-to-estimate button sits in the append slot,
so without the guard pressing it would re-commit the displayed estimate as a held mass. The box is
still rewritten from the parsed number on that silent exit, so 1e1 still becomes 10.
Bounds and Special Values
min and max are inclusive and are enforced inside the widget, before anything is emitted: a
number below min or above max is refused and the host never sees it.
The bounds apply to finite numbers only. Infinity, -Infinity, NaN and the null a blank
field produces all skip the range check, so a field declared with a min of 0 still commits
-Infinity and NaN. Where that matters, the host filters what it receives, or sets integer,
which refuses all three non-finite values along with any fraction.
| Typed | Committed | Shown afterwards |
|---|---|---|
Infinity in any case, or ∞ |
Infinity |
Infinity |
-Infinity in any case, or -∞ |
-Infinity |
-Infinity |
NaN in any case |
NaN |
(empty) |
blank, allowEmpty on |
null |
(empty) |
blank, allowEmpty off |
nothing — parse error | the blank stays |
anything JavaScript's Number() reads as a finite number |
that number | its default string form |
| anything else | nothing — parse error | the rejected text stays |
The text is brought to ASCII before it is read: the full-width digits, point and signs an IME
types, the long-vowel mark ー, 。 and 、 its kana-mode minus, period and comma keys give, and a
pasted minus sign − or dash each become their ASCII form, so 0.5 and 。5 commit 0.5 and ー3
commits -3. Nothing else is folded, so 10² or ① is still a parse error rather than 102 or 1.
Number() is the whole numeric parser, so exponent notation and the 0x, 0b and 0o integer
literals are accepted alongside ordinary decimals, while a thousands separator is not. Its result
must also come back finite to be accepted, so a literal that overflows the double — 1e400 — is a
parse error rather than a second route to Infinity. The two spellings at the head of the table
are the only way to reach one.
Two consequences follow from NaN and null sharing the empty box. A NaN arriving from the
server is indistinguishable from a blank field; and because a blank field commits null under the
default, merely focusing such a field and leaving it replaces the NaN with null. With
allowEmpty turned off the same field instead fails to parse on every blur until something is
typed into it.
The infinity spellings the widget writes and reads are the same strings the API layer puts on the
wire for a non-finite number — see Numeric Input/Output. The
widget itself never touches the wire: it emits a JavaScript number, and converting a non-finite one
into its string form belongs to the host's API module, as wwwroot-src/src/api/mission.ts does for
the mission commands. That conversion is symmetric: the mission module writes all three strings and
reads the same three back case-insensitively, so a NaN returned by the endpoint arrives as NaN;
the caller's supplied default covers only a missing or unparsable value.
Unit Suffix and Precision
unit is rendered as a suffix inside the field, to the right of the text. It is decoration only:
it is not part of the editable text, it is not parsed, and no unit conversion happens anywhere in
the widget — the number committed is in whatever unit the host's model already uses. Hosts pass
plain unit text, mm and deg being the common ones, alongside mm/min, rpm, N, °C and
MB.
There is no precision, decimals or step prop. A value is displayed through JavaScript's default number-to-string conversion, so it shows at full precision and switches to exponent notation at the magnitudes where that conversion does. Rounding, where a host wants it, is the host's own: the graphic-cache current-size field rounds in its commit handler, after the widget has handed the number over, while the two limit fields in the same menu send what they were given.
Validation Messages
Four messages can appear under the field: Invalid number: "…" quoting the rejected text,
Must be ≥ … and Must be ≤ … quoting the bound, and Must be a whole number on an integer field. They
are read from the widgets.numericInput keys of the i18n bundles, so they follow the application's
language setting, like the label and hint hosts pass in already translated.
rules is a separate mechanism and differs in three ways: its functions receive the raw display
text rather than the parsed number, they run on the underlying field's own validation pass —
as the text changes, and again when focus leaves — rather than inside the commit, and they do
not gate the commit: a value failing a caller rule is still parsed, still bounds-checked and
still emitted. While the widget's own message is showing, it also masks the rule's.
No shipped host passes rules, and none fills the append slot, so both are available rather
than exercised. hideBottomSpace is the opposite — it is among the most-passed props on the
widget, set on nearly every field the SoftNc controller panels, the spindle contour editor and the
fluting span list drop into a table cell.
When the Box and the Model Disagree
A rejected commit leaves the typed text in the box. That text is replaced only when modelValue
changes to a value the text does not already parse to, or resetToken changes while the field does
not hold the focus; the widget does not revert
on its own. That is the second thing the vector and matrix editors do that this one does not: both
reinstate the last valid value the moment a cell fails to parse. Two situations follow, and both
put a number on screen that was never stored:
- A number outside the bounds, a fraction on an
integerfield, or text that does not parse, stays in the box under its error message. Nothing restores it until the host pushes a different value or changesresetToken. - A commit the host accepts and the server then refuses leaves the box showing the refused number
unless the host had already applied it locally, because assigning the unchanged value back to
modelValueis not a change and moves nothing; a change ofresetTokenonce the refusal lands puts the stored value back in the box.
The message goes with the text: the next successful commit clears it, and so does a change of
modelValue that replaces the text, or of resetToken on a field without the focus.
Layout
- Numeric Input Field
- Label — the host's
label, positioned inside the field's outline: on the input line while the field is empty and unfocused, floated to the top of the outline once it has a value or the focus; absent when none is given - Text Box — one line, free text until committed
- Unit Suffix — the host's
unit, inside the field at the right; display only - Append Slot — an optional trailing slot for a host-supplied control
- Bottom Strip — the host's
hint, replaced by the validation message while the field is in error.hideBottomSpacestops space being held below the field but does not suppress the strip: a hint or a validation message still renders, and with nothing reserved for it, it grows the field instead of filling a gap already left below.
- Label — the host's
Source Code Path
See HiNC App Anatomy for git repository links.
Web Application
HiNC-2025-webservice (Quasar CLI SPA):
wwwroot-src/src/components/widgets/NumericInput.vue— the widget: the raw-text buffer, the local format and parse pair (the parse reading the text throughasciiNumberTextfromwwwroot-src/src/utils/numberText.ts), the blur-and-Enter commit, the whole-number check, the inclusive bounds check that skips non-finite values, and the four validation messages read fromwidgets.numericInput.wwwroot-src/src/components/preference/GraphicCacheMenu.vue— a compact host of three instances, one of them bounded by the values of the other two; every handler rejectsnulland non-finite numbers, and the current-size handler is the one that rounds before sending.wwwroot-src/src/components/controlTree/toolhouse/CutterSectionPanel.vue— a host that returns early on thenulla cleared field emits, so blanking a cutter dimension cannot write a zero.wwwroot-src/src/components/spindle/SpindleContourEditor.vue— a host that turnsallowEmptyoff and keeps its own draft copy of the contour points around the blur commit.wwwroot-src/src/api/mission.ts— the module that turns a committed non-finite number into theInfinity,-InfinityorNaNstring the endpoint takes, and reads the two infinity spellings back; aNaNarriving from the endpoint falls through to the caller's default instead.
See Also
- Numeric Input/Output — the cross-boundary rule this widget implements the client half of, and how the three numeric inputs differ from one another
- Widgets — the other controls that pages embed rather than own
- Editing Contract — the branch that embeds this field most heavily, and the commit rules its panels inherit from it