Loop Through a Form, Focusing on Required (yet invalid) Fields

270 Views Asked by At

StackBlitz here. Note the addShortcut() function is for creating the hotkeys as described below.

I am wanting to dynamically move through a form, focusing only on the invalid fields, but allowing the user to traverse through these invalid fields as necessary.

For example, if I have a form where the address, phone, and email inputs are the only required fields, the user can push a button and cycle through these specific fields.

Now, say they are moving through the 3 fields, and they fill in the address field. This is no longer invalid, so now the form should loop/cycle through the phone and email input fields only.

I am using a custom hotkey function so that when a user presses alt + a, the focus is set. The user can press this again, and focus goes to the next required field.

When the app is run ngOnInit() builds the form, and triggers a function to find invalid fields:

findInvalidFields() {
  this.invalidFields = [];
  const controls = this.myForm.controls;

  for (const name in controls) {
    if (controls[name].invalid) {
      this.invalidFields.push(name);
    }
  }
}

this.invalidFields is now an array of only the invalid fields:

[
  "customerAddress",
  "customerPhone",
  "customerEmail"
]

The hotkey is pressed, and I run a function to set the focus, then shift the array so it goes to the next invalid item.:

... {
  this.setFocus(this.invalidFields[0]);
  this.invalidFields.push(this.invalidFields.shift());
}

The problem is now even if I fill in a form, the focus will move back and forth through the entire original list -- this is expected because the invalid fields are only being found OnInit... Of course, I can run findInvalidFields() every time the hotkey is pressed, so my hotkey function looks like this:

... {
  this.findInvalidFields();
  this.setFocus(this.invalidFields[0]);
  this.invalidFields.push(this.invalidFields.shift());
}

This works in getting the correct invalid fields, but no longer "cycles" through the invalid fields since the form returns a specific order of controls each time the function is run therefore getting stuck on an input until it is no longer invalid.

Is there a way to dynamically "cycle" through the input fields without getting "stuck" on invalid fields?

Stackblitz.

1

There are 1 best solutions below

0
David Kidwell On

Here is my quick-and-dirty solution. It can likely be cleaned up quite a bit but it seems to work.

First we need to listen for the relevant keypress and fire our function to update the focus, I used the right arrow key to simplify it a bit,

  @HostListener('window:keyup', ['$event'])
  keyEvent(event: KeyboardEvent) {
    if (event.key === 'ArrowRight') {
      this.focusNext();
    }
  }

Then the focusNext() function looks like this,

  private focusNext(): void {
    let focusNextControl: boolean = false;
    let focusSet: boolean = false;
    let firstKey: string;

    let currentControlInvalid: boolean;
    Object.keys(this.myForm.controls).forEach((key, index) => {
      currentControlInvalid = this.myForm.controls[key].invalid;
      //Find the first invalid control, we will need it later in the case that the active focus is on the last control on the form
      if(!firstKey && currentControlInvalid){
        firstKey = key;
      }
      //If the current control is invalid 
      //AND the previous control is the currently focused one 
      //AND we haven't already set the focus.. Then set the focus on this control.
      if(currentControlInvalid && focusNextControl && !focusSet){
        this.elRef.nativeElement.querySelector('[formcontrolname = "' + key + '"]').focus();
        focusSet = true;
      }
      else if(this.myForm.controls[key] == this.formFocus){
        focusNextControl = true;
      }
      else if(!focusSet && focusNextControl && index == Object.keys(this.myForm.controls).length - 1){
        //If we are on the last control and we need to set the 'next' control 
        //AND we haven't already focused on a control
        //Then we need to set the focus to the first invalid control we found
        this.elRef.nativeElement.querySelector('[formcontrolname = "' + firstKey + '"]').focus();
      }
    });
  }

Where this.formFocus is a field that we use to track the actively focused form control,

  private formFocus: FormControl;
  public onControlFocus(control: string): void {
    this.formFocus = this.myForm.controls[control] as FormControl;
  }

Then in your template you will need to bind the (focus) event on each of your inputs to onControlFocus() and pass in the formcontrolname of the input,

    <input
        formControlName="firstName" 
        type="text" class="form-control" id="firstName"
        (focus)="onControlFocus('firstName')"
    >

Most of this solution is inside the focusNext() function and the way I stored the first invalid control is a little messy. If you wanted to clean it up you could try storing the form controls in a circularly linked list that would let you iterate right back to the first invalid element. That way you could avoid some of the tracking variables.