Richard Hunter
  • Projects
  • Writings
  • Contact

commentary on Kara Erickson's talk on Angular Forms
created: Nov 28 2020
edited: Mar 05 2023

commentary on a talk given by Angular Core developer Kara Erickson at Angular Connect in 2017

this talk concerns some advanced topics in relation to Angular forms, such as custom form controls, nested forms, and form projection: important subjects which, by and large, aren't covered particularly well in the Angular docs themselves. although the talk was given a few years ago, it is still relevant, as the API has not changed substantially since

unfortunately, the code shown in the demos was never released, as far as I can ascertain. I will discuss my efforts to recreate it, and also clarify a few things that I found confusing

I'm not going to discuss the first nine minutes of the talk, since it comprises an Angular forms refresher and some updates

custom form controls

9:02

a custom form control is a directive that implements the ControlValueAccessor interface, allowing it to integrate with Angular's Form API. it can be used in an Angular form, just as any native input element can be. it works interchangeably with both reactive and template-driven form modules

what is a ControlValueAccesssor?

an interface that allows the form API to connect with DOM elements. it is responsible for updating the form model in response to some DOM event (e.g., when the user types text into an input field) and for updating DOM elements when the form model changes

why would you want to use them?

9:15

for the same reason you would create any custom component:

  • to break up a template into smaller pieces
  • to enable encapsulation
  • to facilitate code reuse.

some specific examples are:

  • non-native form control elements
  • custom styling / functionality
  • control wrapped with related elements
  • parser / formatter directive

implementing the ControlValueAccessor interface

12:25

the ControlValueAccessor interface comprises 3 required methods and 1 optional:

  writeValue(value: any) {}
  registerOnChange(fn: (value: any) => void) {}
  registerOnTouched(fn: () => void) {}
  setDisabledState(isDisabled: boolean) {}

these can be grouped into two read methods and two write methods. the write methods are writeValue and setDisabledState. they take the current state of the form model and write it into the DOM element. the read methods are registerOnChange and registerOnTouched. they allow listeners to be registered that are called when an event occurs within the DOM.

implementing the first of these two methods presented me with some problems, which I will discuss. the other two did not give me any difficulty

writeValue()

14:30

the writeValue() method is called by the Forms API to set values into our component within the DOM. to do this, we need a reference to our input field. this can be retrieved from our template using a ViewChild query

  @ViewChild("input") input: ElementRef;

we use this reference to set the value of the input element:

  writeValue(val: any) {
    if (this.input) {
      this.input.nativeElement.value = val;
    }
  }

this differs from Kara's example in that an extra check is needed to make sure that the input property exists before attempting to use it. this check is needed when the component is used in a reactive form, but not in a template-driven form. apparently, the Angular Reactive Forms API calls writeValue() before input is set

registerOnChange()

14:45

the registerOnChange() method is called by the forms API to pass a callback to our code, which we must then call whenever there is a change within our component

  // in component

  registerOnChange(fn: (value: any) => void) {
    this.onChange = fn;
  }

  //  in template

  <input type="text" (input)="onChange($event.target.value)"/>

depending on the environment, the above code can cause the following error:

Property 'value' does not exist on type 'EventTarget'.

this is a Typescript problem. Typescript for some reason can't determine the correct type of $event. this is configurable using the strictDOMEventTypes option. the docs say this about it:

Whether $event will have the correct type for event bindings to DOM events. If disabled, it will be any

one solution, therefore, is to set this property to false

as the excerpt from the documentation above suggests, another solution is to cast $event to type any:

<input (input)="onChange($any($event).target.value)"/>

you could also pass $event as the argument to the onChange() method and let the component handle casting, but this is regarded as bad practise

the solution I settled on was to use a template variable and obtain the value of the input from that:

<input #thisInput (input)="onChange(thisInput.value)" />

the thisInput template variable refers to the <input/> element and is given the correct type by Angular

registering with the local injector

15:41

having implemented a ControlValueAccessor, we need to let Angular's Form API know about it. we do this by registering it with the local injector using the NG_VALUE_ACCESSOR token

