Number input
ZNumberInput is one input for every figure in a form: money, percent, count and hours. It is Nuxt UI's UInput, so size, color, disabled, readonly and ui work as they do there. It formats the value as you type. The unit marks sit in fixed slots, so a column of values lines up. The chevrons and the ArrowUp/ArrowDown keys move the value by step from whatever is there, never snapping it to a multiple. The arithmetic is decimal, so 0.1 and 0.2 make 0.3, and the model comes back as the type you bound: a number stays a number, a string a string, a Decimal a Decimal.
Units
unit is money (the currency symbol on the left, from currency and locale), percent, count or hours.
<script setup lang="ts">
const fee = ref<number | null>(38500)
const rate = ref<number | null>(7.25)
const hours = ref<number | null>(40)
</script>
<template>
<div class="grid w-full max-w-xs gap-3">
<UFormField label="Fee">
<ZNumberInput v-model="fee" unit="money" :step="500" class="w-full" />
</UFormField>
<UFormField label="Rate">
<ZNumberInput v-model="rate" unit="percent" :step="0.25" :min="0" :max="100" class="w-full" />
</UFormField>
<UFormField label="Hours">
<ZNumberInput v-model="hours" unit="hours" class="w-full" />
</UFormField>
</div>
</template>Typing and locales
The value is grouped as you type (1,234,567), and the caret stays on its digit as separators come and go. Backspace or Delete over a separator takes the digit beyond it. A key that adds nothing, such as a letter or a second decimal mark, is refused, and a pasted $1,234.50 keeps only its figure. locale sets the marks typed and shown: in de the full stop groups and the comma is the decimal mark, so the field reads and writes 1.234,56. The currency symbol stays on the left in every locale, with the other marks.
<script setup lang="ts">
const visitors = ref<number | null>(1234567)
const umsatz = ref<string | null>('1234.5')
</script>
<template>
<div class="grid w-full max-w-xs gap-3">
<UFormField label="Visitors">
<ZNumberInput v-model="visitors" class="w-full" />
</UFormField>
<UFormField label="Umsatz (de, EUR)">
<ZNumberInput v-model="umsatz" unit="money" currency="EUR" locale="de" class="w-full" />
</UFormField>
</div>
</template>Decimal places and range
min-fraction-digits is written on commit (blur, Enter or Tab) and max-fraction-digits is the most that can be typed and what a commit rounds to, half away from zero. Money defaults to two places; the other units keep what is typed. A commit also keeps the value within min and max: 12 in a field that stops at 10 becomes 10. Until then the field is :invalid, so a form won't submit it. A minus can't be typed where min is 0 or more.
<script setup lang="ts">
const rate = ref<string | null>('0.125')
const price = ref<number | null>(1200)
const rating = ref<number | null>(9)
</script>
<template>
<div class="grid w-full max-w-xs gap-3">
<UFormField label="Rate, 2 to 3 places">
<ZNumberInput v-model="rate" unit="percent" :min-fraction-digits="2" :max-fraction-digits="3" class="w-full" />
</UFormField>
<UFormField label="Price, whole dollars">
<ZNumberInput v-model="price" unit="money" :max-fraction-digits="0" class="w-full" />
</UFormField>
<UFormField label="Rating, 0 to 10" help="Type 12 and press Enter.">
<ZNumberInput v-model="rating" :min="0" :max="10" class="w-full" />
</UFormField>
</div>
</template>Marks, spinning and selection
prefix and suffix add text beside the unit marks, outside the value, so typing never meets them. :spin="false" removes the chevrons and makes ArrowUp and ArrowDown do nothing, for a figure that isn't a quantity. highlight-on-focus selects the whole value when the field is focused, by click or by key.
<script setup lang="ts">
const rent = ref<number | null>(1200)
const invoice = ref<number | null>(10432)
const quantity = ref<number | null>(42)
</script>
<template>
<div class="grid w-full max-w-xs gap-3">
<UFormField label="Rent, about">
<ZNumberInput v-model="rent" unit="money" prefix="~" suffix="/mo" class="w-full" />
</UFormField>
<UFormField label="Invoice number">
<ZNumberInput v-model="invoice" :spin="false" class="w-full" />
</UFormField>
<UFormField label="Quantity">
<ZNumberInput v-model="quantity" highlight-on-focus class="w-full" />
</UFormField>
</div>
</template>Calc
With calc, the field takes + - * / with the usual precedence, worked out in decimal arithmetic by a small parser (never eval). Each number is grouped as you type, and the result shows under the field while it has the focus, without moving anything. Enter, Tab or blur puts the result in, rounded to the field's places and kept in range. Input that isn't a whole expression (678+) or divides by zero gives no value. The value is selected on focus, and an operator typed over the selection is added at the end, so +50 adds to what is there; a minus over everything starts a negative number instead.
A grid cell editor that commits itself sets :commit-on-blur="false" and calls the exposed evaluate(), which works the expression out, hands the value back, and returns it.
<script setup lang="ts">
const expenses = ref<number | null>(1200)
const cell = ref<number | null>(250)
const editor = useTemplateRef<{ evaluate: () => unknown }>('editor')
</script>
<template>
<div class="grid w-full max-w-xs gap-3">
<UFormField label="Expenses" help="Type +50*2 and press Enter.">
<ZNumberInput v-model="expenses" unit="money" calc class="w-full" />
</UFormField>
<UFormField label="Grid cell" :help="`Holds ${cell ?? 'nothing'} until evaluated.`">
<div class="flex gap-2">
<ZNumberInput ref="editor" v-model="cell" unit="money" calc :spin="false" :commit-on-blur="false" class="w-full" />
<UButton variant="soft" @click="editor?.evaluate()">Evaluate</UButton>
</div>
</UFormField>
</div>
</template>Decimal models
A Decimal from decimal.js, such as a Postgres numeric column from a database client that returns decimal.js values, is handed back as a Decimal of the caller's own class: typing, nudging, rounding and calc all do it, and no value passes through a float. A model that starts null has no class to take, so model-type="decimal" says what to hand back. Keep the product's decimal.js range compatible with the layer's (^10.6.0), so one copy is installed.
<script setup lang="ts">
import Decimal from 'decimal.js'
// shallowRef: a Decimal is a value, not state to make deeply reactive
const balance = shallowRef<Decimal | null>(new Decimal('1234.5'))
const tax = shallowRef<Decimal | null>(null)
</script>
<template>
<div class="grid w-full max-w-xs gap-3">
<UFormField label="Balance due" :help="`${balance instanceof Decimal ? 'Decimal' : typeof balance} ${balance ?? ''}`">
<ZNumberInput v-model="balance" unit="money" class="w-full" />
</UFormField>
<UFormField label="Tax, starts empty" :help="`${tax instanceof Decimal ? 'Decimal' : 'null'} ${tax ?? ''}`">
<ZNumberInput v-model="tax" unit="money" model-type="decimal" class="w-full" />
</UFormField>
</div>
</template>API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| modelValuerequired | T | — | — |
| calc | boolean | — | Take `+ - * /` expressions, worked out on commit. |
| commitOnBlur | boolean | true | Commit (clamp, round, work out an expression) on blur; `false` for a grid editor that calls `evaluate()`. |
| currency | string | — | ISO 4217 code for `unit="money"` (default: app.config, then USD). |
| freeform | boolean | — | — |
| highlightOnFocus | boolean | undefined | Select the whole value on focus (default: on in calc). |
| locale | string | — | BCP 47 locale for the grouping and decimal marks, the currency symbol and the chevrons' labels (default: app.config, then en-US). |
| max | string | number | — | — |
| maxFractionDigits | number | — | Decimal places rounded to on commit, and the most that can be typed (default: app.config, then 2 for money, none otherwise). |
| min | string | number | — | — |
| minFractionDigits | number | — | Decimal places written on commit (default: app.config, then 2 for money, 0 otherwise). |
| modelType | ModelKind | — | The kind to hand back when the model starts empty: a `Decimal` v-model that can be null needs `decimal`. |
| prefix | string | — | Text before the value, beside the currency mark. |
| spin | boolean | true | The chevrons and ArrowUp/ArrowDown; `false` takes both away. |
| step | number | — | — |
| suffix | string | — | Text after the value, beside the unit mark. |
| ui | Record<string, string> | — | — |
| unit | NumberUnit | — | — |
Emits
| Event | Payload | Description |
|---|---|---|
| update:modelValue | [value: T] | — |