Angular | Automatically focus next input on max length

Viewed 7197

I am trying to come up with a nicer way to be able to focus the next input on Angular without having to manually enter what input I want to focus.

This is my html that I currently have...

<div class="mb-2 digit-insert d-flex align-items-center">
  <div class="confirmation-group d-flex">
    <div class="digit-wrapper">
      <input #digitOne type="text" (paste)="onDigitPaste($event)" maxlength="1"
        (keyup)="onDigitInput($event, null, digitTwo)" />
    </div>
    <div class="digit-wrapper">
      <input #digitTwo type="text" maxlength="1" (keyup)="onDigitInput($event, digitOne, digitThree)" />
    </div>
    <div class="digit-wrapper">
      <input #digitThree type="text" maxlength="1" (keyup)="onDigitInput($event, digitTwo, digitFour)" />
    </div>
  </div>
  <span class="confirmation-divider m-3">-</span>
  <div class="confirmation-group d-flex">
    <div class="digit-wrapper">
      <input #digitFour type="text" maxlength="1" (keyup)="onDigitInput($event, digitThree, digitFive)" />
    </div>
    <div class="digit-wrapper">
      <input #digitFive type="text" maxlength="1" (keyup)="onDigitInput($event, digitFour, digitSix)" />
    </div>
    <div class="digit-wrapper">
      <input #digitSix type="text" maxlength="1" (keyup)="onDigitInput($event, digitFive, null)" />
    </div>
  </div>
</div>

As you can see I have a key up event which I pass what I want to focus on whether the input is inputted.

This is the typescript markup...

onDigitInput(event: any, previousElement: any, nextElement: any): void {
    if (event.code !== 'Backspace' && nextElement !== null) {
        nextElement.focus();
    }

    if (event.code === 'Backspace' && previousElement !== null) {
        previousElement.focus();
        previousElement.value = '';
    }
}

I was wondering if there is any way to do this with a directive or just something a bit nicer than what this is now?

4 Answers

Change your input element to this:

<input #digitSix type="text" maxlength="1" (keyup)="onDigitInput($event)" />

and then change your function implementation to this:

onDigitInput(event){

   let element;
   if (event.code !== 'Backspace')
        element = event.srcElement.nextElementSibling;

    if (event.code === 'Backspace')
        element = event.srcElement.previousElementSibling;

    if(element == null)
        return;
    else
        element.focus();
}

Results in much cleaner code.

Works with this code:

<div class="mb-2 digit-insert d-flex align-items-center">
  <div class="confirmation-group d-flex">
      <input #digitOne type="text" (paste)="onDigitPaste($event)" maxlength="1"
        (keyup)="onDigitInput($event)" />
      <input #digitTwo type="text" maxlength="1" (keyup)="onDigitInput($event)" />
      <input #digitThree type="text" maxlength="1" (keyup)="onDigitInput($event)" />
  </div>
  <span class="confirmation-divider m-3">-</span>
  <div class="confirmation-group d-flex">
      <input #digitFour type="text" maxlength="1" (keyup)="onDigitInput($event)" />
      <input #digitFive type="text" maxlength="1" (keyup)="onDigitInput($event)" />
      <input #digitSix type="text" maxlength="1" (keyup)="onDigitInput($event)" />
  </div>
</div>

 ngOnInit(): void {
    this.verifyCode = this.formBuilder.group({
      code1: ['', Validators.required],
      code2: ['', Validators.required],
      code3: ['', Validators.required],
      code4: ['', Validators.required],
    });
    }
  nextStep(event, step: number): void {
    if (this.verifyCode.valid) {
      this.onSubmit()
    }
    const prevElement = document.getElementById('code' + (step - 1));
    const nextElement = document.getElementById('code' + (step + 1));
    console.log(event)
    if (event.code == 'Backspace' && event.target.value === '') {
      event.target.parentElement.parentElement.children[step - 2 > 0 ? step - 2 : 0].children[0].value = ''
      if (prevElement) {
        prevElement.focus()
        return
      }
    } else {
      if (nextElement) {
        nextElement.focus()
        return
      } else {

      }
    }


  }

  paste(event) {
    let clipboardData = event.clipboardData;
    let pastedText = clipboardData.getData('text');
    this.verifyCode.setValue({
      code1: pastedText.charAt(0),
      code2: pastedText.charAt(1),
      code3: pastedText.charAt(2),
      code4: pastedText.charAt(3)
    });
    this.onSubmit()
    debugger
  }

  focused(step) {
    if (step === 2) {
      if (this.verifyCode.controls.code1.value === '') {
        document.getElementById('code1').focus();
      }
    }
    if (step === 3) {
      if (this.verifyCode.controls.code1.value === '' || this.verifyCode.controls.code2.value === '') {
        document.getElementById('code2').focus();
      }
    }

    if (step === 4) {
      if (this.verifyCode.controls.code1.value === '' || this.verifyCode.controls.code2.value === '' || this.verifyCode.controls.code3.value === '') {
        document.getElementById('code3').focus();
      }
    }
  }
  
    onSubmit(): void {
    this.submitted = true;
    if (this.verifyCode.invalid) {
      return;
    }
    // ...
    }
