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

ng-hub-ui-forms

v22.17.1

Published

Accessible, signal-based form fields for Angular (input, textarea, slider, segmented, select, datepicker, file input with drag & drop and upload progress) with automatic error display for controls, FormGroups and FormArrays. Reactive Forms today, Signal F

Readme

ng-hub-ui-forms

Español | English

Accessible, signal-based form fields for Angular — input, textarea, slider, select and datepicker — with automatic validation-error display for controls, FormGroups and FormArrays. Reactive Forms today, Signal Forms ready. Themed entirely through --hub-* CSS variables, no Bootstrap required.

Documentation and Live Examples

This package is part of Hub UI, a collection of Angular component libraries for standalone apps.

  • Docs: https://hubui.dev/en/forms/overview/
  • Live examples: https://hubui.dev/en/forms/examples/
  • Hub UI: https://hubui.dev/en/

🧩 Library Family ng-hub-ui

This library is part of the ng-hub-ui ecosystem:


🚀 Quick Start

1. Install

npm install ng-hub-ui-forms

@angular/cdk is a peer dependency (used by the datepicker overlay and the select):

npm install @angular/cdk

2. Import

The fields are standalone — import only what you use:

import { HubInputComponent, HubSelectComponent } from 'ng-hub-ui-forms';

3. Use

<form [formGroup]="form" hubForm (submit)="save()">
	<hub-input formControlName="email" type="email" label="Email" required />
	<hub-select formControlName="country" label="Country" [items]="countries" bindLabel="name" bindValue="code" />
	<button type="submit">Save</button>
</form>

The required email field reveals its error automatically on submit — no manual @if (control.invalid && control.touched) wiring.


📦 Description

ng-hub-ui-forms unifies a set of accessible form fields behind one contract: bind them with Reactive Forms and the matching validation errors appear automatically at the control, group and form level. Fields are standalone, OnPush and signal-native; the select is a maintained fork of ng-select (see Credits); the datepicker is built from scratch on native Date and the Angular CDK overlay. Everything is themed through canonical --hub-* CSS variables with runtime dark mode — no Bootstrap dependency.

🎯 Features

  • Fieldshub-input (text/number/email/password/color/switch/checkbox/counter, with input-group addons & masks, projected in-field affixes, a built-in clearable button and debounced typeahead search; the file format is deprecated → use hub-file-input), hub-otp-input, hub-textarea (+ hubAutoresize), hub-slider (single / dual thumb, gradient fill), hub-segmented (segmented control field — single & multiple selection, horizontal & vertical, with label + validation), hub-select (dropdown format, grouping, client-side search via searchable and server-side async typeahead via a typeahead Subject, tag creation with addTag, custom templates, prepend / append group addons and attached icons/buttons via hubPrepend / hubAppend; the buttons / checkbox / radio formats are deprecated → use hub-segmented), hub-datepicker (single & range at any granularity from a year to a second, time picking, min/max down to the minute, keyboard nav, i18n), hub-file-input (drag & drop, clipboard paste, type/size limits, previews, optional upload progress).
  • Automatic error display — bind a field and its control errors render below it; hub-fieldset, form[hubForm] and hub-legend surface group- and form-level (cross-field) errors the same way, with zero wiring.
  • Containershub-fieldset / form[hubForm] group fields and show their group errors; hub-legend renders an accessible legend.
  • ConfigurableprovideHubForms({ … }) sets the invalid-feedback templates, datepicker locale/labels, file-input labels and more, app-wide or per instance.
  • Validators & helpershubAreEqual cross-field validator, the file validators (hubAcceptedFiles, hubMaxFileSize, hubMinFileSize, hubMaxTotalSize, hubMaxFiles, hubMinFiles), hubValidationError / hubFormText projection directives, and a set of utility pipes.
  • Signal Forms ready — an opt-in ng-hub-ui-forms/signals secondary entry point integrates Angular Signal Forms; the core stays Reactive-Forms-based and Angular-21-safe.
  • Theming — every colour, border, radius and spacing is a --hub-* CSS custom property; ships shared SCSS tokens for consumers.
  • Cross-library adapterhubFormControlAdapter lets other libraries render hub-input / hub-select on demand without hard-depending on this package (see below).