// in RequiredText
providers: [
  {
    provide: NG_VALUE_ACCESSOR,
    multi: true,
    useExisting: RequiredText
  }
]

NG_VALUE_ACCESSOR is a dependency injection token representing classes that implement the ControlValueAccessor interface. the multi property means that we can register multiple providers with this token. useExisting means that we use the existing instance of the RequiredText component that the injector has already created when it encountered the required-text directive. when a form directive, like ngModel is placed on the required-text component, it will inject all the control value accessors configured on the current injector, and then it will work out which one it wants to use, prioritising custom control value accessors, (which ours is)

code example

validation

16:12

validation is achieved by implementing the Validator interface and registering the component using the NG_VALIDATORS token in the local injector:

  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi: true,
      useExisting: RequiredTextComponent,
    },
    {
      provide: NG_VALIDATORS,
      multi: true,
      useExisting: RequiredTextComponent,
    }
  ],

code example

error messages

19:10

in order to show error messages within the component itself, we need to get a reference to the component's form control. one approach to doing this is to provide it as an input to our component. in a reactive form:

<required-text formControlName="three" [control]="form.get('three')"></required-text>

clearly, though, it would be nicer to not have to do this: it's extra code that we have to write and extra "noise" when reading through the code

better is to use dependency injection and inject the form control into our component through its constructor function

constructor(@Self() public controlDir: NgControl) {}

this works because when we add a form directive to an element the underlying object is registered with the local injector

we specify NgControl as the provider, as it is the super-type of all form directives that we might have set on our form element - such as NgModel and FormControlName - so we can use our component with all of these

the @Self decorator is necessary here so that we don't look beyond the local element injector for the NgControl instance

however, now that we are injecting NgControl, we can have a circular dependency because NgControl, (or rather the instances of it, e.g. NgModel), is injecting both NG_VALUE_ACCESSOR and NG_VALIDATOR, which both contain the RequiredText instance; in other words, NgModel is injecting RequiredText, and RequiredText is injecting NgModel - a circular dependency. we thus need to not provide these in our component.

instead, we have to manually wire our component up with the Angular Forms API and add validators to it

constructor(@Self() public controlDir: NgControl) {
  controlDir.valueAccessor = controlDir;
}

similarily, we can configure the validators within ngOnInit

ngOnInit() {
  const control = this.controlDir.control;
  control.setValidators(Validators.required);
  control.updateValueAndValidity();
}

we can also remove the validate method, if we've got it, as we're no longer doing validation that way. the above code could actually go in the constructor, but it's considered best practice to keep as much logic out of there as possible

now that we have a reference to the formControl, we can now set up the error messages within our component. there's nothing too complicated about this: we just query the form control for properties such as valid and pristine and render an error message accordingly:

  <div *ngIf="controlDir && !controlDir.control.pristine &&!controlDir.control.valid" class="error">There was an error</div>

code example

nested forms

25:22

a nested form is a component that comprises part of a form. a common example is an address component that contains separate fields for street, city, postcode etc. the motivation for having a nested form is similar to that of a custom form component: to group related behaviour and for ease of re-use

there are two kinds of nested form components that Kara talks about:

  • composite ControlValueAccessor component: a component which implements the ControlValueAccessor interface but contains multiple form controls instead of just one
  • sub-form component: a Component which does not implement the ControlValueAccessor interface and contains a form fragment

composite ControlValueAccessor component (CCC)

26:25

implementing a composite ControlValueAccessor is largely the same as implementing the kind that we've already seen

this seems to me to be the best way to do nested forms: it gives the greatest amount of flexibility and reusability. the component works in the same way as a native input element, making it easier to compose complex forms out of them

whilst validation and error messages can be self-contained within the component, and we don't have to pass in the form control, it's important to implement the validate() method so that the component's valid status stays in sync with the containing form

code example

sub form component

30:19

it seems like not having to implement the ControlValueAccessor interface would make sub-form components easier to build, but, in fact, it's the opposite. for reasons we'll go into, it's probably better not to choose this over a composite ControlValueAccessor, but studying them is nonetheless instructive

complicating things is the fact that there's a couple of mistakes in the slides. the first of these is at around 32:50 where the component is called 'RequiredText' instead of 'AddressComponent'. the released slides, fortunately, correct this error

let's now have a closer look at what we mean by a sub-form component

supposing we have a form which contains a firstName field, and a group of fields that comprise the address. we might organise this as follows:

<form #form="ngForm">
  <label for="name">name</label>
  <input name="firstName" ngModel id="name" required />

  <div ngModelGroup="address">
    <input ngModel name="city"/>
    <input ngModel name="postcode"/>
  </div>
</form>

the address fields are grouped using the ngModelGroup directive. now supposing we want the address section to be its own component. we might create an Address component with a template that looks like this:

  <div ngModelGroup="address">
    <input ngModel name="city"/>
    <input ngModel name="postcode"/>
  </div>

we might just expect this to work since we haven't changed the actual HTML in anyway, but instead we get this error:

Error: NodeInjector: NOT_FOUND [ControlContainer]

about a ControlContainer, Angular docs says this:

A base class for directives that contain multiple registered instances of NgControl.

if we look at the list of subclasses we see NgForm amongst them. from this we can deduce that the ControlContainer is in fact our form. what is happening is that our component is looking for NgForm in the injector but can't find it. the reason is that NgForm is configured that it will only inject an instance of ControlContainer if it exists in an injector that is not higher in the hierarchy of element injectors than that for the template of the Host component.

the way to fix this is to provide NgForm within the viewProviders array of AddressComponent:

 viewProviders: [
    { provide: ControlContainer, useExisting: NgForm}
  ],

but what if the ControlContainer isn't NgForm? after all, as we saw earlier, ControlContainer has many sub-classes. to explore this possibility, let's change our form so that the address component has as its parent an ngModelGroup instead of an ngModelForm:

<form #form="ngForm">

  <label for="name">name</label>
  <input name="firstName" ngModel id="name" required />

  <div ngModelGroup="home">
    <input name="telephone" ngModel/>
    <my-address></my-address>
  </div>

</form>

we will find that our form data now looks like this:

{
  "firstName": "",
  "home": {
    "telephone": ""
  },
  "address": {
    "city": "",
    "postcode": ""
  }
}

contrary to expectations, address is not nested within home. what has happened is that the Address Component is still configured to see NgForm as its ControlContainer and cannot see the NgModelGroup at all

if we change viewProviders so that ControlContainer looks for an NgModelGroup instead:

  viewProviders: [
    { provide: ControlContainer, useExisting: NgModelGroup}
  ],

then the form data looks like what we would like it to be:

  "firstName": "",
  "home": {
    "telephone": "",
    "address": {
      "city": "",
      "postcode": ""
    }
  }
}

this illustrates an important constraint of Sub Form Components: you have to be careful about how you nest them as you may experience unexpected behaviour. this same constraint is why sub-forms are not interchangeable with reactive and template-driven forms

code example

just to prove this, let's see how you would implement a sub-form using reactive forms. when we create this template within AddressComponent:

   <div formGroupName="address">
     <input formControlName="street"/>
     <input formControlName="city"/>
   </div>

we get the following error:

Error: Cannot read property 'getFormGroup' of null

as before, we register the ControlContainer in the viewProviders array, this time assigning it to be a FormGroupDirective instead of NgModelGroup:

  viewProviders: [
    { provide: ControlContainer, useExisting: FormGroupDirective}
  ],

now we get a different error:

ERROR
Error: Cannot find control with name: 'address'

this is because the address FormGroup does not exist. we need to create it within AddressComponent. we do this by adding the FormGroup to the parent form:

  parent: FormGroupDirective;

  constructor(parent: FormGroupDirective) {
    //this.form = parent.form;
    this.parent = parent;
  }

  ngOnInit() {
    this.parent.form.addControl('address', new FormGroup({
      street: new FormControl(),
      city: new FormControl(),
    }))
  }

now it will work

[in the slides for this there's another mistake: this.form within the constructor wont work because parent.form is null at this point. there's a discussion about this here]

