Andy
Andy

Reputation: 8918

Angular: How to use a custom component to keep forms DRY?

I have a reactive form inside a component, like this:

<form [formGroup]="edit">
  <div class="form-group row">
    <label for="{{componentId}}_iptItemName" class="col-4 col-form-label">Name</label>
    <div class="col-8">
      <input id="{{componentId}}_iptItemName" type="text" class="form-control"
             formControlName="name"/>
    </div>
  </div>
  <!-- more form groups -->
</form>

Most of the fields look the same, so I would like to take the div.form-group and make a reusable component. The problem is, I can't get the formControlName into the nested component.

When I try just specifying formControlName on the new component, I get:

No value accessor for form control with name: 'name'

When I use an @Input() appFormControlName: string and pass that value to the input's [formControlName], I get:

formControlName must be used with a parent formGroup directive. You'll want to add a formGroup directive and pass it an existing FormGroup instance (you can create one in your class).

I've seen things where I could create an extra form group for each nested component, or register some kind of custom ControlValueAccessor to make it a "true" form control, but both of those seem overly complicated for what I'm doing. I just want to create a component to keep things DRY, not add complexity by creating custom form controls or kludge in single-value form groups.

Am I just missing something simple?

Upvotes: 1

Views: 768

Answers (3)

Andy
Andy

Reputation: 8918

I ended up using <ng-content> and just keeping the input in the form component.

<app-card-input label="Name">
  <input type="text" class="form-control" formControlName="name"/>
</app-card-input>

I also set up the component to create the ids on the fly:

let next = 0;

@Component({
  selector: 'app-card-input',
  templateUrl: './card-input.component.html',
  styles: []
})
export class CardInputComponent implements OnInit, AfterViewInit {
  @Input() label: string;
  componentId = `app-card-input-id-${next++}`;

  constructor(private elr: ElementRef) {
  }

  ngOnInit(): void {
  }

  ngAfterViewInit(): void {
    const ipt = this.elr.nativeElement.querySelector('input, select, textarea');
    ipt.setAttribute('id', this.componentId);
  }

}

I mostly decided to go this route because it gives me the flexibility to use different control types. I will need checkboxes, texareas, and other controls, and having separate ControlValueAccessors for each (or trying to cram them all under one) could get cumbersome.

Thanks @Petr and @Bojan for your answers. They definitely pointed me in the right direction.

Upvotes: 0

Petr Averyanov
Petr Averyanov

Reputation: 9486

Short answer just name component input differently e.g. @Input() myFormControlName

Long answer There is a difference between (as I call them) "custom component" and "custom control". When writing custom component avoid "standard" names for Input, Output such as ngModel, formControl, formControlName, etc. And usage looks like:

<my-component [myData]="data" (myDataChange)="change()">

or you may pass formControl/formGroup or whatever:

<my-component [myFormGroup]="someFormGroup" "myControlName"="someControlName">
// 'myControlName' IS @Input of component

As for "custom control" I'll give you https://github.com/angular/components/blob/master/src/material/checkbox/checkbox.ts as an example. And usage is:

<mat-checkbox [(ngModel)]="checked">Checked</mat-checkbox>

or

 <mat-checkbox formControlName="checkBoxName">Checked</mat-checkbox>
 // 'formControlName' IS NOT @Input of component

Upvotes: 1

Bojan Kogoj
Bojan Kogoj

Reputation: 5649

ControlValueAccessor seems complicated at first, but exactly what you need. It will solve many problems later. It's quite easy to use it, just a lot of "boilerplate".

Upvotes: 1

Related Questions