32

I am aware of creating custom controls as components, but I can't figure out how to create custom groups.

The same we can do this by implementing ControlValueAccessor and using a custom component like <my-cmp formControlName="foo"></my-cmp>, how can we achieve this effect for a group?

<my-cmp formGroupName="aGroup"></my-cmp>

Two very common use-cases would be (a) separting a long form into steps, each step in a separate component and (b) encapsulating a group of fields which appear across multiple forms, such as address (group of country, state, city, address, building number) or date of birth (year, month, date).


Example usage (not actual working code)

Parent has the following form built with FormBuilder:

// parent model
form = this.fb.group({
  username: '',
  fullName: '',
  password: '',
  address: this.fb.group({
    country: '',
    state: '',
    city: '',
    street: '',
    building: '',
  })
})

Parent template (inaccessible and non-semantic for brevity):

<!-- parent template -->
<form [groupName]="form">
  <input formControlName="username">
  <input formControlName="fullName">
  <input formControlName="password">
  <address-form-group formGroup="address"></address-form-group>
</form>

Now this AddressFormGroupComponent knows how to handle a group which has these specific controls inside of it.

<!-- child template -->
<input formControlName="country">
<input formControlName="state">
<input formControlName="city">
<input formControlName="street">
<input formControlName="building">
Lazar Ljubenović
  • 17,499
  • 8
  • 54
  • 86

2 Answers2

70

The piece I was missing was mentioned in rusev's answer, and that is injecting the ControlContainer.

Turns out that if you place formGroupName on a component, and if that component injects ControlContainer, you get a reference to the container which contains that form. It's easy from here.

We create a sub-form component.

@Component({
  selector: 'sub-form',
  template: `
    <ng-container [formGroup]="controlContainer.control">
      <input type=text formControlName=foo>
      <input type=text formControlName=bar>
    </ng-container>
  `,
})
export class SubFormComponent {
  constructor(public controlContainer: ControlContainer) {
  }
}

Notice how we need a wrapper for the inputs. We don't want a form because this will already be inside a form. So we use an ng-container. This will be striped away from the final DOM so there's no unnecessary element.

Now we can just use this component.

@Component({
  selector: 'my-app',
  template: `
    <form [formGroup]=form>
      <sub-form formGroupName=group></sub-form>
      <input type=text formControlName=baz>
    </form>
  `,
})
export class AppComponent  {
  form = this.fb.group({
    group: this.fb.group({
      foo: 'foo',
      bar: 'bar',
    }),
    baz: 'baz',
  })

  constructor(private fb: FormBuilder) {}
}

You can see a live demo on StackBlitz.


This is an improvement over rusev's answer in a few aspects:

  • no custom groupName input; instead we use the formGroupName provided by Angular
  • no need for @SkipSelf decorator, since we're not injecting the parent control, but the one we need
  • no awkward group.control.get(groupName) which is going to the parent in order to grab itself.
Lazar Ljubenović
  • 17,499
  • 8
  • 54
  • 86
  • That was really helpful! – papaiatis Oct 27 '17 at 19:11
  • Thanks @lazar-ljubenović this helped and works like a charm. – Sanchit Nov 19 '17 at 17:57
  • @LazarLjubenović Plz have a look at the [link](https://stackoverflow.com/questions/52658221/how-to-access-dynamically-added-control-in-child-component-from-its-parent-comp) – Zaker Oct 05 '18 at 04:07
  • 4
    Great answer thanks, how i could implement Validations?, I am try to do that on subcomponent, but i not have idea, how can I do..? – Brayan Loayza Dec 11 '18 at 18:05
  • 1
    Thanks, very helpful. Is this in the Angular docs? I have looked for it but have not found anything. – Sebastiandg7 Feb 14 '19 at 04:10
  • 2
    You just separated template what about the controls. I something you want to render conditionally and if those properties foo, bar is required then you cannot submit the form. – Ali Adravi Mar 09 '19 at 23:26
  • Works perfect. Thanks @LazarLjubenović – Piosek Mar 13 '19 at 13:56
  • This is exactly what I wanted to achieve. – Kushal J. Mar 23 '20 at 14:01
  • How we can access the whole formGroupName inside the custom component ? – pece Apr 23 '21 at 13:47
  • You shouldn't have to. But if you really want to, you'll need to iterate over the parent's children object until you find a matching value. – Lazar Ljubenović Apr 24 '21 at 14:02
  • 2
    `Type 'AbstractControl | null' is not assignable to type 'FormGroup'. Type 'null' is not assignable to type 'FormGroup'.` I get this message when trying to bind the `controlContainer.control` to the `formGorup` on the ``. I think it has to do with the strict type checking, but how do i fix it? – Martijn van den Bergh Jun 21 '21 at 13:11
  • Cast to `FormGroup` if you're sure that your `AbstractControl | null` is a `FormGroup`. – Lazar Ljubenović Jun 21 '21 at 16:49
  • I am not really sure where i should do that? I can't do that in the template. Should I make a new property and cast it there? – Martijn van den Bergh Jun 22 '21 at 06:33
  • @MartijnvandenBergh I know it's late but just for future reference you can add getter `get formGroupControl(): FormGroup { return this.controlContainer.control as FormGroup; }` Then use it on template `
    ...
    `
    – Liky Mar 26 '22 at 10:39
  • saved my day!!!!! – Mikael Boff Apr 05 '22 at 18:34
5

Angular forms doesn't have concept for group name as for form control name. However you can quite easily workaround this by wrapping the child template in a form group.

Here is an example similar to the markup you've posted - https://plnkr.co/edit/2AZ3Cq9oWYzXeubij91I?p=preview

 @Component({
  selector: 'address-form-group',
  template: `
    <!-- child template -->
    <ng-container [formGroup]="group.control.get(groupName)">
      <input formControlName="country">
      <input formControlName="state">
      <input formControlName="city">
      <input formControlName="street">
      <input formControlName="building">
    </ng-container>
  `
})
export class AddressFormGroupComponent  { 
  @Input() public groupName: string;

  constructor(@SkipSelf() public group: ControlContainer) { }
}

@Component({
  selector: 'my-app',
  template: `
    <!-- parent template -->
    <div [formGroup]="form">
      <input formControlName="username">
      <input formControlName="fullName">
      <input formControlName="password">
      <address-form-group groupName="address"></address-form-group>
    </div>
    {{form?.value | json}}
  `
})
export class AppComponent { 
  public form: FormGroup;

  constructor(private fb: FormBuilder) {
    this.form = this.fb.group({
      username: '',
      fullName: '',
      password: '',
      address: this.fb.group({
        country: '',
        state: '',
        city: '',
        street: '',
        building: '',
      })
    });
  }
}
rusev
  • 1,810
  • 2
  • 17
  • 14