Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get a hold of a FormControl instance by querying a component's template

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.
like image 475
AngularChef Avatar asked Nov 18 '16 10:11

AngularChef


People also ask

How do I get FormControl values?

To fetch the value of a form control, we have to use value property on the instance of FormControl in our class. In the same way we can fetch the value in HTML template. city = new FormControl('Noida'); console.

What is difference between FormBuilder and FormControl?

In Angular, a reactive form is a FormGroup that is made up of FormControls. The FormBuilder is the class that is used to create both FormGroups and FormControls.

Which is a collection of FormControl in Angular?

What is a form group in Angular? Form groups wrap a collection of form controls. Just as the control gives you access to the state of an element, the group gives the same access but to the state of the wrapped controls.


2 Answers

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>
like image 147
n00dl3 Avatar answered Sep 21 '22 15:09

n00dl3


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);
  }
}
like image 29
Steve Brush Avatar answered Sep 21 '22 15:09

Steve Brush