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

萝らか妹 提交于 2021-01-29 11:28:24

问题


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.


回答1:


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>



回答2:


well, just for funny stackblitz. If we has a formControl, we can inject ngControl that it's the control itself. So we can get the formGroup. I control the "submited" making a work-around in the app.component

<button (click)="check()">click</button>

  check() {
    this.submited = false;
    setTimeout(() => {
      this.submited = true;
    })
  }

The directive is like

export class FocusOnErrorDirective implements OnInit {
  @HostListener('input')
  onInput() {
    this._submited = false;
  }

  //I used "set" to avoid ngChanges, but then I need the "ugly" work-around in app.component
  @Input('focusOnError')
  set submited(value) {
    this._submited = value;
    if (this._submited) {  ((is submited is true
      if (this.control && this.control.invalid) { //if the control is invalid
        if (this.form) {
          for (let key of this.keys)  //I loop over all the
          {                           //controls ordered
            if (this.form.get(key).invalid) {  //If I find one invalid
              if (key == this.control.name) {  //If it's the own control
                setTimeout(() => {
                  this.el.nativeElement.focus()   //focus
                });
              }
              break;                           //end of loop
            }
          }
        }
        else
          this.el.nativeElement.focus()
      }
    }
  }
  private form: FormGroup;
  private _submited: boolean;
  private keys: string[];

  constructor(@Optional() private control: NgControl,  private el: ElementRef) {  }

  ngOnInit() {
    //in this.form we has the formGroup.
    this.form = this.control?this.control.control.parent as FormGroup:null;
    //we need store the names of the control in an array "keys"
    if (this.form)
    this.keys = JSON.stringify(this.form.value)
      .replace(/[&\/\\#+()$~%.'"*?<>{}]/g, '')
      .split(',')
      .map(x => x.split(':')[0]);
  }
}


来源:https://stackoverflow.com/questions/56525783/angular-custom-focus-directive-focus-a-forms-first-invalid-input

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!