Creating A Phone Number Input Control With Javascript

In this article I am going to show you how to create a cool phone number control in HTML using Javascript. Surely you have seen forms where you are asked to enter a phone number in three boxes – one for area code and the other two for the phone number. Sorry, international folks, I’m talking about US format but feel free to modify to you needs as the concept stays the same. So, let’s talk about what we want from our control:

  1. Three text boxes for the phone number.
  2. A text box only accepts digits.
  3. As the user finishes typing all numbers in one box, the cursor automatically jumps to the next one.
  4. If a box does not have any digits entered, it will show grayed out text unless the cursor is that box (i.e. it is focused).
  5. The control should be easy to add to your page.
  6. Javascript should provide a way to add any number of phone number controls on the page and distinguish between them.

Naturally, wrapping three text boxes and related functionality in a class should provide us a way to satisfy all of these items – it will give us a way to instantiate multiple instances of the control, encapsulate the text boxes and other data and provide functionality for a specific instance. Given how out class is going to contain HTML controls, it is pretty obvious that we should also have a containing container of some sorts. I’ll use a <div> in this example:

<!DOCTYPE html>
<html>
<head>
<title>Creating A Phone Number Input Control With Javascript</title>
</head>
<body>
	<div>
		Phone Number: 
		<div id="phoneNumberControlContainer">
		</div>
	</div>
</body>
</html>

Note that while I’m using HTML5 DOCTYPE for this example, we are not going to be using any of its features. Now that we have the container, we would like to add the phone number control in it and this is where all the fun begins.

First things first, let’s give the class we are about to create a name: PhoneNumberControl. Now let’s think about what kind of HTML elements PhoneNumberControl encapsulates. We’ll obviously need three text boxes. It would also be nice to maybe have a dash in between them; however, do we have any functionality associated with them? How about that we show grayed out text in text boxes if there is no value? – that is, we will need to store the “actual” value somewhere. A hidden input for each text box should do that. We also specified that we want any number of such controls on the page so we need to be able to distinguish between them – let’s give each instance of PhoneNumberControl an id property that uniquely identifies it (and, as you will see later on, id‘s of HTML elements). Sounds good? Let’s define our class:

function PhoneNumberControl(id, containerElement, noValueCharacter)
{
	this.id = id;
	this.containerElement = containerElement;
	this.noValueCharacter = noValueCharacter;
	
	this.inputAreaCode = this.createTextInput('areaCode', 3, 'phoneNumberControl-areaCode', '-');
	this.inputAreaCodeValue = this.createHiddenInput('areaCode');
	invalidateBlur(this.inputAreaCode);
	
	this.input3Digits = this.createTextInput('3Digits', 3, 'phoneNumberControl-3Digits', '-');
	this.input3DigitsValue = this.createHiddenInput('3Digits');
	invalidateBlur(this.input3Digits);
	
	this.input4Digits = this.createTextInput('4Digits', 4, 'phoneNumberControl-4Digits');
	this.input4DigitsValue = this.createHiddenInput('4Digits');
	invalidateBlur(this.input4Digits);
}

Let’s take a closer look. The things that an instance would need are a unique id that will identify it, a containerElement HTML element that will contain the control and a character to show when no value is entered in a text box. So the first three statements store them. We then create three text box inputs and a hidden input that holds the “real” value for each of them by calling the createTextInput() and createHiddenInput() methods of the class, which we will discuss next. The invalidateBlur() function takes care of showing grayed out text but will become relevant more towards the end of this article so, keep it in mind but don’t worry about it at this time.

Now that we have key pieces of information in order to create the control, we can start creating it. Since all three text boxes behave pretty much the same way, it only makes sense to move such functionality into its own methods within our class. Let’s take a look at the createTextInput() method:

