Forms

Angular Custom Form Controls - Complete Guide

Build a fully functional custom form control, compatible with template-driven and reactive forms, as well as with all built-in and custom form validators.

Angular Custom Form Controls - Complete Guide — Forms article by Angular University on Angular In Depth
Angular Custom Form Controls - Complete Guide — Forms article by Angular University on Angular In Depth
On this page · 16 sections

The Angular Forms and ReactiveForms modules ship with a set of built-in directives that make it straightforward to wire standard HTML elements—such as inputs, checkboxes, and text areas—into a form group.

However, we often need to use custom form controls instead: dropdowns, selection boxes, toggle switches, sliders, and other common UI components that go beyond plain HTML.

For these custom components, we want the same experience as with native inputs: the ability to attach them to a form using the identical directives (ngModel, formControl, formControlName) that we use for standard form fields.

In this guide, we are going to look at exactly how to convert an existing custom component into one that is fully compliant with the Angular Forms API, so it can take part in the parent form’s value tracking and validation mechanics.

In practice, this means:

  • for template-driven forms, we can connect the custom component simply by adding ngModel
  • for reactive forms, we can register the custom component via formControlName (or formControl)

In this guide, we are going to build a simple quantity selector that allows the user to increase or decrease a numeric value. This component will be placed inside a form and will be flagged as invalid whenever the counter falls outside a predefined range.

Our custom form control will work seamlessly with the standard Angular Validators required, max, and any other built-in or custom validators we choose to apply.

We will also cover how to design reusable nested forms—chunks of form structure that can be shared across multiple forms.

The classic example here is an address sub-form, and we are going to implement one in this post.

Table Of Contents

In this post, we are going to cover the following:

  • How standard form controls work internally
  • What control value accessors are
  • An introduction to the ControlValueAccessor interface
  • Implementing the ControlValueAccessor interface step by step
  • Implementing the Validator interface
  • A working demo of a custom form control
  • Building nested form groups (an address nested form)
  • Wrap-up

This post is part of our ongoing Angular Forms series; you can find the complete list of articles here.

Now, let’s dive into everything you need to know to create custom form controls and nested forms!

How do standard form controls work?

To build a custom form control, we first need to understand the mechanics behind the built-in ones.

The built-in form controls are designed to target native HTML elements: inputs, text areas, checkboxes, and so on.

Consider a simple form that contains a couple of plain HTML fields:

As shown, we have a few standard form controls, each with the formControlName attribute attached. That is how the HTML element gets associated with the form.

Whenever the user changes any of these inputs, the form’s value and its validation state are recomputed automatically.

So, what is happening behind the scenes?

What are control value accessors?

Under the hood, the Angular Forms module attaches a built-in directive to each native HTML element. That directive is responsible for watching the value of the field and reporting it back to the parent form.

This special kind of directive is called a control value accessor.

Take, for instance, the checkbox field in the form above. There is a select built-in directive from the reactive forms module whose sole purpose is to track the value of a checkbox and nothing else.

Here is the simplified declaration of that directive:

As the selector shows, this value-tracking directive applies specifically to HTML inputs of type checkbox—but only when one of the ngModel, formControl, or formControlName directives is also present on the element.

What about the other form control types—text inputs, text areas, and so on?

Each of those control types has its own value accessor directive, distinct from CheckboxControlValueAccessor.

All these directives are part of the Angular Forms module and only cover the standard set of HTML form controls.

This implies that if we want to create a custom form control, we have to provide a custom value accessor for it as well.

The custom form control that we will build

Suppose we want to build a custom form control that acts as a numeric counter with increment and decrement buttons—useful, for example, when selecting an order quantity.

Every time the user presses one of the buttons, the counter should go up or down by a configurable step.

We also want the ability to set a maximum allowed value for the field, which should trigger an invalid state if the value goes beyond that limit.

Here’s what the numeric form control will look like:

Angular Custom Form Control

And here is the implementation for this component, before any form-related logic is added:

As it currently stands, the component is not compatible with either template-driven or reactive forms.

Our goal is to be able to place this component in a form exactly like a standard HTML input, by adding the formControlName or ngModel directives to it:

We also want it to work with the built-in validators, so we can make the field required and set a maximum limit:

But if we try to use our component as it is now, we would be greeted with an error:

ERROR Error: Must supply a value for form control with name: 'totalQuantity'.
at forms.js:2692
at forms.js:2639
at Array.forEach (<anonymous>)
at FormGroup._forEachChild (forms.js:2639)
at FormGroup._checkAllValuesPresent (forms.js:2690)
at FormGroup.setValue (forms.js:2490)
at CreateCourseStep1Component.ngOnInit (create-course-step-1.component.ts:51)
at callHook (core.js:3405)
at callHooks (core.js:3375)
at executeInitAndCheckHooks (core.js:3327)

