Focus next input once reaching maxlength value
Asked Answered
I

11

49

How can I focus the next input once the previous input has reached its maxlength value?

a: <input type="text" maxlength="5" />
b: <input type="text" maxlength="5" />
c: <input type="text" maxlength="5" />

If a user pastes text that is greater than the maxlength, ideally it should spill into the next input.

jsFiddle: http://jsfiddle.net/4m5fg/1/

I must stress that I do not want to use a plugin, as I'd much rather learn the logic behind this, than use something that already exists. Thanks for understanding.

Ineducable answered 24/3, 2013 at 5:54 Comment(2)
@Musa His purpose is to "learn the logic behind this".Preterition
Possible duplicate of Moving a focus when the input text field reaches a max lengthLipread
V
71

No jQuery used and is a very clean implementation:

  • Reads from the maxlength attribute.
  • Scales to any number of inputs inside of your container.
  • Automatically finds the next input to focus.
  • No jQuery.

http://jsfiddle.net/4m5fg/5/

<div class="container">
a: <input type="text" maxlength="5" />
b: <input type="text" maxlength="5" />
c: <input type="text" maxlength="5" />
</div>

..

var container = document.getElementsByClassName("container")[0];
container.onkeyup = function(e) {
    var target = e.srcElement || e.target;
    var maxLength = parseInt(target.attributes["maxlength"].value, 10);
    var myLength = target.value.length;
    if (myLength >= maxLength) {
        var next = target;
        while (next = next.nextElementSibling) {
            if (next == null)
                break;
            if (next.tagName.toLowerCase() === "input") {
                next.focus();
                break;
            }
        }
    }
    // Move to previous field if empty (user pressed backspace)
    else if (myLength === 0) {
        var previous = target;
        while (previous = previous.previousElementSibling) {
            if (previous == null)
                break;
            if (previous.tagName.toLowerCase() === "input") {
                previous.focus();
                break;
            }
        }
    }
}
Volnay answered 24/3, 2013 at 6:10 Comment(5)
This only works for the first container in the page. It's relatively trivial to modify it to work for all of them, but it's worth pointing out in case novices come to use it.Schick
var target = e.srcElement || e.target; helped me use your solution. Thank you!Electrocardiograph
If you add maxlength = 2 you cannot edit the input fields.Dupaix
I had to change maxLength to target.maxLength instead of parseInt(target.attributes["maxlength"].value, 10) in order to make it work. However, I'm using the proper listener instead of redefining the onkeyup function: container.addEventListener('keyup', moveToNext) (where moveToNext is the function from the above answer).Revelry
Just now , jsfiddle.net/nsvbtkzqKingsley
S
25

You can watch for input in the fields and test its value:

$("input").bind("input", function() {
    var $this = $(this);
    setTimeout(function() {
        if ( $this.val().length >= parseInt($this.attr("maxlength"),10) )
            $this.next("input").focus();
    },0);
});

Working demo.

