5

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?

1
  • I'm not an Angular expert, but I would add event listener in a wrapping element (which can be a directive) and relay on the event propagation. this way, wrapper can get all the inputs within itself, listen on the keyup event, check the source of the event (event.target) and apply your logic Commented Apr 12, 2020 at 12:24

4 Answers 4

11

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>
Sign up to request clarification or add additional context in comments.

3 Comments

I think I tried something like this but the srcElement was null I will try it again, thank you
I have tried this and nextElementSibling is null. Is it because they are nested within div?
Yes, you are right. They need to be next to each other if any other element comes in between it doesn't work. Look at my updated code that works with my approach. See if you can make it work like that.
2

 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>

1 Comment

it is everytime better to make few words with your answer :)
0

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.

Comments

0

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.