npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ngx-numeric-range-form-field

v5.1.0

Published

Angular Signal Forms numeric range input — two number fields, one composite value, custom-styled and theme-friendly.

Readme

ngx-numeric-range-form-field

A reactive Angular custom form control for a composite numeric range — two number inputs (minimum, maximum) exposed as a single value. Built on Angular 21 Signal Forms (FormValueControl) with no ControlValueAccessor, no Angular Material, no third-party runtime dependencies.

ngx-numeric-range-form-field

CI Coverage Status code style: prettier

Live demo · Changelog

Features

  • Two-input composite numeric range rendered as one field
  • Plug & play with Angular Signal Forms via FormValueControl ([formField])
  • Ships four composable validator helpers — numericRangeOrderValid, numericRangeBounds, numericRangeBothFilled, numericRangeWidth
  • Typed NumericRangeErrorKind contract for error-key matching
  • Schema-driven validation (required, readonly, disabled, validate, …) owned by the consumer's form() definition
  • Accessible by default — visible label wires to the group via aria-labelledby; per-input announcements compose <group> <side>
  • Native numeric-input attributes pass-through (step, autocomplete, per-side name / min / max)
  • Custom outlined field styling — reskin via CSS custom properties without ::ng-deep
  • Tree-shakable (sideEffects: false)
  • Zero runtime dependencies — no Angular Material, no CDK

@angular/forms/signals is marked @experimental 21.0.0. Consumers of [email protected] adopt the same experimental surface.

At a glance

| | | | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Emptyempty | Filledfilled | | Read-onlyreadonly | Invalid orderinvalid |

Install

npm install ngx-numeric-range-form-field

Peer dependencies: @angular/common, @angular/core, @angular/forms (all >=21.0.0 <22.0.0).

Usage

import { Component, signal } from '@angular/core'
import { form, FormField, required } from '@angular/forms/signals'
import {
  INumericRange,
  NumericRangeFormFieldComponent,
  numericRangeOrderValid
} from 'ngx-numeric-range-form-field'

@Component({
  selector: 'app-range-demo',
  standalone: true,
  imports: [NumericRangeFormFieldComponent, FormField],
  template: `
    <ngx-numeric-range-form-field
      [formField]="rangeForm"
      label="Pick a range"
    />
  `
})
export class RangeDemoComponent {
  readonly rangeValue = signal<INumericRange | null>({
    minimum: 10,
    maximum: 50
  })

  readonly rangeForm = form<INumericRange | null>(this.rangeValue, p => {
    required(p)
    numericRangeOrderValid(p)
  })
}

The emitted value type:

type INumericRange = {
  minimum: number | null
  maximum: number | null
}

Either side may be null to represent a half-filled range. When both sides are null, the value itself becomes null.

Inputs

All inputs are signal inputs. Inputs marked (schema-driven) are automatically written by the FormField directive from the attached form() schema — bind them directly only when using the component without [formField].

| Input | Type | Default | Description | | -------------------------- | -------------------------------------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | label | string | '' | Visible label rendered above the field. When set, becomes the group's aria-labelledby target. | | minPlaceholder | string | 'From' | Placeholder for the minimum input. | | maxPlaceholder | string | 'To' | Placeholder for the maximum input. | | minLabel | string \| null | null | Override the minimum input's accessible name. Defaults to minPlaceholder when null. | | maxLabel | string \| null | null | Override the maximum input's accessible name. Defaults to maxPlaceholder when null. | | resetLabel | string | 'Reset range' | aria-label for the reset (✕) button. | | resettable | boolean | true | Show the reset (✕) button when a value is present. | | minReadonly | boolean | false | Make the minimum input read-only while the maximum remains editable. | | maxReadonly | boolean | false | Mirror of minReadonly for the maximum input. | | step | number \| string \| null | null | Native step attribute forwarded to both inputs (e.g. 0.5). | | autocomplete | string \| null | null | Native autocomplete attribute forwarded to both inputs (e.g. 'off'). | | minName / maxName | string \| null | null | Per-side native name attribute — useful inside a native <form>. | | minMin / minMax | number \| string \| null | null | Per-side native HTML min / max attributes for the minimum input. Steers the browser spinner only — schema validation remains the source of truth. | | maxMin / maxMax | number \| string \| null | null | Per-side native HTML min / max for the maximum input. | | value (schema-driven) | INumericRange \| null | null | Composite value. Two-way via [(value)] or through form(). | | disabled (schema-driven) | boolean | false | Disable both inputs. | | readonly (schema-driven) | boolean | false | Render both inputs read-only. | | required (schema-driven) | boolean | false | Show the required marker on the label. Visual flag — the actual validity comes from your form() schema. | | touched (schema-driven) | boolean | false | Marks the field as touched — flips the invalid styling on when paired with errors. | | errors (schema-driven) | readonly ValidationError.WithOptionalFieldTree[] | [] | Error list. Non-empty + touched paints the field red. |