🔌 Cross-library adapter (hubFormControlAdapter)

Other ng-hub-ui libraries can host the forms controls without taking a hard dependency on ng-hub-ui-forms. They expose an optional token; you wire the ready-made hubFormControlAdapter once and their primitive controls upgrade to hub-input / hub-select. For example, the ng-hub-ui-paginable table:

import { provideHubPaginableFormControls } from 'ng-hub-ui-paginable';
import { hubFormControlAdapter } from 'ng-hub-ui-forms';

export const appConfig: ApplicationConfig = {
  providers: [provideHubPaginableFormControls(hubFormControlAdapter)]
};

The adapter creates components dynamically and bridges value-in / change-out; it needs provideHubForms() or the default config in the environment. See the ecosystem-wide Synergies & agnosticism section.


📦 Installation

npm install ng-hub-ui-forms @angular/cdk

Peer Dependencies

{
	"@angular/cdk": ">=21.0.0",
	"@angular/common": ">=21.0.0",
	"@angular/core": ">=21.0.0",
	"@angular/forms": ">=21.0.0",
	"@angular/platform-browser": ">=21.0.0"
}

⚙️ Usage

Input

<hub-input formControlName="email" type="email" label="Email" required />
<hub-input formControlName="amount" type="number" label="Amount" />
<hub-input formControlName="darkMode" format="switch" label="Dark mode" />

Icon affix & typeahead (search boxes)

Project a leading / trailing icon inside the field, emit a debounced term on every keystroke, and let the field render its own clear button:

<!-- Project any icon (any pack via the shorthand) + debounced search + built-in clear -->
<hub-input
	label="Search frameworks"
	[clearable]="true"
	[debounceTime]="300"
	(search)="onSearch($event)"
>
	<hub-icon hubInputPrefix name="fa:solid:magnifying-glass" />
</hub-input>

<!-- Projected affixes for any content (a unit label, an inline SVG…) -->
<hub-input label="Amount">
	<span hubInputPrefix>€</span>
</hub-input>
  • Project a <hub-icon> (or any element) into hubInputPrefix / hubInputSuffix; the field themes it with its --hub-input-icon-* tokens.
  • [clearable] renders an internal ✕ button once the field holds a value — it resets the control and emits an empty search term (no manual suffix wiring). The glyph is the swappable --hub-input-clear-icon token.
  • The control reserves inline padding automatically so its text never sits under the affix.
  • Affixes use logical CSS properties, so start/end follow the writing direction and flip automatically under dir="rtl".
  • (search) fires debounceTime ms after typing stops (0 = every keystroke); identical consecutive terms are skipped, and valueChange stays synchronous.

Password fields

type="password" renders a masked field with an integrated reveal toggle inside the input group (a trailing addon, not a detached button):

<hub-input
	formControlName="password"
	type="password"
	label="Password"
	autocomplete="new-password"
	passwordStrength
/>
  • [(passwordRevealed)] — two-way model for the reveal state; drive it externally or read it.
  • passwordToggle (default true) — set to false to hide the integrated toggle button.
  • hideOnBlur (default true) — a revealed password re-masks automatically once focus leaves the field.
  • capsLockWarning (default true) — shows a hint under the field while Caps Lock is active.
  • passwordStrength (default false) — opt-in 4-segment strength meter, scored by the exported scorePasswordStrength heuristic (length ≥ 8, mixed case, digit, symbol) unless overridden.
  • autocomplete — native attribute for password managers, e.g. current-password / new-password.
  • Readonly password fields stay masked (they no longer force type="text"); an explicit toggle click can still reveal them.

Labels and the strength scorer are localizable app-wide via provideHubForms:

provideHubForms({
	password: {
		showPasswordLabel: 'Show password',
		hidePasswordLabel: 'Hide password',
		capsLockWarning: 'Caps Lock is on',
		strengthLabels: ['Weak', 'Fair', 'Good', 'Strong'],
		strengthFn: (value) => myCustomScorer(value) // optional, 0-4
	}
});

Select

<!-- object items -->
<hub-select formControlName="country" label="Country" [items]="countries" bindLabel="name" bindValue="code" />