The setTimeout is there to ensure the code will only run after the input is completed and the value updated. Binding input ensures most types of input will trigger the event, including key presses, copy/paste (even from mouse) and drag & drop (though in this case, the latter won't work, since the focus was on the draggable, not the droppable).

Note: on some older browsers, you might also need to bind propertychange.


If a user pastes text that is greater than the maxlength, ideally it should spill into the next input.

To do that, you might need to remove the maxlength attribute using JavaScript (to be able to capture the full input), and implement that functionality yourself. I made a small example, relevant parts below:

$("input").each(function() {
    var $this = $(this);
    $(this).data("maxlength", $this.prop("maxlength"));
    $(this).removeAttr("maxlength");
})

This removes the attribute, but saves it in data, so you can access it later.

function spill($this, val) {
    var maxlength = $this.data("maxlength");
    if ( val.length >= maxlength ) {
        $this.val(val.substring(0, maxlength));
        var next = $this.next("input").focus();
        spill(next, val.substring(maxlength));
    }
    else
        $this.val(val);
}

Here the max length logic is reintroduced in JavaScript, as well as getting the "discarded" part and using it in a recursive call to spill. If there's no next element, the call to data will return undefined and the loop will stop, so the input will be truncated in the last field.

Synergism answered 24/3, 2013 at 5:58 Comment(7)
your fiddle works perfect and +1 for that but, is binding propertyChange mandatory ? it seems to work fine even without thatRobbie
@Bingo According to this answer, input is for Firefox and propertychange for the others, but things might have changed since then (i.e. maybe every browser supports input now). The only way to be sure is testing on different environments. (P.S. it's propertychange, not propertyChange - I already corrected my answer)Synergism
This should be the correct answer. If you add maxlength = 2, on the accepted answer you cannot edit the inputs but in this one you can!Dupaix
this is a good alternative but have problems, for example if you have each input in a different div doesn't workIllegality
Ya this doesn't work if you have the input fields in seperate divs or if you have a div say containing a dash ( - ) in between the input fields.Shocker
That's true, however the code needs one way to specify what the "next input" is - you can't just search for any input in the page and use that, since this behavior might lead to problems... The overall code is fine, just the selection part (in this case, the .next call) that needs to be adapted to each particular case (for instance a .nextAll if they are all in the same div, or a .parent().next().find if they are each in its own div, etc).Synergism
bind is deprecated, use onKyanize
P
10

You can use plain JavaScript:

See DEMO.

Check the character length with el.value.length. If it is equal to the maximum value, move to the next field by using focus(). Bind this function to the keyup event with onkeyup so that the function fires every time after the user keys in a character.

var a = document.getElementById("a"),
    b = document.getElementById("b"),
    c = document.getElementById("c");

a.onkeyup = function() {
    if (this.value.length === parseInt(this.attributes["maxlength"].value)) {
        b.focus();
    }
}

b.onkeyup = function() {
    if (this.value.length === parseInt(this.attributes["maxlength"].value)) {
        c.focus();
    }
}
Preterition answered 24/3, 2013 at 6:0 Comment(1)
Using keyup function might create issue, For e.g. you have to delete some number inside textbox, that event will not allow to delete as it's a keypress. Instead use oninput event. It will be helpfulAcutance
M
7

if you are going to have many fields you can do something like this.

basically on keyup get the length of the input and then compare it to the maxlength, if matches, then focus onto the next input field.

http://jsfiddle.net/btevfik/DVxDA/

$(document).ready(function(){
    $('input').keyup(function(){
        if(this.value.length==$(this).attr("maxlength")){
            $(this).next().focus();
        }
    });
});
Marrow answered 24/3, 2013 at 6:9 Comment(1)
Updated your code for better solution - jsfiddle.net/ssuryar/DVxDA/224 - DEMOAcutance
E
4

let otp = document.querySelector('#otp-screen');

for(let pin of otp.children) {
    pin.onkeyup = function() {
        if(pin.nextElementSibling) {
            pin.nextElementSibling.focus();
        }
    }
}
<div class="otp-screen" id="otp-screen">
    <input type="text" placeholder="0" maxlength="1"/> 
    <input type="text" placeholder="0" maxlength="1"/> 
    <input type="text" placeholder="0" maxlength="1"/> 
    <input type="text" placeholder="0" maxlength="1"/> 
</div>
Epicycle answered 10/7, 2020 at 13:7 Comment(1)
This only works with a maxlength of 1. The question is clearly not assuming such a maxlength.Jacoby
A
3

Updated btevfik code, Onkeyup or onkeydown will create issue as you won't be able to delete the previous input on tab navigation. It will be tough to edit or change the text inside the input box as it will be limited to maxlength. So we can use oninput event to achieve the task.

DEMO

HTML

<ul>
<li>a: <input type="text" maxlength="5" /></li>
<li>b: <input type="text" maxlength="3" /></li>
<li>c: <input type="text" maxlength="5" /></li>
<li>d: <input type="text" maxlength="3" /></li>
<li>e: <input type="text" maxlength="6" /></li>
<li>f: <input type="text" maxlength="10" /></li>
<li>g: <input type="text" maxlength="7" /></li>
</ul>

Javascript

$(document).ready(function(){
    $('input').on("input", function(){
        if($(this).val().length==$(this).attr("maxlength")){
            $(this).next().focus();
        }
    });
});

CSS

ul {list-style-type:none;}
li {padding:5px 5px;}
Acutance answered 18/1, 2018 at 14:16 Comment(2)
This worked great for me, I also modified it so it will go to the previous field when you delete everything from the focused field.Unstuck
@icortesi, you could share this modification, it would be very useful for other people.Virelay
M
1

Other answers do give some idea how this can be implemented, but I find that they do not consider some minor things among which are:

  1. The fact, that you do not want to auto-focus any elements across whole page, but rather within specific form.
  2. Input elements can be wrapped in some other elements (for example I wrap them in span or div to allow floating labels through CSS, and I've seen forms that use table to for structure).
  3. Validity of the field, when spilling over or moving to next one automatically.
  4. Input events when spilling over.
  5. Cursor position when returning to previous field (it looks like it can be saved by browser, thus backspacing can focus not in the end of the field, but, for example, in the middle).

Below code is trying to account to all of this, at least. Most of it can be tested on codepen: paste-spilling does not work there, looks like because of Clipboard API (other codepens with it do not work for me either).
Let me know if anything is unclear in the code, I'll update my answer and the code. If you find some edge case that is not covered - let me know as well.

For paste-spilling test using form from codepen, you can use something like this: 123456789123456789012345678903454353434534

Video sample of how it works in a more "live" envitonment on youtube

//List of input types, that are "textual" by default, thus can be tracked through keypress and paste events. In essence,
// these are types, that support maxlength attribute
const textInputTypes = ['email', 'password', 'search', 'tel', 'text', 'url', ];

formInit();

//Add listeners
function formInit()
{
    document.querySelectorAll('form input').forEach((item)=>{
        if (textInputTypes.includes(item.type)) {
            //Somehow backspace can be tracked only on keydown, not keypress
            item.addEventListener('keydown', inputBackSpace);
            if (item.getAttribute('maxlength')) {
                item.addEventListener('input', autoNext);
                item.addEventListener('change', autoNext);
                item.addEventListener('paste', pasteSplit);
            }
        }
    });
}


//Track backspace and focus previous input field, if input is empty, when it's pressed
function inputBackSpace(event)
{
    let current = event.target;
    if ((event.keyCode || event.charCode || 0) === 8 && !current.value) {
        let moveTo = nextInput(current, true);
        if (moveTo) {
            moveTo.focus();
            //Ensure, that cursor ends up at the end of the previous field
            moveTo.selectionStart = moveTo.selectionEnd = moveTo.value.length;
        }
    }
}

//Focus next field, if current is filled to the brim and valid
function autoNext(event)
{
    let current = event.target;
    //Get length attribute
    let maxLength = parseInt(current.getAttribute('maxlength'));
    //Check it against value length
    if (maxLength && current.value.length === maxLength && current.validity.valid) {
        let moveTo = nextInput(current, false);
        if (moveTo) {
            moveTo.focus();
        }
    }
}

async function pasteSplit(event)
{
    let permission = await navigator.permissions.query({ name: 'clipboard-read',});
    //Check permission is granted or not
    if (permission.state === 'denied') {
        //It's explicitly denied, thus cancelling action
        return false;
    }
    //Get buffer
    navigator.clipboard.readText().then(result => {
        let buffer = result.toString();
        //Get initial element
        let current = event.target;
        //Get initial length attribute
        let maxLength = parseInt(current.getAttribute('maxlength'));
        //Loop while the buffer is too large
        while (current && maxLength && buffer.length > maxLength) {
            //Ensure input value is updated
            current.value = buffer.substring(0, maxLength);
            //Trigger input event to bubble any bound events
            current.dispatchEvent(new Event('input', {
                bubbles: true,
                cancelable: true,
            }));
            //Do not spill over if a field is invalid
            if (!current.validity.valid) {
                return false;
            }
            //Update buffer value (not the buffer itself)
            buffer = buffer.substring(maxLength);
            //Get next node
            current = nextInput(current);
            if (current) {
                //Focus to provide visual identification of a switch
                current.focus();
                //Update maxLength
                maxLength = parseInt(current.getAttribute('maxlength'));
            }
        }
        //Check if we still have a valid node
        if (current) {
            //Dump everything we can from leftovers
            current.value = buffer;
            //Trigger input event to bubble any bound events
            current.dispatchEvent(new Event('input', {
                bubbles: true,
                cancelable: true,
            }));
        }
    }).catch(err => {
        //Most likely user denied request. Check status
        navigator.permissions.query({ name: 'clipboard-read',}).then(newPerm => {
            if (newPerm.state === 'granted') {
                console.log('Failed to read clipboard', err);
            } else {
                console.log('Request denied by user. Show him some notification to explain why enabling permission may be useful');
            }
        }).catch(errPerm => {
            console.log('Failed to read clipboard', errPerm);
        });
    });
}

//Find next/previous input
function nextInput(initial, reverse = false)
{
    //Get form
    let form = initial.form;
    //Iterate inputs inside the form. Not using previousElementSibling, because next/previous input may not be a sibling on the same level
    if (form) {
        let previous;
        for (let moveTo of form.querySelectorAll('input')) {
            if (reverse) {
                //Check if current element in loop is the initial one, meaning
                if (moveTo === initial) {
                    //If previous is not empty - share it. Otherwise - false, since initial input is first in the form
                    if (previous) {
                        return previous;
                    } else {
                        return false;
                    }
                }
            } else {
                //If we are moving forward and initial node is the previous one
                if (previous === initial) {
                    return moveTo;
                }
            }
            //Update previous input
            previous = moveTo;
        }
    }
    return false;
}
Mildamilde answered 2/9, 2021 at 15:33 Comment(0)
G
0

If you are adding input text fields dynamically then you can try this.

This will re-inject the script into the DOM and works Perfectly.

$('body').on('keyup', '#num_1',function(){
  if (this.value.length === parseInt(this.attributes["maxlength"].value)) {
    $('#num_2').focus();
  }
})
$('body').on('keyup','#num_2', function(){
   if (this.value.length === parseInt(this.attributes["maxlength"].value)) {
    $('#num_3').focus();
  }
})
<input type="text" class="form-control" name="number" maxlength="3" id="num_1">
<input type="text" class="form-control" name="number" maxlength="3" id="num_2">
<input type="text" class="form-control" name="number" maxlength="4" id="num_3">
Gelid answered 4/8, 2016 at 11:18 Comment(0)
U
0

If you're focused on creating card(debit/credit) number input type. Then clean an easily manageable jQuery version as follows:

/*..............................................................................................
* jQuery function for Credit card number input group
......................................................................................................*/
            // make container label of input groups, responsible
            $('.card-box').on('focus', function(e){
                $(this).parent().addClass('focus-form-control');
            });
            $('.card-box').on('blur', function(e){
                $(this).parent().removeClass('focus-form-control');
            });
            $('.card-box-1').on('keyup', function(e){
                e.preventDefault();
                var max_length = parseInt($(this).attr('maxLength'));
                var _length = parseInt($(this).val().length);
                if(_length >= max_length) {
                    $('.card-box-2').focus().removeAttr('readonly');
                    $(this).attr('readonly', 'readonly');
                }
                if(_length <= 0){
                    return;
                }
            });
            $('.card-box-2').on('keyup', function(e){
                e.preventDefault();
                var max_length = parseInt($(this).attr('maxLength'));
                var _length = parseInt($(this).val().length);
                if(_length >= max_length) {
                    $('.card-box-3').focus().removeAttr('readonly');
                    $(this).attr('readonly', 'readonly');
                }
                if(_length <= 0){
                    $('.card-box-1').focus().removeAttr('readonly');
                    $(this).attr('readonly', 'readonly');
                }
            });
             $('.card-box-3').on('keyup', function(e){
                e.preventDefault();
                var max_length = parseInt($(this).attr('maxLength'));
                var _length = parseInt($(this).val().length);
                if(_length >= max_length) {
                    $('.card-box-4').focus().removeAttr('readonly');
                    $(this).attr('readonly', 'readonly');
                }
                if(_length <= 0){
                    $('.card-box-2').focus().removeAttr('readonly');
                    $(this).attr('readonly', 'readonly');
                }
            });
            $('.card-box-4').on('keyup', function(e){
                e.preventDefault();
                var max_length = parseInt($(this).attr('maxLength'));
                var _length = parseInt($(this).val().length);
                if(_length >= max_length) {
                    return;
                }
                if(_length <= 0){
                    $('.card-box-3').focus().removeAttr('readonly');
                    $(this).attr('readonly', 'readonly');
                }
            });
/*..............................................................................................
* End jQuery function for Credit card number input group
......................................................................................................*/
/* Hide HTML5 Up and Down arrows. */
                                input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button {
                                    -webkit-appearance: none; margin: 0;
                                }
                                input[type="number"] { -moz-appearance: textfield; }
                                .card-box {
                                    width: 20%; display: inline-block; height: 100%; border: none;
                                }
                                
                                .focus-form-control {
                                    border-color: #66afe9; outline: 0;-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);
                                        box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);
                                }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<div class="form-control" style="padding: 0; max-width: 300px; ">
                                        <input class="card-box card-box-1" type="number" id="CreditCard_CardNumber1" required step="1" minlength="4" maxlength="4" pattern="[0-9]{4}" value="" placeholder="0000"
                                            onClick="this.setSelectionRange(0, this.value.length)" oninput="this.value=this.value.slice(0,this.maxLength||'');this.value=(this.value < 1) ? ('') : this.value;"/> 
                                        <input class="card-box card-box-2" type="number" id="CreditCard_CardNumber2" readonly required step="1" minlength="4" maxlength="4" pattern="[0-9]{4}" value="" placeholder="0000"
                                            onClick="this.setSelectionRange(0, this.value.length)" oninput="this.value=this.value.slice(0,this.maxLength||'');this.value=(this.value < 1) ? ('') : this.value;" />
                                        <input class="card-box card-box-3" type="number" id="CreditCard_CardNumber3" readonly required step="1" minlength="4" maxlength="4" pattern="[0-9]{4}" value="" placeholder="0000"
                                            onClick="this.setSelectionRange(0, this.value.length)" oninput="this.value=this.value.slice(0,this.maxLength||'');this.value=(this.value < 1) ? ('') : this.value;" />
                                        <input class="card-box card-box-4" type="number" id="CreditCard_CardNumber4" readonly required step="1" minlength="4" maxlength="4" pattern="[0-9]{4}" value="" placeholder="0000"
                                            onClick="this.setSelectionRange(0, this.value.length)" oninput="this.value=this.value.slice(0,this.maxLength||'');this.value=(this.value < 1) ? ('') : this.value;" />
                                    </div>
Usual answered 2/12, 2016 at 16:9 Comment(0)
B
0

Verified Answer have one issue which focus previous field if previous field have valid length

I have Modified Above Answer to fix complete length of previous tag

var container = document.getElementsByClassName("container")[0];
    container.onkeyup = function(e) {
    var target = e.srcElement || e.target;
    var maxLength = parseInt(target.attributes["maxlength"].value, 10);
    var myLength = target.value.length;
    if (myLength >= maxLength) {
        var next = target;
        while (next = next.nextElementSibling) {
            if (next == null)
                break;
            if (next.tagName.toLowerCase() === "input") {
                next.focus();
                break;
            }
        }
    }
    // Move to previous field if empty (user pressed backspace)
    else if (myLength === 0) {
         var previous = target;
       // Move to previous field if backspace is pressed
        if (code == 8) {
            previous = previous.previousElementSibling;
            if (previous != null) {
                if (previous.tagName.toLowerCase() === "input") {
                    previous.focus();
                }
            }
        } else {
            while (previous = previous.previousElementSibling) {
                if (previous == null)
                    break;
                if (previous.tagName.toLowerCase() === "input") {
                    var mLength = parseInt(previous.attributes["maxlength"].value, 10);
                    var pMyLength = previous.value.length;
                    // Move to previous field if it does not have required length
                    if (mLength == pMyLength) {
                        break;
                    } else {
                        previous.focus();
                        break;
                    }
                }
            }
        }
    }
}
Butta answered 30/8, 2018 at 14:34 Comment(0)
S
0

I think this is a shorter way. As long as you use a specific structure in HTML

const inputHandler = (event) => {
  if (event.target.nodeName == "INPUT" && event.target.nextElementSibling != null) {
    event.target.nextElementSibling.focus();
  }
}
<div class="enter-code">
  <input type="text" maxlength="1" @keyup="inputHandler">
  <input type="text" maxlength="1" @keyup="inputHandler">
  <input type="text" maxlength="1" @keyup="inputHandler">
  <input type="text" maxlength="1" @keyup="inputHandler">
</div>
Scrutable answered 16/2, 2023 at 16:44 Comment(1)
snippet does not work (I'm using Firefox)Deme

© 2022 - 2024 — McMap. All rights reserved.