To fix that error and make the choose-quantity component compatible with Angular Forms, we need to give it a value accessor—the same way native elements like text inputs and checkboxes have one.

To achieve that, we are going to make our component implement the ControlValueAccessor interface.

Understanding the ControlValueAccessor interface

Let’s walk through the methods of the ControlValueAccessor interface. These are not meant to be called from our own code—they are framework callbacks.

All of them are invoked internally by the Forms module at runtime, and they serve as the communication bridge between our control and the parent form.

Here are the methods in the interface and what each one does:

  • writeValue: the Forms module calls this method whenever it needs to set a value in the form control
  • registerOnChange: when the value of the form control changes—typically due to user interaction—we must report the new value back to the parent form. This is achieved by invoking a callback that the parent form registered via registerOnChange
  • registerOnTouched: the first time the user interacts with the form control, the control is deemed to have been “touched,” which is often used for styling purposes. To notify the parent form about that touch, we use a callback registered through registerOnTouched
  • setDisabledState: forms allow controls to be enabled or disabled. This state is passed down to our control via setDisabledState

Here is the component with the ControlValueAccessor interface fully implemented:

Now let’s look at each method in turn and see how it’s implemented.

Implementing writeValue

The Angular forms module calls writeValue whenever the parent form needs to set a value in the child control.

In our case, we simply take the incoming value and assign it to the internal quantity property:

Implementing registerOnChange

The parent form can pass a value down to the child control via writeValue, but what about the reverse direction?

If the user interacts with the control—say by clicking the increment or decrement button—the new value has to be sent back up to the parent form.

The child control can signal the parent form that a new value exists by invoking a callback function.

For this to work, the parent form first registers that callback with the child control using the registerOnChange method:

As you can see, when this method gets called, we receive the callback function and store it in a member variable for later use.

The onChange member is declared as a function and is initialised with an empty function—that is, a function with a blank body.

This way, if our code accidentally invokes it before the registerOnChange call has been made, no errors will surface.

Whenever the counter value changes—because the user pressed either the increment or the decrement button—we need to inform the parent form.

We do that by calling the callback function and passing along the new value:

Implementing registerOnTouched

In addition to reporting value changes, we also need to notify the parent form when the child control has been touched by the user.

When a form is first created, every form control—and indeed the form group itself—has the status “untouched,” and the ng-untouched CSS class is applied to both the form group and to every one of the child controls.

However, as soon as a child control is touched—meaning the user has interacted with it at least once—the whole form is considered touched, and the ng-touched CSS class is applied.

These touched/untouched CSS classes play a key role in styling form error messages, so our custom control must support this as well.

Just like before, we need to have a callback in place so the child control can hand its touched status back to the parent form:

Next, we need to call this callback at the moment the control becomes touched, which will happen the first time the user clicks either button:

As you can see, the first time one of the two buttons is pressed, we call the onTouched callback, and from that point on the parent form treats the control as touched.

The custom form control will then have the ng-touched CSS class applied, just like any native control:

Angular Custom Form Control in status touched

Implementing setDisabledState

It is also possible for the parent form to enable or disable any of its child controls through the setDisabledState method. We store the disabled state in the member variable disabled, and we use it to toggle the increment and decrement functionality:

Dependency injection configuration for ControlValueAccessor

Finally, to implement the ControlValueAccessor interface correctly, we must register our custom form control as a recognised value accessor in the dependency injection system:

Without this configuration, our custom form control will not work as intended.

So what exactly does this configuration do? We are adding our component to the list of known value accessors, all of which are registered under the unique NG_VALUE_ACCESSOR dependency injection key (otherwise known as an injection token).

Take note of the multi flag, which is set to true. This tells the DI system that this provider contributes a list of values, not just a single value. That’s necessary because Angular Forms has many value accessors registered under NG_VALUE_ACCESSOR, and ours is only one of them.

All the built-in value accessors—for text inputs, checkboxes, etc.—are likewise registered under the same token.

So whenever the Angular Forms module needs the full set of available value accessors, all it has to do is inject NG_VALUE_ACCESSOR.

With this in place, our component is now able to assign a value to a property within a form.

More than that, it can take part in the form validation flow and is fully compatible with, for example, the built-in required and max validators.

But what if the component needs its own validation rules that are always active for every instance of the component, regardless of the form configuration?

Understanding the Validator interface

For our custom form control, we want it to enforce a positive quantity value. When the value is negative, the form field should be flagged as invalid across all instances of the component.