PhoneNumberControl.prototype.createTextInput = function(id, maxLength, className, appendLabel)
{
	var input = document.createElement('input');
	input.id = this.id + '_' + id;
	input.type = 'text';
	input.maxLength = maxLength;
	input.className = className;
	input.phoneNumberControl = this;
	input.onkeypress = this.onKeyPress;
	input.onkeyup = this.onKeyUp;
	input.onfocus = this.onFocus;
	input.onblur = this.onBlur;
	this.containerElement.appendChild(input);
	if (typeof appendLabel == 'string') 
	{  
		var label = document.createElement('span');
		label.innerHTML = appendLabel;
		this.containerElement.appendChild(label);
	}
	return input;
}

First, the methods takes in the id parameter – not to be confused with the control’s this.id property. This parameter simply identifies a text box with the control and, if you look at the previous code snippet, you can see that we have a different one for each box: areaCode, 3Digits and 4Digits. The next parameter, maxLength, identifies the maximum number of digits a user can enter in the text box we are creating. The third parameter specifies a CSS class name so that we can style the control appropriately, as well as giving us flexibility to apply different styles to all three boxes. Finally, the appendLabel parameter specifies HTML that can be used to show something between boxes. As you will see, if this parameter is not set or is not a string, nothing will show after the text box.

In the method itself, we first create an <input> element and give it a unique identifier – this is where the control’s id gets added to the id parameter of the method, uniquely identifying this particular element. We then set pretty self-explanatory maxLength and className properties and move on to setting the phoneNumberControl property. This is not a built-in property of the <input> element however, by assigning it, Javascript will create one for us and assign it to the specified value. You may be wondering why we need to create it – for one, given how the text box is encapsulated in our control, it can be beneficial later on to have access to the containing object; and then, we actually will need it later on and I’ll explain why.

We then subscribe to events that will bring in the magic of grayed out text to life and which we are going to look at a bit later. As you can see, we’ll need onkeypress, onkeyup, onfocus and onkeyblur events in order for that to happen, and that we are subscribing to methods that will be specific to an instance (i.e. methods defined in our class).

The final thing left to do is to add the newly created <input> to the HTML element containing our control and, if there is something we want to put in between the text boxes (or rather right after this <input>), add that too into a <span>.

The second method we call creates a hidden input element – which is going to hold the “real” value of a text box created in the method above:

PhoneNumberControl.prototype.createHiddenInput = function(id)
{
	var input = document.createElement('input');
	input.id = this.id + '_' + id + '_value';
	input.type = 'hidden';
	this.containerElement.appendChild(input);
	return input;
}

As you can see, pretty self-explanatory code. So, by now, if you take out event subscriptions and the invalidateBlur() method, we can actually have our control on the page – we just need to add it. If you go over the code above, you’ll see that the only statement we need to write is one that creates an instance of PhoneNumberControl class – the rest is taken care of. There is one very important detail, though, in order for this to work: you should create the control after its container has been created. A good way to do this is in the onload event which will ensure all static HTML elements on the page are loaded:

window.onload = function() { new PhoneNumberControl('pnc', $('phoneNumberControlContainer'), 'X'); }

In the code above, we give our phone control a unique id of “pnc” and we are using the letter “X” for grayed out text. In case you have not dealt with prototype or a similar Javascript library, the $ function returns an element with the id that is passed to it as an argument. Which brings me to the fact that we are not going to use any of that but there are a couple of functions that we will need to finish up our control. So I am going to go ahead and define them next:

function $(id)
{
	return document.getElementById(id);
}

function $$(id)
{
	return $(id).style;
}

String.prototype.padLeft = function(totalLength, character)
{
	var str = this;
	while (str.length < totalLength)
	{
		str = character + str;
	}
	return str;
}

As you can see, the $() function returns a HTML element by its id and the $$() function returns such elements style. We also define a method on the String class that allows us to left pad a string with a specific character.

Now that we got our helper functions, let's move on to the events that make it all happen and come together. Let's start with the one that handles the cursor moving to the next text box once all digits are entered:

PhoneNumberControl.prototype.selectNext = function(currentInput)
{
	if (currentInput == this.inputAreaCode) { this.input3Digits.focus(); }
	else if (currentInput == this.input3Digits) { this.input4Digits.focus(); }
}

