Angular 2 - Move form validation to directive

617 Views Asked by At

I would like to move the form validation to directive and keep ngForm validation across all fields in the form. In the following example I move the input text field of the following validation (1. Works), to a directive. But when I move it to the directive, the model is updated correct, but the form.valid operation is not effected by the directive. The form is not valid, because input text has no value. When it is given a value the butten is activated and the form is valid.

1. Works:

    <form (ngSubmit)="onSubmit()" #aliasForm="ngForm">

                        <div class="form-group">
                            <label for="firstname">Fornavn</label>
                            <input type="text" class="form-control" id="firstname"
                                   required
                                   [(ngModel)]="model.primaryAlias.firstName" name="firstname"
                                   #name="ngModel">
                            <div [hidden]="name.valid || name.pristine"
                                 class="alert alert-danger">
                                Fornavn er påkrævet
                            </div>
                        </div>


                        <div class="form-group col-sm-6">
                            <p-dropdown [style]="{'width':'150px'}" [options]="genders" [(ngModel)]="model.primaryAlias.gender" name="genders" [required]="true"></p-dropdown>
                        </div>

Opret

Button is disabled

2. NOT working with directive (p-input):

<form (ngSubmit)="onSubmit()" #aliasForm="ngForm">
<p-input [(value)]="model.primaryAlias.firstName" directiveLabel="Fornavn"></p-input>
<div class="form-group">
    <p-dropdown [style]="{'width':'150px'}" [options]="genders" [(ngModel)]="model.primaryAlias.gender" name="genders" [required]="true"></p-dropdown>
</div>
<button type="submit" class="btn btn-success pull-right" [disabled]="!aliasForm.form.valid">Opret</button></form>

p-input.ts: enter image description here

p-input.html:

<div class="form-group">
<label for="firstname">Fornavn</label>
<input type="text" class="form-control" id="firstname"
       required
       [(ngModel)]="value" (ngModelChange)="onChange($event)" ngControl="value" name="firstname"
       #name="ngModel">
<div [hidden]="name.valid || name.pristine"
     class="alert alert-danger">
    Fornavn er påkrævet
</div>

enter image description here

How do I move the validation to a directive and keep the form handling, for the entire form intact ?

1

There are 1 best solutions below

1
On BEST ANSWER

I would either implement ControlValueAccessor for your custom directive like:

p-input.ts

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

@Component({
  selector: 'p-input',
  template: `
    <div class="form-group">
      <label for="firstname">{{directiveLabel}}</label>
      <input type="text" class="form-control" id="firstname"
             required
             [(ngModel)]="value" 
             (ngModelChange)="onModelChange($event)" #name="ngModel">
      <div [hidden]="name.valid || name.pristine"
           class="alert alert-danger">
        Fornavn er påkrævet
      </div>
    </div>
  `,
  providers: [INPUT_VALUE_ACCESSOR]
})
export class InputTestComponent implements ControlValueAccessor {
  onChange = (_: any) => {};
  onTouched = () => {};

  value: string;
  @Input() directiveLabel: string;


  @ViewChild('name') inputRef: ElementRef;

  constructor(private renderer: Renderer2) {}

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

  onModelChange(newValue: string) {
    this.value = newValue;
    this.onChange(this.value);
  }

  writeValue(val: any): void {
    this.value = val;
  }

  setDisabledState(isDisabled: boolean): void {
    this.renderer.setProperty(this.inputRef.nativeElement, 'disabled', isDisabled);
  }
}

parent.html

<p-input required 
  name="firstName" [(ngModel)]="model.primaryAlias.firstName" directiveLabel="Fornavn">
</p-input>

Stackblitz Example

or add inner input control to your form manually:

p-input.ts

@Component({
  selector: 'p-input',
  template: `
    <div class="form-group">
      <label for="firstname">{{directiveLabel}}</label>
      <input type="text" class="form-control" id="firstname"
             required
             [(ngModel)]="value" 
             (ngModelChange)="onChange($event)" name="firstname"
             #name="ngModel">
      <div [hidden]="name.valid || name.pristine"
           class="alert alert-danger">
        Fornavn er påkrævet
      </div>
    </div>
  `
})
export class InputTestComponent {
  @Input() value: string;
  @Input() directiveLabel: string;

  @Output() valueChange: EventEmitter<string> = new EventEmitter();

  @ViewChild(NgControl) inputControl: NgControl;

  constructor(@Optional() private controlContainer: ControlContainer) {}

  ngOnInit() {
    if(this.controlContainer) {
      this.controlContainer.formDirective.addControl(this.inputControl);
    }
  }

  onChange(newValue: string) {
    this.value = newValue;
    this.valueChange.emit(this.value);
  }
}

parent.html

<p-input [(value)]="model.primaryAlias.firstName" directiveLabel="Fornavn"></p-input>

Stackblitz Example