typescript
54 lines · 8 steps
A custom range validator directive in Angular
An Angular attribute directive that validates a numeric range and mirrors its state into ARIA attributes.
Explained by
highlit
1import { Directive, Input, HostBinding, HostListener } from '@angular/core';
2import { NG_VALIDATORS, Validator, AbstractControl, ValidationErrors } from '@angular/forms';
3
4@Directive({
5 selector: '[appNumericRange]',
6 standalone: true,
7 providers: [
8 { provide: NG_VALIDATORS, useExisting: NumericRangeDirective, multi: true },
9 ],
10})
11export class NumericRangeDirective implements Validator {
12 @Input({ required: true, alias: 'appNumericRange' }) range!: [number, number];
13
14 private outOfRange = false;
15
16 @HostBinding('attr.aria-invalid')
17 get ariaInvalid(): 'true' | null {
18 return this.outOfRange ? 'true' : null;
19 }
20
21 @HostBinding('attr.aria-valuemin')
22 get ariaValueMin(): number {
23 return this.range[0];
24 }
25
26 @HostBinding('attr.aria-valuemax')
27 get ariaValueMax(): number {
28 return this.range[1];
29 }
30
31 @HostListener('input', ['$event.target.value'])
32 onInput(raw: string): void {
33 const value = Number(raw);
34 this.outOfRange = raw !== '' && (Number.isNaN(value) || value < this.range[0] || value > this.range[1]);
35 }
36
37 validate(control: AbstractControl): ValidationErrors | null {
38 const [min, max] = this.range;
39 const value = Number(control.value);
40
41 if (control.value === '' || control.value == null) {
42 this.outOfRange = false;
43 return null;
44 }
45
46 if (Number.isNaN(value) || value < min || value > max) {
47 this.outOfRange = true;
48 return { numericRange: { min, max, actual: control.value } };
49 }
50
51 this.outOfRange = false;
52 return null;
53 }
54}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Registering a directive under NG_VALIDATORS with multi:true plugs custom logic into Angular's form validation pipeline.
- 2HostBinding getters let a directive keep DOM attributes in sync with internal state declaratively.
- 3Reflecting validation state into ARIA attributes makes custom form controls accessible to assistive technology.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
java
package com.acme.billing.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties;
Feature-flagged beans with Spring @ConditionalOnProperty
feature-flags
conditional-beans
strategy-pattern
Intermediate
5 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-custom-range-validator-directive-in-angular-explained-typescript-c1b6/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.