This method simply focuses the next text box based on what the current one is, falling well within the helper methods' category. I wanted to separate this one because it's our class' member.

Now, on to events – for the phone number, you would only want digits entered. This is handled by the keypress event:

PhoneNumberControl.prototype.onKeyPress = function(e)
{
	if (window.event) { var charCode = window.event.keyCode; }
	else if (e) { var charCode = e.which; }
	else { return true; }
	if (charCode > 31 && (charCode < 48 || charCode > 57)) { return false; }
	return true;
}

If you want more understanding about what is happening in the method above, check out the How to Allow Only Numbers in HTML TextBox Using Javascript article.

Remember the hidden input that contains the "real" value in the text box? – well, we cannot update it in keypress because the input value is updated after this event fires. A good place to do so would, however, be the keyup event, which fires after the value was updated. So, let's subscribe to it so we can keep our hidden control synced:

PhoneNumberControl.prototype.onKeyUp = function(e)
{
	var ev = window.event ? window.event : e;
	var input = ev.target ? ev.target : ev.srcElement;
	$(input.id + '_value').value = input.value;
	if (input.selectionStart == input.maxLength) { input.phoneNumberControl.selectNext(input); }
}

For both methods above, it is very important to note that their scope is outside of your instance or, in other words, do not count on accessing you instance properties in there just because it is a handler defined as method in your class. As you can see, there are no this references but! – remember that time we set phoneNumberControl in the very beginning? – that can now be used as a gateway to our PhoneNumberControl instance.

We now have the two methods above tracking the actual value of the inputs. All we have to do is deal with the element being active or not, and showing something when there is no value. This is actually done in a very simple way:

PhoneNumberControl.prototype.onFocus = function(e)
{
	var ev = window.event ? window.event : e;
	var input = ev.target ? ev.target : ev.srcElement;
	if ($(input.id + '_value').value != '') { return; }
	input.value = '';
	input.className = input.className.substring(0, input.className.length - 8);
}

PhoneNumberControl.prototype.onBlur = function(e)
{
	var ev = window.event ? window.event : e;
	var input = ev.target ? ev.target : ev.srcElement;
	invalidateBlur(input);
}

The OnFocus() method basically says, if the "real" value is blank – clear out the text in the text box and change the CSS class.
The OnBlur() method however, calls the invalidateBlur() function which does the opposite – if there is no "real" value, it populates the text box with grayed out text and changes the CSS class accordingly. The invalidateBlur() has to be made a function versus a member of the PhoneNumberControl class because you do not get any visibility to the object it was fired off of.

So, here is the final code you can play with, and I added some CSS so that it looks ok:

<!DOCTYPE html>
<html>
<head>
<title>Creating A Phone Number Input Control With Javascript</title>
<style type="text/css" rel="stylesheet">
body,
input
{
	font-family: Verdana;
	font-size: 12px;
}
div#phoneNumberControlContainer
{
	display: inline-block;
}
input.phoneNumberControl-areaCode,
input.phoneNumberControl-3Digits,
input.phoneNumberControl-4Digits
{
	font-weight: bold;
	color: #000080;
}
input.phoneNumberControl-areaCode-noValue,
input.phoneNumberControl-3Digits-noValue,
input.phoneNumberControl-4Digits-noValue
{
	font-weight: normal;
	color: #dddddd;
}
input.phoneNumberControl-areaCode,
input.phoneNumberControl-areaCode-noValue
{
	width: 26px;
}
input.phoneNumberControl-3Digits,
input.phoneNumberControl-3Digits-noValue
{
	width: 26px;
}
input.phoneNumberControl-4Digits,
input.phoneNumberControl-4Digits-noValue
{
	width: 34px;
}
</style>
<script type="text/javascript">
	function $(id)
	{
		return document.getElementById(id);
	}

	function $$(id)
	{
		return $(id).style;
	}
	
	String.prototype.padLeft = function(totalLength, character)
	{
		var str = this;
		while (str.length < totalLength)
		{
			str = character + str;
		}
		return str;
	}
	
	function invalidateBlur(input)
	{
		if ($(input.id + '_value').value != '') { return; }
		input.value = ''.padLeft(parseInt(input.maxLength), input.phoneNumberControl.noValueCharacter);
		input.className += '-noValue';
	}
	
	function PhoneNumberControl(id, containerElement, noValueCharacter)
	{
		this.id = id;
		this.containerElement = containerElement;
		this.noValueCharacter = noValueCharacter;
		
		this.inputAreaCode = this.createTextInput('areaCode', 3, 'phoneNumberControl-areaCode', '-');
		this.inputAreaCodeValue = this.createHiddenInput('areaCode');
		invalidateBlur(this.inputAreaCode);
		
		this.input3Digits = this.createTextInput('3Digits', 3, 'phoneNumberControl-3Digits', '-');
		this.input3DigitsValue = this.createHiddenInput('3Digits');
		invalidateBlur(this.input3Digits);
		
		this.input4Digits = this.createTextInput('4Digits', 4, 'phoneNumberControl-4Digits');
		this.input4DigitsValue = this.createHiddenInput('4Digits');
		invalidateBlur(this.input4Digits);
	}
	
	PhoneNumberControl.prototype.createTextInput = function(id, maxLength, className, appendLabel)
	{
		var input = document.createElement('input');
		input.id = this.id + '_' + id;
		input.type = 'text';
		input.maxLength = maxLength;
		input.className = className;
		input.phoneNumberControl = this;
		input.onkeypress = this.onKeyPress;
		input.onkeyup = this.onKeyUp;
		input.onfocus = this.onFocus;
		input.onblur = this.onBlur;
		this.containerElement.appendChild(input);
		if (typeof appendLabel == 'string') 
		{  
			var label = document.createElement('span');
			label.innerHTML = appendLabel;
			this.containerElement.appendChild(label);
		}
		return input;
	}
	
	PhoneNumberControl.prototype.createHiddenInput = function(id)
	{
		var input = document.createElement('input');
		input.id = this.id + '_' + id + '_value';
		input.type = 'hidden';
		this.containerElement.appendChild(input);
		return input;
	}
	
	PhoneNumberControl.prototype.selectNext = function(currentInput)
	{
		if (currentInput == this.inputAreaCode) { this.input3Digits.focus(); }
		else if (currentInput == this.input3Digits) { this.input4Digits.focus(); }
	}
	
	PhoneNumberControl.prototype.onKeyPress = function(e)
	{
		if (window.event) { var charCode = window.event.keyCode; }
		else if (e) { var charCode = e.which; }
		else { return true; }
		if (charCode > 31 && (charCode < 48 || charCode > 57)) { return false; }
		return true;
	}
	
	PhoneNumberControl.prototype.onKeyUp = function(e)
	{
		var ev = window.event ? window.event : e;
		var input = ev.target ? ev.target : ev.srcElement;
		$(input.id + '_value').value = input.value;
		if (input.selectionStart == input.maxLength) { input.phoneNumberControl.selectNext(input); }
	}
	
	PhoneNumberControl.prototype.onFocus = function(e)
	{
		var ev = window.event ? window.event : e;
		var input = ev.target ? ev.target : ev.srcElement;
		if ($(input.id + '_value').value != '') { return; }
		input.value = '';
		input.className = input.className.substring(0, input.className.length - 8);
	}
	
	PhoneNumberControl.prototype.onBlur = function(e)
	{
		var ev = window.event ? window.event : e;
		var input = ev.target ? ev.target : ev.srcElement;
		invalidateBlur(input);
	}
	
	window.onload = function() { new PhoneNumberControl('pnc', $('phoneNumberControlContainer'), 'X'); }
</script>
</head>
<body>
	<div>
		Phone Number: 
		<div id="phoneNumberControlContainer">
		</div>
	</div>
</body>
</html>
This entry was posted in Javascript and tagged , , , , , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *