Table of Contents

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:modelValue carries a number or null. null is a real outcome rather than an error signal, so a host that must not receive one either turns allowEmpty off or filters what it gets: the cutter section panel returns early on null so that clearing a field cannot write a zero into the profile, and the graphic-cache menu rejects null and every non-finite value before it calls the server.
  • parseError carries 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 integer field, 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 changes resetToken.
  • 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 modelValue is not a change and moves nothing; a change of resetToken once 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. hideBottomSpace stops 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.

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 through asciiNumberText from wwwroot-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 from widgets.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 rejects null and 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 the null a cleared field emits, so blanking a cutter dimension cannot write a zero.
  • wwwroot-src/src/components/spindle/SpindleContourEditor.vue — a host that turns allowEmpty off 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 the Infinity, -Infinity or NaN string the endpoint takes, and reads the two infinity spellings back; a NaN arriving 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