To achieve this, our component needs to implement the Validator interface, which defines just two methods:

  • validate: This method evaluates the current value of the form control and is invoked whenever a new value is passed to the parent form. It should return null when the value is acceptable, or an error object containing the necessary details to show a relevant error message to the user.
  • registerOnValidatorChange: This registers a callback that allows us to manually trigger validation of the custom control. This is not needed for value changes since validation happens automatically in that case. It is only necessary when another input affecting the outcome of validate changes.

Let's proceed to see how we can apply this interface and finalize the demonstration of our component.

Applying the Validator interface

The only method from Validator that we are required to implement is validate:

In this implementation, we return null when the value passes validation, and an error object detailing the issue when it doesn't.

For our specific component, we didn't implement registerOnValidatorChange since it's optional.

This method would be needed if, for instance, our component had configurable validation rules based on component inputs. In such a case, we could trigger validation on demand when those inputs change.

Since our validate method relies solely on the current control value, we didn't need to capture the callback from registerOnValidatorChange.

To make the Validator interface effective, our custom component must also be registered with the NG_VALIDATORS injection token:

It's important to note that without properly registering the class in NG_VALIDATORS, the validate method will never be invoked.

Demonstration of a complete custom form control

By combining the ControlValueAccessor and Validator interfaces, we now have a fully operational custom form control that works with both reactive and template-driven forms. It can set form property values and take part in the form's validation process.

Here's the complete code:

Now, let's see how this component behaves at runtime by integrating it into a form along with standard validators:

We've made the field required and set a maximum limit of 100. The control starts with a value of 60, which is considered valid.

What happens when we change the value to something like 110? The form becomes invalid, and the totalQuantity control will have an error attached to it.

We can inspect this error by looking at the errors property of form.controls['totalQuantity']:

As expected, the built-in Validators.max(100) validator flagged our custom control as invalid.

What if we instead set the quantity to a negative value like -10? Here's what the errors property would show:

This time, the validate method produced a ValidationErrors object, which was subsequently added to the form control's errors.

We now have a fully functional custom form control that is compatible with template-driven forms, reactive forms, and all built-in validators.

Building nested form groups (an address sub-form)

A common forms pattern you can implement with the techniques discussed is a nested form group that can be reused across multiple forms.

A typical example is an address form containing the standard address fields:

Angular Nested Form Example - an Address form

Suppose your application has multiple forms requiring an address. You wouldn't want to duplicate all the display and validation logic for those fields in every form.

Instead, the goal is to create a reusable form section as an Angular component that can be plugged into various forms—a sort of nested, reusable sub-form.

Here's how we'd want to use such an address form component:

Our address form component should be fully compatible with Angular forms, meaning it supports the ngModel, formControl, and formControlName directives and can contribute to the parent form's validation.

Sounds familiar?

To accomplish this, we simply implement the ControlValueAcessor and Validator interfaces, just as before. So, how does it work?

First, we need to define the view of our nested address form component:

Our nested address form is itself a form group that internally uses Angular forms to collect and validate each address field.

The form object already contains all the necessary information about the subform's values and validity. We can leverage this to quickly implement both the ControlValueAccessor and Validator interfaces:

Here are some key points about this implementation:

  • The address form component internally uses a form with built-in validators for all its validation logic.
  • We rely heavily on the principle of delegation—for instance, pulling all needed information from the form object.
  • As an example, writeValue is implemented via form.setValue, and setDisabledState uses form.enable() and form.disable().
  • We subscribe to the valueChanges Observable to detect new values from the address form and call the onChange callback to inform the parent form.
  • Since we manually subscribe to valueChanges, we also unsubscribe using OnDestroy to prevent memory leaks.
  • The validate method checks the embedded form controls for any errors and passes them into a ValidationErrors object.

Demonstration of the nested address form

When you enter values into the address form, they are forwarded to the parent form and appear under the address property.

Here's the value property of the parent form containing address-form after typing an address:

Wrap-up

Every form control is associated with a control value accessor that handles the interaction between the control and its parent form.

This applies to all standard HTML form controls—text inputs, checkboxes, and more—for which the forms module provides built-in control value accessors.

For custom form controls, you'll need to build your own accessor by implementing the ControlValueAccessor interface. If you also want custom validation, you'll implement the Validator interface as well.

The same technique can be used to create nested form groups, like an address sub-form, that can be reused across different forms.

I hope this post has been helpful. If you're interested in learning more about Angular Forms, consider the Angular Forms In Depth course, which covers validators, custom form controls, and other advanced topics in detail.

If you have any questions or comments, feel free to leave them below, and I'll respond.

To stay updated on future Angular posts, subscribe to our newsletter:

If you're new to Angular, you might also find the Angular for Beginners Course useful:

Angular Custom Form Controls - Complete Guide — figure 4
AU
Angular University

Writes about RxJS, Components, Signals. Active 2015–2026.

All 79 articles →