14
votes

I have a custom FormFieldComponent that encapsulates the HTML and error display logic for a form field:

@Component({
  selector: 'field',
  template: `
    <div class="form-group">
      <label class="control-label">{{label}}</label>
      <ng-content></ng-content>  <!-- Will display the field -->
      <!-- Here, put error display logic -->
    </div>
  `
})
export class FormFieldComponent {
  @Input() label: string;  // Field label
  @Input() theControl: FormControl;  // Current form control, required to display errors
}

In FormFieldComponent, I need an instance of the FormControl to display errors.

My form then looks like this:

<form [formGroup]="myForm">
  ...
  <field label="Title" [theControl]="myForm.get('title')">
    <input type="text" formControlName="title">
  </field>
  ...
</form>

But I'm not entirely satisfied with the above code. As you can see, I am specifying the field's key in two locations: in the [theControl] input property and in the formControlName directive.

The code would be more concise if I could just write:

<field label="Title">
  <input type="text" formControlName="title">
</field>

Notice how the [theControl] input property is gone. The FieldComponent should be able to get a hold of the FormControl instance it contains, but how?

I have tried using the @ContentChildren decorator to query the component's template for FormControl directives, but it doesn't work:

export class FormFieldComponent {
  @ContentChildren(FormControlDirective) theControl: any;
}

Another option would be to pass the field's key as an input to FormFieldComponent and then let the component use that key to:

  • Programmatically apply the formControlName directive to the field it contains.
  • Get a hold of its parent <form>, access the corresponding FormGroup instance, and extract the FormControl instance from that.
3
I don't understand what you are trying to do. I have many reactive forms in my app and never once used [control], I only use formControlName.John Baird
@John: [control] is not an Angular directive, it's just the name I gave to my input property. I'll rename [control] to [theControl] to make it more obvious. So, do you know how a component can get a hold of the FormControl instances that appear in its template?AngularChef
Can't you add #formInstance to the <form> tag?John Baird
My question is why do you want to do that as long as you can create your own custom input by implementing ControlValueAccessor ?n00dl3
Love to see that solution n00dl3John Baird

3 Answers

11
votes

short answer: You can't

You just can't. (Well, maybe be you can but it will be hacky !)

long answer: you can't, but...

FormControl is not injectable. Directives are injectable, but, you would have to deal with formControlName,ngModel,formControl, etc and they wouldn't be accessible from the wrapping component but its children...

For your case you could try with @ContentChildren(FormControlName) theControl: any; as there is no FormControlDirective implied in your code, but you wouldn't be able to access the FormControl anyway (property _control is internal, soo it would be a hack)...

So you should stick to managing your errors from the component dealing with the FormGroup.

BUT if you want to display a custom input (that won't display error message as is but will be able to show this input is in error state (the host element will get the ng-valid, ng-invalid classes, so it's just a matter of style), you can do this by implementing ControlValueAccessor.

A bridge between a control and a native element.

A ControlValueAccessor abstracts the operations of writing a new value to a DOM element representing an input control.

It means directives/components implementing this interface can be used with ngModel, formControl,etc...

eg: <my-component [(ngModel)]="foo"></my-component>

it is not the exact reproduction of your problem, but this implementation solved the same kind of problem for me :

export const INPUT_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => InputComponent),
  multi: true
};

@Component({
  selector: "field",
  template: `<!--put anything you want in your template-->
            <label>{{label}}</label>
            <input #input (input)="onChange($event.target.value)" (blur)="onTouched()" type="text">`,
  styles: [],
  providers: [INPUT_VALUE_ACCESSOR]
})
export class InputComponent implements ControlValueAccessor {
  @ViewChild("input")
  input: ElementRef;
  @Input()
  label:string;
  onChange = (_: any) => { };
  onTouched = () => { };

  constructor(private _renderer: Renderer) { }

  writeValue(value: any): void {
    const normalizedValue = value == null ? "" : value;
    this._renderer.setElementProperty(this.input.nativeElement, "value", normalizedValue);
  }

  registerOnChange(fn: (_: any) => void): void {
      this.onChange = fn;
  }
  registerOnTouched(fn: () => void): void { this.onTouched = fn; }

  setDisabledState(isDisabled: boolean): void {
    this._renderer.setElementProperty(this.input.nativeElement, "disabled", isDisabled);
  }
}

then you can just :

<field label="Title" formControlName="title"></field>
6
votes

You can get ahold of the Form Control Name instance via:

@Component({
  selector: 'field',
  templateUrl: './field.component.html',
  styleUrls: ['./field.component.scss']
})
export class FieldComponent implements AfterContentInit {
  @Input()
  public label: string;

  @ContentChild(FormControlName)
  public controlName: FormControlName;

  public ngAfterContentInit(): void {
    console.log(this.controlName.control);
  }
}
0
votes

Not sure if things have changed, but I found a seemingly supported way to do this here

Essentially, you can use the @ContentChild decorator and depending on your form control or how you have it linked into your template control, use the proper directive as the selector for the decorator.

For example, if I am projecting an input that uses the case as you have, the [formControl], you are in the same boat as the author of that GitHub ticket. For me, I was using the 'formGroupName' directive, and as confusing as it was, the correct usage of the directive for that worked out to be @ContentChild(FormControlName) control;

HTH!