<div class="d-inline-block" style="width: 200px;">
                <div class="row ltr">
                  <div class="col-3 px-2">
                    <input type="text" class="form-control text-center ltr p-0" id="code1" [ngClass]="{'border-danger': submitted && f.code1.errors}" (keyup)="nextStep($event,1)" (focus)="focused(1)" formControlName="code1" maxlength="1"
                           (paste)="paste($event)" autofocus>
                  </div>
                  <div class="col-3 px-2">
                    <input type="text" class="form-control text-center ltr p-0" id="code2" [ngClass]="{'border-danger': submitted && f.code2.errors}" (keyup)="nextStep($event,2)" (focus)="focused(2)" formControlName="code2" maxlength="1">
                  </div>
                  <div class="col-3 px-2">
                    <input type="text" class="form-control text-center ltr p-0" id="code3" [ngClass]="{'border-danger': submitted && f.code3.errors}" (keyup)="nextStep($event,3)" (focus)="focused(3)" formControlName="code3" maxlength="1">
                  </div>
                  <div class="col-3 px-2">
                    <input type="text" class="form-control text-center ltr p-0" id="code4" [ngClass]="{'border-danger': submitted && f.code4.errors}" (keyup)="nextStep($event,4)" (focus)="focused(4)" formControlName="code4" maxlength="1">
                  </div>
                </div>

Here I have used a host listener to capture the keyboard events and a custom directive to handle the focus.

  1. focus.directive.ts

This has an Input variable which is a type of EventEmitter which accepts a string parameter. That parameter will hold the element id to be focused.

import { Directive, EventEmitter, Input, OnInit, Renderer2 } from '@angular/core';

@Directive({
 selector: '[appFocus]'
})
export class FocusDirective implements OnInit {
@Input('appFocus') eventEmitter: EventEmitter<string>;

  constructor(private renderer: Renderer2) { }

  ngOnInit() {
   this.eventEmitter.subscribe(elementId => {
    try {
     this.renderer.selectRootElement(elementId).focus();
    } catch (ex) {
     // If the element doesn't exist or if the element disappears when this called then no need to do anything
    }
   });
  }
}
  1. inputs.component.ts

    import { Component, HostListener, EventEmitter } from '@angular/core';
    
    @Component({
     selector: 'app-inputs',
     templateUrl: './inputs.component.html'
    })
    
    export class InputsComponent {
      inputFocusEmitter = new EventEmitter<string>();
    
      @HostListener('window:keydown', ['$event'])
      HandlKeyEvents(event) {
       const key = event.key.toLocaleLowerCase();
       const inputIds = ['digitOne', 'digitTwo', 'digitThree'];
       let elementId = '';
    
       switch (key) {
        case 'backspace':
         elementId = event.target.id;
         const prevInputIndex = inputIds.indexOf(elementId) - 1;
         if (prevInputIndex >= 0) {
          this.inputFocusEmitter.emit(`#${inputIds[prevInputIndex]}`);
         }
         break;
    
       default:
        elementId = event.target.id;
        const index = inputIds.indexOf(elementId);
        const nextInputIndex = index + 1;
        if (nextInputIndex > 0 && nextInputIndex < inputIds.length) {
         this.inputFocusEmitter.emit(`#${inputIds[nextInputIndex]}`);
        }
        break;
      }
    }
    
  2. inputs.component.html

Here the focus directive is bound with each input elements

 <div class="mb-2 digit-insert d-flex align-items-center">
      <div class="confirmation-group d-flex">
          <div class="digit-wrapper">
              <input #digitOne type="text" maxlength="1"  [appFocus]="inputFocusEmitter"/>
          </div>
          <div class="digit-wrapper">
              <input #digitTwo type="text" maxlength="1"  [appFocus]="inputFocusEmitter"/>
          </div>
          <div class="digit-wrapper">
              <input #digitThree type="text" maxlength="1" [appFocus]="inputFocusEmitter"/>
          </div>
      </div>
 </div> 

Hope this will help.

I have used jquery, things are simpler this way and less error prone.

move-focus-to-next.directive.ts

import { Directive, HostListener } from '@angular/core';
import * as $ from 'jquery';

@Directive({
    selector: '[moveFocusToNext]'
})
export class MoveFocusToNext {
    @HostListener('input', ['$event']) onInputChange(event) {
        if (event.target && event.target.maxLength === event.target.value.length) {
            $(":input")[$(":input").index(document.activeElement) + 1].focus();
        }
    }
}

my-component.html

<input [(ngModel)]="myModal" moveFocusToNext maxlength="10">
Related