Number Input
The number input component provides a structured way for users to input numerical data with built-in validation and controls. It combines a text input field with stepper buttons and can include unit selection functionality for measurements, currency, or other quantifiable values.
import {NumberInputModule} from "@qualcomm-ui/angular/number-input"Overview
- This component implements the ARIA Spinbutton pattern. The
HomeandEndkeys work differently than a text input:Home: If the spinbutton has a minimum value, sets the value to its minimum.End: If the spinbutton has a maximum value, sets the value to its maximum.
- Template-driven and Reactive form values are supplied as numbers. When the field is cleared, the value is set to
NaN. You must account for this in your application logic.
Examples
Simple
The simple API bundles all subcomponents together into a single directive.
<q-number-input class="w-72" label="Label" placeholder="Enter a number" />
Child Directives
Provide child directives to customize specific elements while keeping the simple API's default structure.
<q-number-input class="w-72" placeholder="Enter a number">
<label q-number-input-label>Label</label>
</q-number-input>
Composite
Build with the composite API for granular control. This API requires you to provide each subcomponent, but gives you full control over the structure and layout.
<div class="w-72" q-number-input-root>
<label q-number-input-label>Label</label>
<div q-number-input-input-group>
<input placeholder="Enter a number" q-number-input-input />
<div q-number-input-control></div>
<span q-number-input-error-indicator></span>
</div>
<div q-number-input-error-text>Error</div>
</div>
Min and Max values
Pass the min and max props to the root component to set the minimum and maximum values of the number input.
If the value entered is less than min or greater than max, it will be clamped to the nearest boundary on blur or enter.
<q-number-input
aria-label="Number input with min and max"
class="w-72"
defaultValue="7"
max="10"
min="5"
/>
Step interval
Pass the step prop to the root component to set the increment or decrement step interval.
<q-number-input
aria-label="Number input with step"
class="w-72"
placeholder="Enter a number"
step="3"
/>
States
The following shows how the component appears in each interactive state.
<q-number-input disabled label="Disabled" placeholder="Disabled" />
<q-number-input label="Read only" placeholder="Read only" readOnly />
<q-number-input
errorText="Invalid"
invalid
label="Invalid"
placeholder="Invalid"
/>
Sizes
Customize size using the size prop. The default size is md.
<q-number-input
aria-label="Number input with size sm"
class="w-56"
placeholder="sm"
size="sm"
startIcon="Sigma"
/>
<q-number-input
aria-label="Number input with size md"
class="w-64"
placeholder="md"
size="md"
startIcon="Sigma"
/>
<q-number-input
aria-label="Number input with size lg"
class="w-72"
placeholder="lg"
size="lg"
startIcon="Sigma"
/>
Unit Selector
Add a unit selector to the number input by passing an array of options to the unitOptions prop.
The selected unit follows our controlled state pattern: use defaultUnit for uncontrolled state, or unit with (unitChanged) for controlled state.
<q-number-input
class="w-72"
defaultUnit="USD"
label="Price"
placeholder="0.00"
[unitOptions]="unitOptions"
/>
Error Text and Indicator
Error messages are displayed using two props:
The error text and indicator will only render when invalid is true.
import {Component, computed, signal} from "@angular/core"
import {FormsModule} from "@angular/forms"
import {NumberInputModule} from "@qualcomm-ui/angular/number-input"
@Component({
imports: [NumberInputModule, FormsModule],
selector: "number-input-error-text-demo",
template: `
<q-number-input
class="w-72"
defaultValue="0"
errorText="Value must be greater than 0"
label="Label"
placeholder="Enter a value"
[invalid]="isInvalid()"
/>
`,
})
export class NumberInputErrorTextDemo {
readonly value = signal<number>(0)
readonly isInvalid = computed(() => {
const value = this.value()
return isNaN(value) || value <= 0
})
}Forms
Template Forms
When using template-driven forms with ngModel, perform validation manually in your component and pass the result to the invalid property.
TIP
Form validation timing is controlled by the updateOn property on the form control.
<q-number-input
class="w-72"
errorText="Value must be greater than 0"
label="Label"
placeholder="Enter a value"
[invalid]="isInvalid()"
[(ngModel)]="value"
/>
Required
When using template forms, pass the required property to apply a validator to the form control.
<q-number-input
#textInput
class="w-72"
label="Required"
placeholder="Enter a value"
required
[(ngModel)]="value"
/>
Reactive Forms
Use Reactive forms for better control of form state and validation.
import {Component, inject} from "@angular/core"
import {
type AbstractControl,
FormBuilder,
ReactiveFormsModule,
type ValidationErrors,
Validators,
} from "@angular/forms"
import {requiredNumberValidator} from "@qualcomm-ui/angular-core/number-input"
import {ErrorTextComponent} from "@qualcomm-ui/angular/input"
import {NumberInputModule} from "@qualcomm-ui/angular/number-input"
function aspectRatioValidator(
control: AbstractControl,
): ValidationErrors | null {
const width = control.get("width")?.value
const height = control.get("height")?.value
const ratio = width / height
return ratio >= 0.25 && ratio <= 4 ? null : {invalidAspectRatio: true}
}
@Component({
imports: [NumberInputModule, ReactiveFormsModule, ErrorTextComponent],
selector: "number-input-reactive-forms-demo",
template: `
<form class="flex flex-col gap-3" [formGroup]="aspectRatioFormGroup">
<div class="flex flex-wrap items-start gap-2">
<q-number-input
class="w-56"
errorText="Width must be between 1 and 4096"
formControlName="width"
label="Width"
step="0.01"
/>
<q-number-input
class="w-56"
errorText="Height must be between 1 and 4096"
formControlName="height"
label="Height"
step="0.01"
/>
</div>
@if (aspectRatioFormGroup.hasError("invalidAspectRatio")) {
<div q-error-text>
Invalid aspect ratio: {{ aspectRatio() }} (must be between 0.25 and 4)
</div>
}
</form>
`,
})
export class NumberInputReactiveFormsDemo {
protected readonly fb = inject(FormBuilder)
aspectRatioFormGroup = this.fb.group(
{
height: [
600,
[Validators.min(1), Validators.max(4096), requiredNumberValidator],
],
width: [
800,
[Validators.min(1), Validators.max(4096), requiredNumberValidator],
],
},
{validators: [Validators.required, aspectRatioValidator]},
)
aspectRatio() {
const height = this.aspectRatioFormGroup.get("height")?.value || 1
const width = this.aspectRatioFormGroup.get("width")?.value || 0
return (width / height).toFixed(5)
}
}State Guidelines
The disabled, invalid, and required properties have no effect when using Reactive Forms. Use the equivalent Reactive Form bindings instead:
disabledField = new FormControl(5)
invalidField = new FormControl(0, {
validators: [Validators.min(1)],
})
requiredField = new FormControl(5, {validators: [requiredNumberValidator]})
ngOnInit() {
this.disabledField.disable()
this.invalidField.markAsDirty()
}
Composite API & Forms
When using the composite API with Reactive Forms, always apply form control bindings to the q-number-input-root directive.
import {Component, computed, signal} from "@angular/core"
import {FormsModule} from "@angular/forms"
import {NumberInputModule} from "@qualcomm-ui/angular/number-input"
@Component({
imports: [NumberInputModule, FormsModule],
selector: "number-input-composite-forms-demo",
template: `
<div
class="w-72"
q-number-input-root
[invalid]="isInvalid()"
[(ngModel)]="value"
>
<label q-number-input-label>Composite Forms</label>
<div q-number-input-input-group>
<input placeholder="Enter a value" q-number-input-input />
<div q-number-input-control></div>
<span q-number-input-error-indicator></span>
</div>
<div q-number-input-error-text>Value must be greater than 0</div>
</div>
`,
})
export class NumberInputCompositeFormsDemo {
readonly value = signal<number>(0)
readonly isInvalid = computed(() => {
return isNaN(this.value()) || this.value() <= 0
})
}Explorer
Component Anatomy
Hover to highlight, click to view API
API
<q-number-input>
The <q-number-input> component extends the q-number-input-root directive with the following properties:
stringstringstring<q-number-input>
<div q-number-input-error-text>...</div>
</q-number-input>
string<q-number-input>
<div q-number-input-hint>...</div>
</q-number-input>
string<q-number-input>
<div q-number-input-label>...</div>
</q-number-input>
stringComposite API
q-number-input-root
booleanbooleanbooleanstringnumber'ltr' | 'rtl'
boolean| string
| LucideIconData
<div q-number-input-input-group>
<div q-input-end-icon [icon]="..."></div>
</div>
booleanNumberFormatOptionsIntl.NumberFormat constructor() =>
| Node
| ShadowRoot
| Document
| 'text'
| 'tel'
| 'numeric'
| 'decimal'
booleanstringnumbernumberstringstringbooleanboolean| 'sm'
| 'md'
| 'lg'
boolean| string
| LucideIconData
<div q-number-input-input-group>
<div q-input-start-icon [icon]="..."></div>
</div>
number{
decrementLabel?: string
incrementLabel?: string
valueText?: (
value: string,
) => string
}
stringArray<{
displayText?: string
label: string
value: string
}>
string{
value: string
valueAsNumber: number
}
{
reason:
| 'rangeUnderflow'
| 'rangeOverflow'
value: string
valueAsNumber: number
}
class'qui-input__root'data-disableddata-focusdata-invaliddata-number-input-part'root'data-size| 'sm'
| 'md'
| 'lg'
q-number-input-input-group
NumberInputInputGroupComponentclass'qui-input__input-group'data-disableddata-focusdata-invaliddata-number-input-part'input-group'data-readonlydata-size| 'sm'
| 'md'
| 'lg'
q-number-input-label
NumberInputLabelComponentclass'qui-input__label'data-disableddata-focusdata-invaliddata-number-input-part'label'data-size| 'sm'
| 'md'
| 'lg'
q-number-input-hint
stringclass'qui-input__hint'data-disableddata-number-input-part'hint'hiddenbooleanq-number-input-control
class'qui-number-input__control'data-disableddata-focusdata-invaliddata-number-input-part'control'data-size| 'sm'
| 'md'
| 'lg'
q-number-input-decrement-trigger
NumberInputDecrementTriggerComponentclass'qui-number-input__step-trigger'data-disableddata-number-input-part'decrement-trigger'data-size| 'sm'
| 'md'
| 'lg'
tabIndex-1
q-number-input-increment-trigger
NumberInputIncrementTriggerComponentclass'qui-number-input__step-trigger'data-disableddata-number-input-part'increment-trigger'data-size| 'sm'
| 'md'
| 'lg'
tabIndex-1
q-number-input-input
stringstringstringclass'qui-input__input'data-disableddata-emptydata-invaliddata-number-input-part'input'data-size| 'sm'
| 'md'
| 'lg'
q-number-input-error-text
stringclass'qui-input__error-text'data-number-input-part'error-text'hiddenbooleanq-number-input-error-indicator
NumberInputErrorIndicatorComponentclass'qui-input__error-indicator'data-number-input-part'error-indicator'data-size| 'sm'
| 'md'
| 'lg'
hiddenbooleanq-number-input-unit-select
{
x: number
y: number
}
booleanbooleanstringboolean'ltr' | 'rtl'
() =>
| Node
| ShadowRoot
| Document
stringstringbooleanbooleanbooleanbooleanPositioningOptionsbooleanbooleanbooleanbooleanKeyboardEventvoidCustomEvent<{
event?: E
}>
string| CustomEvent<{event?: E}>
| CustomEvent<{event?: E}>
{
href: string
node: HTMLAnchorElement
value: string
}
booleanCustomEvent<{
event?: E
}>
CustomEvent<{
originalIndex: number
originalLayer: HTMLElement
targetIndex: number
targetLayer: HTMLElement
}>
stringclass'qui-number-input__unit-select'data-disableddata-number-input-part'unit-select'data-readonlydata-size| 'sm'
| 'md'
| 'lg'
Data Structures
UnitOption
Each unit option object accepts the following properties:
stringstringstring