【问题标题】:Angular Custom focus Directive. Focus a form's first invalid inputAngular 自定义焦点指令。聚焦表单的第一个无效输入
【发布时间】:2019-10-24 19:38:21
【问题描述】:

如果输入无效,我创建了一个指令来聚焦输入

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());
    }
  }

}

我确实有一个带有两个输入的反应式表单,并且我已将指令应用于两个输入

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

提交表单后它工作正常,但我正在努力实现以下目标:

预期结果: - 提交表单后,如果两个输入都无效,则只关注第一个。

当前结果: - 如果两个输入都无效,则在提交表单后,第二个获得焦点。

我不知道如何指定“仅当它是第一个孩子时才这样做”,我已经尝试使用指令的选择器但没有运气。

有什么想法吗?

非常感谢。

【问题讨论】:

  • 这是我认为您的表单应该负责关注字段而不是字段本身的地方。因为它更有意义。取消焦点的唯一方法是让输入字段了解其他字段,这没有多大意义
  • @Guillermo,我改进了我的答案,将指令应用于表单

标签: javascript angular angular-reactive-forms angular-forms angular-validation


【解决方案1】:

要控制表单的输入,我认为更好的解决方案是使用 ViewChildren 来获取所有元素。因此,我们可以遍历这些元素并关注第一个。

所以,我们可以有一个辅助的简单指令:

@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) {  }
}

而且,在我们的组件中,我们有一些类似

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

您可以在stackblitz 中查看实际操作

更新总是可以改进的:

为什么不创建一个应用于表单的指令?

@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;
      }
    }
  }

所以,我们的 .html 就像

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

见stackblitz

更重要的是,如果我们使用作为选择器形式

@Directive({
  selector: 'form'
})

我们甚至可以移除表单中的focusOnError

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

更新 2 formGroup 与 formGroup 的问题。 已解决

NgControl 只考虑具有 [(ngModel)]、formControlName 和 [formControl] 的控件,所以。如果我们可以使用类似的形式

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)
    })
  })

我们可以使用如下形式:

<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>

我们在 .ts 中的位置

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

Update 3 用Angular 8 可以得到孩子的后代,所以很简单的写

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

【讨论】:

  • 感谢@eliseo 的精彩回答!
【解决方案2】:

好吧,只是为了好玩stackblitz。如果我们有一个 formControl,我们可以注入 ngControl,它就是控件本身。所以我们可以得到formGroup。我控制“提交”在 app.component 中进行变通

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

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

指令是这样的

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]);
  }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-24
    • 2021-07-26
    • 1970-01-01
    • 2016-01-02
    • 1970-01-01
    相关资源
    最近更新 更多