Crafting a Custom Angular Directive for Reusable Form Validation
Angular ships with a generous toolbox for handling user input, yet many teams still repeat the same validation glue across component after component. A well-designed custom directive collapses that repetition into a single declarative attribute that any form control can wear. For Australian teams shipping anything from a Melbourne-based booking platform to a Brisbane health portal, that kind of consistency matters because users expect error feedback to behave the same way regardless of which screen they are looking at.
This walk-through moves from the underlying validator function through to a fully wired attribute directive, then rounds out with testing habits and a small checklist you can apply on your next feature branch. The goal is not to invent a new validation library but to give you a mental model for shaping your own, so the rules your product needs sit alongside Angular's built-in validators instead of fighting them.
Why a directive beats scattered validators
Reactive forms in Angular treat validation as a function attached to a control, and there is nothing wrong with that pattern. The trouble begins when the same rule appears in five sibling components, each with its own slightly different error key or message. A team in Sydney I worked with last year had three near-identical postcode validators scattered across checkout, address book and profile screens, and the bugs they produced were almost as similar.
A custom attribute directive offers a different shape. Instead of bolting a validator onto every FormGroup by hand, you drop an attribute onto the input itself and Angular takes care of registration, update cycles and teardown. The control stays in charge of its own state, the directive stays in charge of presentation, and the component template stays readable. The downstream benefit is that designers in Adelaide or Perth who prototype new flows can drop the attribute onto a field and trust the behaviour to match what already exists in production.
This separation pays off again during audits. When a regulator asks how a form enforces a rule, you can point at one file rather than grep the repository. Australian compliance teams working under the Privacy Act tend to appreciate that kind of traceability, especially when the rule touches personal data such as a Tax File Number or a Medicare identifier.
Designing the validator function that powers the directive
Before any directive exists, you need a plain validator function. Angular exposes the ValidatorFn interface, and its signature is small enough to keep in your head: it takes an AbstractControl and returns ValidationErrors or null. The null return means everything is fine; an object return means something is wrong, and the keys of that object become the error tokens your template can switch on.
Consider a validator that checks whether an input contains a plausible Australian mobile number. The function would normalise the string, strip spaces and the leading zero, then compare against the 04 prefix range used by Telstra, Optus and Vodafone. Returning something like { auMobile: true } gives the directive a stable token to read later. Because the function is pure, you can unit-test it without spinning up a TestBed, which keeps the feedback loop fast.
The function does not need to know anything about the DOM. That decoupling is what lets you reuse the same validator inside a reactive form, a template-driven form or a directive wrapper. Keeping it side-effect-free also means you can compose it with Angular's Validators.compose helper, so a single field can carry several independent rules without one bleeding into another.
Wrapping the function in an attribute directive
Once the validator function is stable, the directive becomes a thin shim. You give it a selector that reads naturally in templates, such as [appAuMobile], and you register the validator through Angular's dependency injection using NG_VALIDATORS. That single provider line is what hooks the function into the form lifecycle.
The directive class itself usually exposes one or more @Input setters. Each setter calls this.validator to recreate the closure with the latest options, then pokes the control's update cycle so the new rule runs immediately. This pattern is what allows a directive like [appAuMobile] to also accept configuration, for example a future-proof flag that toggles support for the new +61 country prefix. The component template stays declarative: <input formControlName="phone" appAuMobile>, and the directive handles the rest.
Because the directive lives at the template layer, it can also reach into the host element's accessibility tree. Setting aria-invalid when the control becomes invalid, or announcing the error through an aria-describedby link to a message element, keeps screen reader users in Canberra and Darwin on the same footing as everyone else. That detail often gets overlooked when validation lives in TypeScript files far away from the markup.
Surfacing friendly error messages without template clutter
A validator that returns { auMobile: true } is only useful if the user sees what went wrong. The cleanest pattern is to keep the message rendering inside the component template, but driven by the same error token the directive produced. A small *ngIf block bound to control.errors?.auMobile lets each form decide its own wording, while the directive stays presentational-light.
Where teams get into trouble is by hard-coding copy inside the validator itself. Resist that urge. Returning a translated string from a pure function couples your rule to your i18n bundle, which becomes painful the moment a Perth-based partner asks for the same form in Mandarin. The cleaner approach is to let the directive emit error tokens and let the template map those tokens to whatever copy the product team has approved.
Error timing matters too. Angular updates validity on every value change by default, which is great for password strength bars but noisy for a postcode field. The directive can opt into updateOn: 'blur' by tweaking the control provider, and the template can stagger messages so the field only complains after the user has finished typing. This is a small ergonomic touch that Australian users tend to notice, particularly when filling out long claim forms on a flaky regional connection.
Test patterns that catch regressions early
A custom directive deserves the same testing rigour as a service. The fastest starting point is a standalone spec for the validator function, where you feed in strings like '0412 345 678', '+61412345678' and '02 1234 5678' and assert the returned error tokens. Because it has no Angular dependencies, this spec runs in milliseconds and becomes a living specification of the rule.
For the directive itself, TestBed is the right tool. You build a small ReactiveFormsModule host component, attach the directive to a real input, and assert that marking the control dirty and setting an invalid value flips aria-invalid and populates the expected error token. Snapshot testing the rendered template is tempting but brittle, so favour behaviour assertions. The goal is to catch the day someone changes the selector and breaks every form in the codebase, not to lock down pixel-perfect output.
If your team already runs Cypress or Playwright, layer in a single end-to-end check that exercises the directive through a real form flow. That extra minute of work tends to surface the awkward interactions between native browser validation, Angular's own cycle and assistive technology, all of which can drift independently between Chrome, Safari and Firefox on Australian devices.
Habits that keep validators maintainable over time
Custom directives tend to grow quietly, and without a few guardrails they turn into a junk drawer. The checklist below keeps the pattern clean and helps new contributors land safely.
- Treat the validator function as pure. No DOM access, no service injection, no console logs. Anything impure belongs in the directive.
- Pick stable, namespaced error tokens such as
auMobileortfnFormatinstead of genericinvalidkeys that clash with built-ins. - Document each directive with a short block comment explaining the rule in plain English so non-Angular reviewers can audit it.
- Co-locate the validator, the directive and its spec in a single folder. Discoverability beats premature abstraction.
- Add an integration test that runs the directive through a real
FormGrouponce per release. Unit specs alone miss lifecycle bugs. - Review the directive's inputs whenever the rule's business owner changes. Rules drift; code should follow.
Pair this habit list with a quick code review checkpoint and the directive pattern will keep paying for itself long after the feature ships.
If you want to see how a custom directive like the one above slots into a wider application, the walk-through at building-a-full-stack-angular-and-net-application-from-scratch shows the same technique wired into a real Angular front end talking to a .NET API. Skim the section on form handling and you will spot where this directive pattern would slot in cleanly, especially around the Australian address fields used throughout that demo. Drop the attribute onto a control, point it at the validator, and the rest of the form keeps working without any extra ceremony.