code example

form projection

36:54

form projection is where you project content into a form element. for example, you have a wrapper component that looks something like this:

<form>
  <ng-content></ng-content>
</form>

and used like this:

<wrapper-component>
  <input />
</wrapper-component>

the <input/> element will be projected into the form so that the final DOM looks like:

<form>
  <input/>
</form>

this approach is somewhat complex to get working and Kara advises against it; nonetheless, she shows a way of making it work

in the demo, this is the content that is being projected into a form:

  <div ngModelGroup="address">
    <input name="street" ngModel/>
    <input name="city" ngModel/>
  </div>

code example is a demo that shows this.

note the error message:

NodeInjector: NOT_FOUND [ControlContainer]

this is the same error we got with sub-form components. once again, ngModelGroup cannot find its ControlContainer: the NgForm directive. the same solution will not work here though as the situation is a little different. for the FormStepper component, we have a view in which the <form> exists, and content, where we find the form elements, including the ngModelGroup directive that is searching for the ControlContainer. the problem is that the view and the content are not able to directly contact each other; therefore, they must do so through the FormStepper component.

within the FormStepper component, we obtain the NgForm directive using a ViewChild query. we provide this to the component's content by adding it to the Providers array using useFactory. we hope that this should allow ngModelGroup to get a reference to NgForm through dependency injection

code example

but this too is insufficient. there are two reasons for this. Kara gives one reason, and the other one I discovered myself. Kara's reason is that the form still can't be found by ngModelGroup because it lives in the view, whilst ngModelGroup is in the content, and because content is instantiated before the view, the form doesn't exist at the point when ngModelGroup goes looking for it. the second problem is that useFactory also runs before the template view is created and so the reference it gets to it is undefined

dealing with the first problem first, the solution is to instantiate the content after the view has been created. we do this by wrapping the content in an ng-template element (which means it's not rendered), obtaining this within our component using a ChildContent query, then placing it into our view using a ngTemplateOutlet directive:

<div *ngTemplateOutlet="template"></div>

this cleverly means content is processed after the view is instantiated

the second problem can be fixed by forcing the ViewChild query to run earlier. when Kara did this talk, the ViewChild would run, by default, before change detection. later this default was changed to running afterwards. the ViewChild query takes an option of static: true to force it to run before. this was pointed out to me by StackOverflow user g0rb, and I thank him for his help

 @ViewChild(NgForm, { static: true }) form: NgForm;

here is the working code

conclusion

this is by far the best resource I've managed to find on advanced Angular form techniques. some of the techniques shown are probably not ones you absolutely would need to know - or may even be described as anti-patterns - but they are useful to know in case you do come across them, and they are instructive as to some of the deeper workings of Angular

resources

  • another good resource regarding the ControlValueAccessor interface is this blog by Jennifer Wadella. she also does some talks on the same subject that can be found on YouTube

  • Ward Bell, wrote an article about reactive forms that mentions this talk. he remarks that he's somewhat confused by the section on nested forms

Featured writings

creating a bar chart using D3

a tutorial demonstrating how to create a vertical bar chart ...

making sense of Angular forms

a deep dive into Angular's two form modules...

Angular Resolution Modifiers and scope

how resolution modifiers like @Host and @Self affect injecto...

Injectors in Angular Dependency Injection

a deep dive into the scope of Component and Directive inject...

commentary on Kara Erickson's talk on Angular Forms

commentary on a talk given by Angular Core developer Kara Er...

Building my first computer game

My first attempt at creating a computer game in Javascript...

Animated page transitions in a Single Page App

In a Single Page App, we can take control of the routing pro...

Problems with Redux and why Streams are better

A discussion on some of the drawbacks of using Redux within ...

Implementing Angular's Hero app using React

Implementing Angular documentation's Hero app using React. ...

Dependency Injection

A discussion on Dependency Injection and a library I have wr...

Ways of making CSS clip-path property work better

Ways of making the clip path better...

Carracci: a UML diagram editing tool

Carracci is a project that I have been working on recently i...
  • Home
  • Projects
  • Writings
  • Contact