skorenb
skorenb

Reputation: 674

Forwarding formControlName to inner component in Angular with Reactive Forms

I have one custom control component <some-input> that i wrapped to <ext-some-input>. SomeInput is encapsulated, has own API and supports reactive forms. ExtSomeInput is created as high-level wrapper over SomeInput.

I have following html:

<form [formGroup]="form">
    <ext-some-input formControlName="name">
</form>

and ExtSomeInput's html:

<some-input formControlName="_???_"></some-input>

The question is how to forward formControlName to inner SomeInput component? I need to tie the form and inner formControl up. Is this possible?

EDITED:

I've created stackblitz project with this issue: here

Upvotes: 31

Views: 15726

Answers (6)

Camille
Camille

Reputation: 2531

I had almost same scenario and issues. My relieve came with this article by Todd Motto.

His approach is to give formGroup as parent to custom input component. Then, from this component, you can bind control (validator) and formControlName (value) to input tag.

As usually, example speaks more by himself.

Form component ts

// Reactive Forms
form: FormGroup;    

constructor(private formBuilder: FormBuilder) { }

ngOnInit() {
  // Form structure and validators
  this.form = this.formBuilder.group({
    'user' : this.formBuilder.group({
      'username' : ['', Validators.required],
      'email' : ['', [Validators.required, Validators.email]]
    }),
    'identity' : this.formBuilder.group({
      'firstname' : ['', Validators.required],
      'lastname'  : ['', Validators.required],
      'address' : this.formBuilder.group({
        'street' : ['', Validators.required],
        'city'  : ['', Validators.required],
      })
    })
  });        
}

onSubmit() {
    // Get object with same structure as form but only with values
    console.log(this.form.value);
    alert('Form is ' + (this.form.invalid ? 'invalid' : 'valid'));
}

Form component html

<form [formGroup]="form" (ngSubmit)="onSubmit()">
    <form-text [formGroupParent]="form.get(['user'])"
               [formGroupControlName]="'username'">
    </form-text>
    <form-text [formGroupParent]="form.get(['user'])"
               [formGroupControlName]="'email'">
    </form-text>
    <hr>
    <form-text [formGroupParent]="form.get(['identity'])"
               [formGroupControlName]="'firstname'">
    </form-text>
    <form-text [formGroupParent]="form.get(['identity'])"
               [formGroupControlName]="'lastname'">
    </form-text>
    <hr>
    <form-text [formGroupParent]="form.get(['identity','address'])"
               [formGroupControlName]="'street'">
    </form-text>
    <form-text [formGroupParent]="form.get(['identity','address'])"
               [formGroupControlName]="'city'">
    </form-text>
    <button type="submit">Submit</button>
</form>

Custom input component ts (form-text)

// Needed to bind formControlName
@Input() formGroupParent: FormGroup;
@Input() formGroupControlName: string;
// FormControl store validators
control: FormControl;

ngOnInit() {
    // Fetch Form control (validator) from FormGroup parent
    this.control = <FormControl>this.formGroupParent.get(this.formGroupControlName);
}

Custom input component html (form-text)

<ng-container [formGroup]="formGroupParent">
  <label>{{formGroupControlName}}</label> 
  <input type="text" formControlName="{{formGroupControlName}}">
</ng-container>

Upvotes: 6

yurzui
yurzui

Reputation: 214315

Your inner component can take @Input controlName but it won't work out of the box:

Error: formControlName must be used with a parent formGroup directive.

In order to tie your control with parent FormGroup you can define viewProvider as follows:

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

@Component({
  ...
  viewProviders: [
    {
      provide: ControlContainer,
      useExisting: FormGroupDirective
    }
  ]
})
export class DateWrapperComponent implements OnInit {
    @Input() controlName: string;
}

Forked Stackblitz

In case you don't know which exactly directive is provided as a ControlContainer(FormGroupDirective, FormGroupName or FormArrayName) you can use more generic approach:

viewProviders: [
  {
    provide: ControlContainer,
    useFactory: (container: ControlContainer) => container,
    deps: [[new SkipSelf(), ControlContainer]]
  }
]

Demo

Upvotes: 64

maxime1992
maxime1992

Reputation: 23813

Hey making a second answer here because we came up at work with a generic approach that we released as an open source library =).

You can find it here: https://github.com/cloudnc/ngx-sub-form

It should help you manage: - nested forms - forms with polymorphic data - better typings

Everything is explained into the readme and a complete example is provided into the /src folder. (lib is actually in projects/ngx-sub-form).

Live demo available here too: https://cloudnc.github.io/ngx-sub-form

Upvotes: 1

Sunil
Sunil

Reputation: 11241

You can create a @Input in component ext-some-input

ext-some-input.ts

@Input 
formControlName

ext-some-input.html

<some-input [formControlName]="formControlName"></some-input>

Upvotes: 0

Abraham Al-Dabbagh
Abraham Al-Dabbagh

Reputation: 316

I've run into a similar scenario where I did not want to pass formControlName to the wrapper component. My preferred way to solve this is to just reuse the formGroup passed from the parent form in the wrapper component. You can do this by injecting ControlContainer in the constructor of ExtSomeInput component:

ParentForm.component.html

<form [formGroup]="form">
  <ext-some-input controlName="name">
</form>

ExtSomeInput.component.ts

// pass the formControlName as string to wrapper
@Input public controlName: string;
public form: FormGroup;

constructor(public controlContainer: ControlContainer) {}

ngOnInit() {
  this.form = <FormGroup>this.controlContainer.control;
}

ExtSomeInput.component.html

// use ng-container to omit this from the DOM
<ng-container [formGroup]="form">
  // wrapper markup here
  <some-input [formControlName]="controlName"></some-input>
</ng-container>

Upvotes: 3

maxime1992
maxime1992

Reputation: 23813

I think that you're actually looking something a bit more complex than an input.

If you want to create a customized input you can create a class which is going to be a ControlValueAccessor (custom form control).

Please check the following article where it's really well explained:

https://alligator.io/angular/custom-form-control

Upvotes: -2

Related Questions