Search code examples
javascriptangularangular-reactive-formsangular-formsangular-validation

Angular Custom focus Directive. Focus a form's first invalid input


I have created a directive to focus an input if it's invalid

import { Directive, Input, Renderer2, ElementRef, OnChanges } from '@angular/core';

@Directive({
  // tslint:disable-next-line:directive-selector
  selector: '[focusOnError]'
})
export class HighlightDirective implements OnChanges {
  @Input() submitted: string;

  constructor(private renderer: Renderer2, private el: ElementRef) { }

  ngOnChanges(): void {
    const el = this.renderer.selectRootElement(this.el.nativeElement);
    if (this.submitted && el && el.classList.contains('ng-invalid') && el.focus) {
      setTimeout(() => el.focus());
    }
  }

}

I do have a reactive form with two inputs, and I've applied the directive to both inputs

<form>
  ...
  <input type="text" id="familyName" focusOnError />
  ...
  <input type="text" id="appointmentCode" focusOnError />
  ...
</form>

After submitting the form it works fine, but what I'm struggling to achieve is the following:

Expected result: - After submitting the form if both inputs are invalid, only the first one should be focused.

Current result: - After submitting the form if both inputs are invalid, the second one gets focused.

I don't know how to specify "only do this if it's the first child", I've tried with the directive's selector with no luck.

Any ideas?

Thanks a lot in advance.


Solution

  • To control the inputs of a Form, I think the better solution is use ViewChildren to get all elements. So, we can loop over this elements and focus the first.

    So, we can has a auxiliar simple directive :

    @Directive({
      selector: '[focusOnError]'
    })
    export class FocusOnErrorDirective  {
      
      public get invalid()
      {
        return this.control?this.control.invalid:false;
      }
      public focus()
      {
         this.el.nativeElement.focus()
      }
      constructor(@Optional() private control: NgControl,  private el: ElementRef) {  }
    }
    

    And, in our component we has some like

    @ViewChildren(FocusOnErrorDirective) fields:QueryList<FocusOnErrorDirective>
    check() {
        const fields=this.fields.toArray();
        for (let field of fields)
        {
          if (field.invalid)
          {
            field.focus();
            break;
          }
        }
      }
    

    You can see in action in the stackblitz

    UPDATE always the things can improve:

    Why not create a directive that applied to the form?

    @Directive({
      selector: '[focusOnError]'
    })
    export class FocusOnErrorDirective {
    
      @ContentChildren(NgControl) fields: QueryList<NgControl>
    
      @HostListener('submit')
      check() {
        const fields = this.fields.toArray();
        for (let field of fields) {
          if (field.invalid) {
            (field.valueAccessor as any)._elementRef.nativeElement.focus();
            break;
          }
        }
      }
    

    So, our .html it's like

    <form [formGroup]="myForm" focusOnError>
      <input type="text" formControlName="familyName" />
      <input type="text" formControlName="appointmentCode" />
      <button >click</button>
    </form>
    

    See the stackblitz

    Even more, if we use as selector form

    @Directive({
      selector: 'form'
    })
    

    Even we can remove the focusOnError in the form

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

    Update 2 Problems with formGroup with formGroup. SOLVED

    NgControl only take account the controls that has [(ngModel)], formControlName and [formControl], so. If we can use a form like

    myForm = new FormGroup({
        familyName: new FormControl('', Validators.required),
        appointmentCode: new FormControl('', Validators.required),
        group: new FormGroup({
          subfamilyName: new FormControl('', Validators.required),
          subappointmentCode: new FormControl('', Validators.required)
        })
      })
    

    We can use a form like:

    <form [formGroup]="myForm"  focusOnError (submit)="submit(myForm)">
      <input type="text" formControlName="familyName" />
      <input type="text" formControlName="appointmentCode" />
      <div >
        <input type="text" [formControl]="group.get('subfamilyName')" />
        <input type="text" [formControl]="group.get('subappointmentCode')" />
      </div>
      <button >click</button>
    </form>
    

    where in .ts we has

    get group()
      {
        return this.myForm.get('group')
      }
    

    Update 3 with Angular 8 you can get the descendants of the children, so it's simply write

     @ContentChildren(NgControl,{descendants:true}) fields: QueryList<NgControl>