Monday, May 23, 2022

No provider for ControlContainer!

As you can probably deduce from the title, this post is all about an annoying error in Angular that reads "No provider for ControlContainer!". If you're just getting this error in general, the first thing you should do is make sure you've imported the FormsModule and ReactiveFormsModule (yes, both of them). That will probably clear it up for you. If you've done that and you're still getting this error, specifically while running your unit tests, I have a solution for you. I'm nearly 100% certain I didn't come up with this on my own, but since I'm not sure where I originally found it I can't link you over to it. Sorry for that.

First off, let me show you the code that triggered this issue when I started testing. Like a good programmer following DRY (Don't Repeat Yourself) I will often componentize even small things that are reused and require some bit of setup or configuration. For instance, when creating forms that require masked input fields (like phone number or credit card) I'll create a component that allows me to quickly and easily drop that into my form and just specify which mask to use. The way I do that requires me to inject ControlContainer directly into the component. Here's some example code:

import { Component, Input, OnInit } from '@angular/core';
import { ControlContainer, FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-masked-input'
  templateUrl: './masked-input.html'
})
export class MaskedInputComponent implements OnInit {
  @Input() controlName: string;
  @Input() label: string;
  @Input() mask: any;

  constructor(public controlContainer: ControlContainer) { }

  public ngOnInit(): void {
    // Set our form property to the parent control
    // (i.e. FormGroup) that was passed to us, so that our
    // view can data bind to it
    this.form = this.controlContainer.control as FormGroup;
    this.control = this.form.get(this.controlName) as FormControl;
  }
}

That's the relevant part to this error. If we try to run unit tests around that code we'll get the error "No provider for ControlContainer!" even if we import FormsModule and ReactiveFormsModule in our test file. To fix that error, we have to manually create a FormGroupDirective in our test file and change the provider for ControlContainer to use that FormGroupDirective. Here are the relevant bits of code:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ControlContainer, FormControl, FormGroup, FormGroupDirective, FormsModule, ReactiveFormsModule } from '@angular/forms';
...snip...
describe('MaskedInputComponent'), () => {
  let component: MaskedInputComponent;
  let fixture: ComponentFixture<MaskedInputComponent>;
  const formGroup: FormGroup = new FormGroup({
    dynamicControlName: new FormControl('')
  });
  const formGroupDirective: FormGroupDirective = new FormGroupDirective([], []);
  formGroupDirective.form = formGroup;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ MaskedInputComponent ],
      imports: [ FormsModule, ReactiveFormsModule ],
      providers: [ { provide: ControlContainer, useValue: formGroupDirective } ],
    })
    compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(MaskedInputComponent);
      component = fixture.componentInstance;
      component.controlName = 'dynamicControlName';
      fixture.detectChanges();
    })
    compileComponents();
  });
});

That's it! This will alleviate the "No provider for ControlContainer!" error in your test run. It's simple enough once you know what to do, but it was a pain figuring it out. Hope this helps.