<!-- multiple + client-side search (searchable filters the loaded items as you type) -->
<hub-select formControlName="tags" label="Tags" [items]="tags" [multiple]="true" [searchable]="true" />

<!-- grouped -->
<hub-select formControlName="city" label="City" [items]="cities" bindLabel="name" bindValue="id" groupBy="country" />

Addons and attached content

prepend / append are group addons carrying text — a currency, a unit, a protocol. Available on hub-input, hub-select, hub-textarea and hub-datepicker: every field that renders as a box with a value.

<hub-input formControlName="amount" label="Amount" prepend="€" append=".00" />
<hub-textarea formControlName="notes" label="Notes" append="Markdown" />

For an icon or a button — anything richer than text — project a [hubPrepend] / [hubAppend] template. Both compose: the strings render first, so projected content is always outermost on its side. A unit labels the field; the action sits beyond it.

<hub-input formControlName="query" label="Search">
	<ng-template hubAppend>
		<button type="button" aria-label="Run the search" (click)="search()">
			<hub-icon name="fa:solid:magnifying-glass" />
		</button>
	</ng-template>
</hub-input>

Whatever is projected wears the field's border, radius and height rather than its own, so a button does not draw a second, thicker seam beside the control.

Import HubPrependDirective / HubAppendDirective from ng-hub-ui-forms. [hubSelectSuffix] is deprecated in favour of [hubAppend], which does the same on every field rather than only on the select.

Datepicker

<hub-datepicker formControlName="date" label="Date" />
<hub-datepicker formControlName="range" mode="range" label="Stay" />

granularity sets how precise each picked point is, and selects the panel with it. It is orthogonal to mode: mode says how many points are picked, granularity how precise each one is.

<!-- a validity window: each endpoint carries its own time -->
<hub-datepicker formControlName="window" mode="range" granularity="minute" [minuteStep]="15" />

<!-- coarse units get a 12-cell period grid instead of the calendar -->
<hub-datepicker formControlName="billingPeriod" granularity="month" />

| granularity | Panel | Value (default valueFormat) | | --- | --- | --- | | year | Decade grid | "2026" | | month | 12-month grid | "2026-09" | | day (default) | Calendar | "2026-09-01" | | hour / minute / second | Calendar + time strip | "2026-09-01T09:30:00+02:00" |

The value's timezone. At day and coarser it is a bare calendar date with no zone attached, exactly as before. From hour onwards it is a full ISO 8601 timestamp carrying the reader's local wall clock and the offset of that very date+02:00 in Madrid in September, +01:00 for the same clock in January. It denotes an unambiguous instant; convert with new Date(value).toISOString() if you need UTC.

min and max honour the time too: a day is disabled only when no instant of it is allowed, so min="2026-09-01T14:00" leaves 1 September clickable and the time controls refuse the earlier hours.

Three independent axes control the formats:

<!-- what the control holds: 'iso' (default) | 'date' | 'timestamp' | (date) => unknown -->
<hub-datepicker formControlName="due" valueFormat="date" />

<!-- what the user reads: Intl options | an Angular pattern | (date) => string -->
<hub-datepicker formControlName="due" displayFormat="dd/MM/yyyy HH:mm" />

<!-- how an incoming value is read; also applies to min/max -->
<hub-datepicker formControlName="due" [parse]="parseLegacyDate" />

ISO strings of any width, Date instances and epoch milliseconds are detected automatically, so parse is only needed for dialects outside that set.

File input

Drag & drop, clipboard paste, constraints and previews. The control value stays native — a File, a File[], or null — so it goes straight into a FormData.

<hub-file-input
	formControlName="attachments"
	label="Attachments"
	[multiple]="true"
	accept="image/*,.pdf"
	[maxSize]="5 * 1024 * 1024"
	[maxFiles]="3"
	preview="grid"
	(rejected)="notify($event)"
/>

accept, maxSize, maxFiles and friends filter: an offending file never reaches the value and surfaces through (rejected) with a typed reason. They are enforced by hand, because the native accept attribute only filters the operating-system dialog — a drop or a paste bypasses it. To make the control invalid as well (worth doing when a value can also be patched in programmatically), add the matching validators:

new FormControl<File[]>([], [hubMaxFiles(3), hubMaxFileSize(5 * 1024 * 1024), hubAcceptedFiles('image/*,.pdf')]);

Uploading is opt-in and transport-agnostic. Implement the contract in your application — the library never ships an endpoint — and the field renders per-file progress, cancel and retry:

@Injectable({ providedIn: 'root' })
export class ApiFileUploader implements HubFileUploader {
	readonly #http = inject(HttpClient);

	upload(file: File): Observable<HubFileUploadEvent> {
		const body = new FormData();
		body.append('file', file);

		return this.#http.post('/api/files', body, { reportProgress: true, observe: 'events' }).pipe(
			map((event) => {
				if (event.type === HttpEventType.UploadProgress) {
					// `total` is undefined when the size is unknown — pass null, not 0, so the bar
					// renders indeterminate instead of looking stalled.
					return { status: 'progress', loaded: event.loaded, total: event.total ?? null } as const;
				}
				if (event.type === HttpEventType.Response) {
					return { status: 'done', response: event.body } as const;
				}
				return null;
			}),
			filter((event) => event !== null),
			catchError((error) => of({ status: 'error', error } as const))
		);
	}
}

bootstrapApplication(App, { providers: [provideHttpClient(), provideHubFileUploader(ApiFileUploader)] });

The observable must be cold: one subscription is one request. cancel() unsubscribes, which is what aborts the underlying XMLHttpRequest. A shared or hot observable silently breaks cancellation. Bind a submit button to uploading() if you need to wait for the uploads: the control stays valid while they run, by design.

Whatever the uploader reports on done is kept on the item, so the ids the server minted are there when you submit the form:

const uploadedIds = fileInput.files().map((item) => (item.response as { id: string }).id);

Customize it without forking the template: the --hub-file-input-* tokens (every icon is a swappable CSS mask), the hub-file-input-theme(...) mixin, and three projection slots.

<hub-file-input formControlName="attachments" [multiple]="true">
	<ng-template hubFileIcon let-item>
		<hub-icon [name]="item.file.type === 'application/pdf' ? 'fa:solid:file-pdf' : 'fa:solid:file'" />
	</ng-template>
</hub-file-input>

Reproducing your own dropzone

The dropzone is built from a glyph, an invitation and a browse action, each themeable on its own — so a design system reproduces its own without forking the template.

  • Icon medallion--hub-file-input-icon-bg, -icon-chip-size and -icon-chip-radius put the glyph on a tinted, rounded surface. Transparent and square by default.
  • Browse as a button--hub-file-input-browse-bg, -hover-bg, -padding-x, -padding-y, -radius and an optional leading glyph (-browse-icon, -browse-icon-display, -browse-icon-size). A transparent underlined link by default.
  • Two invitation lines[dropText] and [dropSubtext] (or the dropHere / dropSubtext labels), stacked with --hub-file-input-prompt-direction: column. The second line is empty by default.
  • A leading noticehubFileDropzoneNotice projects markup inside the dropzone, between the glyph and the invitation, for something the invitation cannot say.
<hub-file-input dropText="Drop your documents here" dropSubtext="or click to browse" buttonLabel="Select files">
	<ng-template hubFileDropzoneNotice>
		<strong class="missing">{{ missingCount }} documents still missing</strong>
	</ng-template>
</hub-file-input>

Automatic errors at every level

<form [formGroup]="form" hubForm (submit)="save()">
	<hub-fieldset legend="Credentials">
		<hub-input formControlName="email" type="email" label="Email" required />
		<hub-input formControlName="confirm" type="email" label="Confirm email" required />
	</hub-fieldset>
	<button type="submit">Create account</button>
</form>
form = new FormGroup(
	{ email: new FormControl('', Validators.required), confirm: new FormControl('') },
	{ validators: hubAreEqual('email', 'confirm') }
);

On submit, each invalid field shows its error and the cross-field hubAreEqual error is surfaced by the fieldset/form — no manual error markup anywhere.

Validation states (invalid is automatic, valid is opt-in)

The invalid state is always automatic: a touched, invalid field shows its error styling and message with no configuration. The valid / success state is strictly opt-in — success is never shown automatically. Enable it per field with the showValid input, and optionally add a validFeedback message that renders below the control once the field is touched and valid:

<hub-input formControlName="username" label="Username" required [showValid]="true" validFeedback="Looks good!" />

To turn the success state on for every field at once, set it globally — see Configuration. A per-field showValid always overrides the global default.


🛠️ Configuration

Provide app-wide defaults (invalid-feedback copy, datepicker locale/labels…):

import { provideHubForms } from 'ng-hub-ui-forms';

bootstrapApplication(AppComponent, {
	providers: [
		provideHubForms({
			showValid: true,
			datepicker: { firstDayOfWeek: 1, displayFormat: 'dd/MM/yyyy' }
		})
	]
});

showValid (default false) turns the opt-in valid/success state on for every field once it is touched and valid. The invalid state is unaffected — it is always automatic; only success is gated behind this flag. A per-field showValid input overrides the global default.


🎨 Styling

Everything is themed through --hub-* CSS custom properties. The package ships shared SCSS tokens; import them once at the app root:

@use 'ng-hub-ui-forms/src/lib/styles/index' as hub-forms;
hub-input,
hub-select {
	--hub-input-border-color: #cbd5e1;
	--hub-select-option-selected-bg: #e0e7ff;
}

The opt-in valid/success state is themed through four tokens (chained to the --hub-sys-color-success family by default):

hub-input {
	--hub-form-valid-color: #198754;
	--hub-form-valid-border-color: #198754;
	--hub-form-valid-focus-ring-color: rgba(25, 135, 84, 0.25);
	--hub-form-valid-feedback-color: #198754;
}

hub-slider--hub-slider-track-fill takes a full <image> (e.g. a linear-gradient(to right, …)) for the filled part of the track, which renders intact clipped to the current percentage; --hub-slider-value-space is the value-bubble headroom and collapses to 0 on a [showValue]="false" (flush) slider:

.gradient-slider {
	--hub-slider-track-fill: linear-gradient(to right, #22c55e, #eab308, #ef4444);
}

hub-select inside a modal — the dropdown panel stacks through --hub-select-dropdown-zindex (default calc(var(--hub-sys-zindex-modal, 1055) + 5); the previous --hub-select-dropdown-z-index spelling is deprecated but still honoured), so a select opened inside a HubModal renders above the dialog instead of being clipped underneath it.

hub-segmented variants & theming — the color input tints the selected segment from the semantic families (<hub-segmented color="primary">); any of the --hub-segmented-* slots can be set directly, or in one call with the SCSS mixin:

@use 'ng-hub-ui-forms/styles' as *;

.brand-toggle {
	@include hub-segmented-theme($selected-bg: gold, $selected-color: #111, $radius: 999px);
}

In single-select mode the selected pill slides between options (--hub-segmented-indicator-transition, default 0.2s ease; disabled under prefers-reduced-motion).


✨ Signal Forms (opt-in)

ng-hub-ui-forms/signals is a secondary entry point — the only place that imports @angular/forms/signals, so the core stays Angular-21-safe. Recommended on Angular ≥ 22.

import { HubSignalFieldControl, hubSignalErrorMessages } from 'ng-hub-ui-forms/signals';

♿ Accessibility

  • Labels are associated with their control (for/id); required fields are marked.
  • required — set inline or derived from Validators.required, with formControlName or a direct [formControl] binding — is reflected as aria-required on every field, including the select's combobox search input, the segmented radiogroup and each OTP cell.
  • Validation errors render in an role="alert" region tied to the field.
  • The select exposes correct combobox/listbox semantics; the datepicker is fully keyboard-navigable.

📊 Changelog

See CHANGELOG.md.


🙏 Credits

hub-select is a maintained fork of ng-select by the ng-select contributors. The upstream src/ng-select sources are vendored in place and re-themed with --hub-* tokens — pinned to upstream v23.0.1 (tracked in src/lib/select/UPSTREAM; deviations documented in src/lib/select/PATCHES.md). ng-select is distributed under the MIT License, and the original copyright notices are retained in the vendored files.

The datepicker, inputs and validation layer are original to ng-hub-ui-forms.


📄 License

MIT © Carlos Morcillo