5
votes

Working with angular 7 and Bootstrap 4, I want to wrap my bootstrap 4 inputs in a custom component in order to reduce the boilerplate in my templates.

I want that the final main component look like:

<form [formGroup]="myForm" (submit)="submit(myForm.value)">

    <app-form-control label="Lastname" placeholder="Lastname" formControlName="lastName"></app-form-control>
    <app-form-control label="Firstname" placeholder="Firstname" formControlName="firstName"></app-form-control>

    <button class="pull-right" type="submit">
        SUBMIT
    </button>
    <button (click)="reset()">
        RESET
    </button>
</form>

Where my formGroup is created like this:

public createFormGroup() {
    return this.fb.group({
        firstName: [null, Validators.required],
        lastName: [null, Validators.required],
    });
}

The template of app-form-control should look like this:

<div class="form-group row">
  <label class="col-2 col-form-label">{{label}}</label>
  <div class="col-10">
    <input class="form-control" placeholder="{{placeholder}}" [formControlName]="formControlName" autocomplete="nope"/>
  </div>
</div>

But I don't know how to write the component (in TypeScript). How do I bind the outer formControlName attribute to the inner input field? How to make validation work?

2
You need to create a custom form control. Please check you need to implement ControlValueAccessor for form control. please check alligator.io/angular/custom-form-controlRANJIT PATRA
@RANJITPATRA, in this case you needn't make a ControlValueAccesor, see my answerEliseo

2 Answers

10
votes

The "key" is using viewProvider. You use a @Input set to give value to a formControl, see stackblitz. The "magic" is that if equal refered to formControl in the "children" or form.get('input1') in the parent

@Component({
  selector: 'app-form-control',
  template: `
  <div class="form-group row">
  <label class="col-2 col-form-label">{{label}}</label>
  <div class="col-10">
    <input class="form-control" placeholder="{{placeholder}}"
    [formControl]="formControl" autocomplete="nope"/>
  </div>
  </div>
  <!--you can control the properties of formControl-->
  {{formControl.valid}}{{formControl.touched}}}
`,
viewProviders: [{ provide: ControlContainer, useExisting: FormGroupDirective }]})
export class HelloComponent {

  formControl: FormControl;

  constructor(private parentF: FormGroupDirective) { }

  @Input() 
  set controlName(value) {
    this.formControl = this.parentF.form.get(value) as FormControl
  }

  @Input() label: string;
  @Input() placeholder: string;

}

And call the component this way:

<form [formGroup]="myForm" (submit)="submit(myForm.value)">
    <app-form-control label="Lastname" placeholder="Lastname" controlName="lastName"></app-form-control>
</form>

Update well, (after a year) take account the stackblitz was erroneous. when you (click) in buttons create a new Form:

 this.form=this.createForm({note:'lll'})

This "break" the relationship between the component and the form because the relation is about the older form -only change if change the @Input "nameControl". So the correct is use a setValue to give a new value to the form.

3
votes

You can implement by implementing ControlValueAccessor. Lets do the step by step process by building a TextBoxComponent.

step 1: Creating NG_VALUE_ACCESSOR for textfield as TEXTBOX_VALUE_ACCESSOR.

const TEXTBOX_VALUE_ACCESSOR = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => TextBoxComponent),
  multi: true,
}; 

step 2: implemnting ControlValueAccessor to our component TextBoxComponent.

export class TextBoxComponent implements ControlValueAccessor{
...
...
}

step 3: define unimplemnted methods of ControlValueAccessor. The detailed code of TextBoxComponent as below.

@Component({
  selector: "text-box",
  template: `

    <div class="form-group row">
      <label class="col-2 col-form-label">{{label}}</label>
      <div class="col-10">
        <input class="form-control" placeholder="{{placeholder}}" [(ngModel)]="inputValue" />
      </div>
    </div>
  `,
  providers: [TEXTBOX_VALUE_ACCESSOR],
})

export class TextBoxComponent implements ControlValueAccessor {
  private _inputValue: any = '';
  private _onTouchedCallback: () => {};
  private _onChangeCallback: (_:any) => {};

  @Input("label") label: string = "Your Label";
  @Input("placeholder") placeholder: string = "Your Placeholder";


  get inputValue(): any {
    return this._inputValue;
  }
  set inputValue(value: any) {
    if (value !== this._inputValue) {
      this._inputValue = value;
      this._onChangeCallback(value);
    }

    this._onTouchedCallback();

  }

  //From ControlValueAccessor interface
  writeValue(value: any) {
    this._inputValue = value;
  }

  //From ControlValueAccessor interface
  registerOnChange(fn: any) {
    this._onChangeCallback = fn;
  }

  //From ControlValueAccessor interface
  registerOnTouched(fn: any) {
    this._onTouchedCallback = fn;
  }
}

How to use :

<form [formGroup]="formGroup">
  <text-box formControlName="textboxControl" label="My Label" placeholder="My Placeholder"></text-box>

  <pre>{{formGroup.value | json}}</pre>
</form>

The complete code is available in stackblitz.