Schema validators

The lib ships four helpers that compose into any form() schema. Unless noted otherwise they treat a null on either side as "not yet set" and pass in that case — pair them with required(p) or numericRangeBothFilled(p) when "half-filled" should be rejected.

numericRangeOrderValid(path)

Fails with { kind: 'invalidRange' } when maximum < minimum.

invalid range

import { numericRangeOrderValid } from 'ngx-numeric-range-form-field'

rangeForm = form<INumericRange | null>(this.rangeValue, p => {
  numericRangeOrderValid(p)
})

numericRangeBounds(path, { min, max })

Keeps both sides within consumer-supplied bounds. Emits { kind: 'min', message: 'Minimum must be at least …' } when a side is below the floor and { kind: 'max', message: 'Maximum must not exceed …' } when a side is above the ceiling. Pass min or max alone for one-sided bounds.

bounds error

import { numericRangeBounds } from 'ngx-numeric-range-form-field'

rangeForm = form<INumericRange | null>(this.rangeValue, p => {
  numericRangeBounds(p, { min: 1, max: 10 })
})

numericRangeBothFilled(path)

Fails with { kind: 'incomplete' } until both sides are populated. required(p) alone only checks that the composite value is not null, so { minimum: 5, maximum: null } passes it — use this helper when you need the stronger guarantee.

import { numericRangeBothFilled } from 'ngx-numeric-range-form-field'

rangeForm = form<INumericRange | null>(this.rangeValue, p => {
  numericRangeBothFilled(p)
})

numericRangeWidth(path, { min, max })

Constrains the span of the range (maximum - minimum), not the endpoints. Emits { kind: 'minWidth' } when the span is below bounds.min and { kind: 'maxWidth' } when it exceeds bounds.max; both carry a readable message. Skipped while either side is null or the range is mis-ordered (let numericRangeOrderValid own that case).

import { numericRangeWidth } from 'ngx-numeric-range-form-field'

rangeForm = form<INumericRange | null>(this.rangeValue, p => {
  numericRangeWidth(p, { min: 5, max: 30 })
})

Reading errors in a template:

@for (err of rangeForm().errors(); track $index) {
<p class="error">{{ err.message || err.kind }}</p>
}

Typed error-kind contract

Filter errors by typed constants instead of duplicating string literals:

import { NumericRangeErrorKind } from 'ngx-numeric-range-form-field'

const hasOrderError = rangeForm()
  .errors()
  .some(e => e.kind === NumericRangeErrorKind.OutOfOrder)

| Constant | Emitted by | kind value | | ---------------------------------- | ---------------------------------- | ---------------- | | NumericRangeErrorKind.OutOfOrder | numericRangeOrderValid | 'invalidRange' | | NumericRangeErrorKind.BoundsMin | numericRangeBounds (lower bound) | 'min' | | NumericRangeErrorKind.BoundsMax | numericRangeBounds (upper bound) | 'max' | | NumericRangeErrorKind.Incomplete | numericRangeBothFilled | 'incomplete' | | NumericRangeErrorKind.WidthMin | numericRangeWidth (lower span) | 'minWidth' | | NumericRangeErrorKind.WidthMax | numericRangeWidth (upper span) | 'maxWidth' |

The string values match the kinds the validators emit today, so existing comparisons against the raw strings keep working.

Accessibility

When label is set the component wires the visible label to the role="group" via aria-labelledby using stable per-instance IDs, and each input's aria-label composes <group label> <side label> — a screen reader announces e.g. "Price range From" and "Price range To" instead of just "From" / "To". Override the per-side announcement with minLabel / maxLabel, or the reset button announcement with resetLabel.

Styling

The component ships a minimal outlined field. Override these custom properties on the host (or anywhere in the cascade) to restyle:

| Property | Default | | ------------------------------- | --------------------- | | --ngx-nrff-font-family | inherit | | --ngx-nrff-font-size | 0.95rem | | --ngx-nrff-label-font-size | 0.8rem | | --ngx-nrff-label-color | rgba(0, 0, 0, 0.6) | | --ngx-nrff-text-color | rgba(0, 0, 0, 0.87) | | --ngx-nrff-placeholder-color | rgba(0, 0, 0, 0.4) | | --ngx-nrff-border-color | rgba(0, 0, 0, 0.23) | | --ngx-nrff-border-hover-color | rgba(0, 0, 0, 0.52) | | --ngx-nrff-focus-color | #1976d2 | | --ngx-nrff-error-color | #b3261e | | --ngx-nrff-background | transparent | | --ngx-nrff-disabled-color | rgba(0, 0, 0, 0.38) | | --ngx-nrff-radius | 6px | | --ngx-nrff-padding-y | 10px | | --ngx-nrff-padding-x | 12px | | --ngx-nrff-gap | 8px |

License

MIT License — Copyright (c) 2022-2026 Dino Klicek