19
Scripting Forms
WHAT’S IN THIS CHAPTER?
➤➤ Understanding form basics
➤➤ Text box validation and interaction
➤➤ Working with other form controls
WROX.COM DOWNLOADS FOR THIS CHAPTER
Please note that all the code examples for this chapter are available as a part of this chapter’s
code download on the book’s website at www.wrox.com/go/projavascript4e on the Down-
load Code tab.
One of the original uses of JavaScript was to offload some form-processing responsibilities onto
the browser instead of relying on the server to do it all. Although the web and JavaScript have
evolved since that time, web forms remain more or less unchanged. The failure of web forms to
provide out-of-the-box solutions for common problems led developers to use JavaScript not just
for form validation but also to augment the default behavior of standard form controls.
FORM BASICS
Web forms are represented by the
element in HTML and by the HTMLFormElement typein JavaScript. The HTMLFormElement type inherits from HTMLElement and therefore has all of
the same default properties as other HTML elements. However, HTMLFormElement also has the
following additional properties and methods:
➤➤ acceptCharset—The character sets that the server can process; equivalent to the
HTML accept-charset attribute.
Professional JavaScript® for Web Developers, Fourth Edition. Matt Frisbie.
© 2020 John Wiley & Sons, Inc. Published 2020 by John Wiley & Sons, Inc.
708 ❘ CHAPTER 19 Scripting Forms
➤➤ action—The URL to send the request to; equivalent to the HTML action attribute.
➤➤ elements—An HTMLCollection of all controls in the form.
➤➤ enctype—The encoding type of the request; equivalent to the HTML enctype attribute.
➤➤ length—The number of controls in the form.
➤➤ method—The type of HTTP request to send, typically "get" or "post"; equivalent to the
HTML method attribute.
➤➤ name—The name of the form; equivalent to the HTML name attribute.
➤➤ reset()—Resets all form fields to their default values.
➤➤ submit()—Submits the form.
➤➤ target—The name of the window to use for sending the request and receiving the response;
equivalent to the HTML target attribute.
References to
elements can be retrieved in a number of different ways. The most c ommonway is to treat them as any other elements and assign the id attribute, allowing the use of
g etElementById(), as in the following example:
let form = document.getElementById("form1");
All forms on the page can also be retrieved from document.forms collection. Each form can be
accessed in this collection by numeric index and by name, as shown in the following examples:
// get the first form in the page
let firstForm = document.forms[0];
// get the form with a name of "form2"
let myForm = document.forms["form2"];
Older browsers, or those with strict backwards compatibility, also add each form with a name as
a property of the document object. For instance, a form named "form2" could be accessed via
document.form2. This approach is not recommended, because it is error-prone and may be removed
from browsers in the future.
Note that forms can have both an id and a name and that these values need not be the same.
Submitting Forms
Forms are submitted when a user interacts with a submit button or an image button. Submit b uttons
are defined using either the element or the element with a type attribute of " submit",
and image buttons are defined using the element with a type attribute of "image". All of
the following, when clicked, will submit a form in which the button resides:
Submit Form
Form Basics ❘ 709
If any one of these types of buttons is within a form that has a submit button, pressing Enter on the
keyboard while a form control has focus will also submit the form. (The one exception is a textarea,
within which Enter creates a new line of text.) Note that forms without a submit button will not be
submitted when Enter is pressed.
When a form is submitted in this manner, the submit event fires right before the request is sent to the
server. This gives you the opportunity to validate the form data and decide whether to allow the form
submission to occur. Preventing the event’s default behavior cancels the form submission. For exam-
ple, the following prevents a form from being submitted:
let form = document.getElementById("myForm");
form.addEventListener("submit", (event) => {
// prevent form submission
event.preventDefault();
});
The preventDefault() method stops the form from being submitted. Typically, this functionality is
used when data in the form is invalid and should not be sent to the server.
It’s possible to submit a form programmatically by calling the submit() method from JavaScript.
This method can be called at any time to submit a form and does not require a submit button to be
present in the form to function appropriately. Here’s an example:
let form = document.getElementById("myForm");
// submit the form
form.submit();
When a form is submitted via submit(), the submit event does not fire, so be sure to do data valida-
tion before calling the method.
One of the biggest issues with form submission is the possibility of submitting the form twice. Users
sometimes get impatient when it seems like nothing is happening and may click a submit button mul-
tiple times. The results can be annoying (because the server processes duplicate requests) or damag-
ing (if the user is attempting a purchase and ends up placing multiple orders). There are essentially
two ways to solve this problem: disable the submit button once the form is submitted, or use the
onsubmit event handler to cancel any further form submissions.
Resetting Forms
Forms are reset when the user clicks a reset button. Reset buttons are created using either the
or the element with a type attribute of "reset", as in these examples:
Reset Form
710 ❘ CHAPTER 19 Scripting Forms
Either of these buttons will reset a form. When a form is reset, all of the form fields are set back to
the values they had when the page was first rendered. If a field was originally blank, it becomes blank
again, whereas a field with a default value reverts to that value.
When a form is reset by the user clicking a reset button, the reset event fires. This event gives you
the opportunity to cancel the reset if necessary. For example, the following prevents a form from
being reset:
let form = document.getElementById("myForm");
form.addEventListener("reset", (event) => {
event.preventDefault();
});
As with form submission, resetting a form can be accomplished via JavaScript using the reset()
method, as in this example:
let form = document.getElementById("myForm");
// reset the form
form.reset();
Unlike the submit() method’s functionality, reset() fires the reset event the same as if a reset
button were clicked.
NOTE Form resetting is typically a frowned-upon approach to web form design.
It’s often disorienting to the user and, when triggered accidentally, can be quite
frustrating. There’s almost never a need to reset a form. It’s often enough to pro-
vide a cancel button that takes the user back to the previous page rather than
explicitly revert all values in the form.
Form Fields
Form elements can be accessed in the same ways as any other elements on the page using native
DOM methods. Additionally, all form elements are parts of an elements collection that is a property
of each form. The elements collection is an ordered list of references to all form fields in the form
and includes all , , , , and elements. Each form
field appears in the elements collection in the order in which it appears in the markup, indexed by
both position and name. Here are some examples:
let form = document.getElementById("form1");
// get the first field in the form
let field1 = form.elements[0];
// get the field named "textbox1"
let field2 = form.elements["textbox1"];
// get the number of fields
let fieldCount = form.elements.length;
Form Basics ❘ 711
If a name is in use by multiple form controls, as is the case with radio buttons, then an H TMLCollection
is returned containing all of the elements with the name. For example, consider the following HTML
snippet:
- Red
- Green
- Blue
The form in this HTML has three radio controls that have "color" as their name, which ties the
fields together. When accessing elements["color"], a NodeList is returned, containing all three
elements; when accessing elements[0], however, only the first element is returned. Consider
this example:
let form = document.getElementById("myForm");
let colorFields = form.elements["color"];
console.log(colorFields.length); // 3
let firstColorField = colorFields[0]; // true
let firstFormField = form.elements[0];
console.log(firstColorField === firstFormField);
This code shows that the first form field, accessed via form.elements[0], is the same as the first ele-
ment contained in form.elements["color"].
NOTE It’s possible to access elements as properties of a form as well, such as
form[0] to get the first form field and form["color"] to get a named field. These
properties always return the same thing as their equivalent in the elements
collection. This approach is provided for backwards compatibility with older
browsers and should be avoided when possible in favor of using elements.
Common Form-Field Properties
With the exception of the
element, all form fields share a common set of properties.Because the type represents many form fields, some properties are used only with certain
field types, whereas others are used regardless of the field type. The common form-field properties
and methods are as follows:
➤➤ disabled—A Boolean indicating if the field is disabled.
➤➤ form—A pointer to the form that the field belongs to. This property is read-only.
➤➤ name—The name of the field.
➤➤ readOnly—A Boolean indicating if the field is read-only.
➤➤ tabIndex—Indicates the tab order for the field.
712 ❘ CHAPTER 19 Scripting Forms
➤➤ type—The type of the field: "checkbox", "radio", and so on.
➤➤ value—The value of the field that will be submitted to the server. For file-input fields, this
property is read only and simply contains the file’s path on the computer.
With the exception of the form property, JavaScript can change all other properties dynamically.
Consider this example:
let form = document.getElementById("myForm");
let field = form.elements[0];
// change the value
field.value = "Another value";
// check the value of form
console.log(field.form === form); // true
// set focus to the field
field.focus();
// disable the field
field.disabled = true;
// change the type of field (not recommended, but possible for )
field.type = "checkbox";
The ability to change form-field properties dynamically allows you to change the form at any time
and in almost any way. For example, a common problem with web forms is users’ tendency to click
the submit button twice. This is a major problem when credit-card orders are involved because it may
result in duplicate charges. A very common solution to this problem is to disable the submit button
once it’s been clicked, which is possible by listening for the submit event and disabling the submit
button when it occurs. The following code accomplishes this:
// Code to prevent multiple form submissions
let form = document.getElementById("myForm");
form.addEventListener("submit", (event) => {
let target = event.target;
// get the submit button
let btn = target.elements["submit-btn"];
// disable the submit button
btn.disabled = true;
});
This code attaches an event handler on the form for the submit event. When the event fires, the
submit button is retrieved and its disabled property is set to true. Note that you cannot attach an
onclick event handler to the submit button to do this because of a timing issue across browsers:
some browsers fire the click event before the form’s submit event, some after. For browsers that
fire click first, the button will be disabled before the submission occurs, meaning that the form will
never be submitted. Therefore it’s better to disable the submit button using the submit event. This
approach won’t work if you are submitting the form without using a submit button because, as stated
before, the submit event is fired only by a submit button.
Form Basics ❘ 713
The type property exists for all form fields except
. For elements, this value isequal to the HTML type attribute. For other elements, the value of type is set as described in the
following table.
DESCRIPTION SAMPLE HTML VALUE OF T YPE
Single-select list ... "select-one"
Multi-select list ... "select-multiple"
Custom button ... "submit"
Custom nonsubmit button ... "button"
Custom reset button ... "reset"
Custom submit button ... "submit"
For and elements, the type property can be changed dynamically, whereas the
element’s type property is read-only.
Common Form-Field Methods
Each form field has two methods in common: focus() and blur(). The focus() method sets the
browser’s focus to the form field, meaning that the field becomes active and will respond to keyboard
events. For example, a text box that receives focus displays its caret and is ready to accept input. The
focus() method is most often employed to call the user’s attention to some part of the page. It’s quite
common, for instance, to have the focus moved to the first field in a form when the page is loaded.
This can be accomplished by listening for the load event and then calling focus() on the first field,
as in the following example:
window.addEventListener("load", (event) => {
document.forms[0].elements[0].focus();
});
Note that this code will cause an error if the first form field is an element with a type of
"hidden" or if the field is being hidden using the display or visibility CSS property.
HTML5 introduces an autofocus attribute for form fields that causes supporting browsers to auto-
matically set the focus to that element without the use of JavaScript. For example:
In order for the previous code to work correctly with autofocus, you must first detect if it has been
set. If autofocus is set, you should not call focus():
window.addEventListener("load", (event) => {
let element = document.forms[0].elements[0];
if (element.autofocus !== true) {
element.focus();
console.log("JS focus");
}
});
714 ❘ CHAPTER 19 Scripting Forms
Because autofocus is a Boolean attribute, the value of the autofocus property will be true in
supporting browsers. (It will be the empty string in browsers without support.) So this code calls
focus() only if the autofocus property is not equal to true, ensuring forwards compatibility. The
autofocus property is supported in most modern browsers. It only lacks support in iOS Safari,
Opera Mini, and Internet Explorer 10 and below.
NOTE By default, only form elements can have focus set to them. It’s possible to
allow any element to have focus by setting its tabIndex property to –1 and then
calling focus(). The only browser that doesn’t support this technique is Opera.
The opposite of focus() is blur(), which removes focus from the element. When blur() is called,
focus isn’t moved to any element in particular; it’s just removed from the field on which it was called.
This method was used early in web development to create read-only fields before the readonly
attribute was introduced. There’s rarely a need to call blur(), but it’s available if necessary. Here’s
an example:
document.forms[0].elements[0].blur();
Common Form-Field Events
All form fields support the following three events in addition to mouse, keyboard, mutation, and
HTML events:
➤➤ blur—Fires when the field loses focus.
➤➤ change—Fires when the field loses focus and the value has changed for and
elements; also fires when the selected option changes for elements.
➤➤ focus—Fires when the field gets focus.
Both the blur and the focus events fire because of users manually changing the field’s focus, as well
as by calling the blur() and focus() methods, respectively. These two events work the same way for
all form fields. The change event, however, fires at different times for different controls. For
and elements, the change event fires when the field loses focus and the value has
changed since the time the control got focus. For elements, however, the change event fires
whenever the user changes the selected option; the control need not lose focus for change to fire.
The focus and blur events are typically used to change the user interface in some way, to provide
either visual cues or additional functionality (such as showing a drop-down menu of options for a
text box). The change event is typically used to validate data that was entered into a field. For exam-
ple, consider a text box that expects only numbers to be entered. The focus event may be used to
change the background color to more clearly indicate that the field has focus, the blur event can be
used to remove that background color, and the change event can change the background color to red
if nonnumeric characters are entered. The following code accomplishes this:
let textbox = document.forms[0].elements[0];
Scripting Text Boxes ❘ 715
textbox.addEventListener("focus", (event) => {
let target = event.target;
if (target.style.backgroundColor != "red") {
target.style.backgroundColor = "yellow";
}
});
textbox.addEventListener("blur", (event) => {
let target = event.target;
target.style.backgroundColor = /[^\d]/.test(target.value) ? "red" : "";
});
textbox.addEventListener("change", (event) => {
let target = event.target;
target.style.backgroundColor = /[^\d]/.test(target.value) ? "red" : "";
});
The onfocus event handler simply changes the background color of the text box to yellow, more
clearly indicating that it’s the active field. The onblur and onchange event handlers turn the back-
ground color red if any nonnumeric character is found. To test for a nonnumeric character, use
a simple regular expression against the text box’s value. This functionality has to be in both the
onblur and onchange event handlers to ensure that the behavior remains consistent regardless of text
box changes.
NOTE The relationship between the blur and the change events is not strictly
defined. In some browsers, the blur event fires before change; in others, it’s the
opposite. You can’t depend on the order in which these events fire, so use care
whenever they are required.
SCRIPTING TEXT BOXES
There are two ways to represent text boxes in HTML: a single-line version using the element
and a multiline version using . These two controls are very similar and behave in similar
ways most of the time. There are, however, some important differences.
By default, the element displays a text box, even when the type attribute is omitted (the
default value is "text"). The size attribute can then be used to specify how wide the text box should
be in terms of visible characters. The value attribute specifies the initial value of the text box, and
the maxlength attribute specifies the maximum number of characters allowed in the text box. So to
create a text box that can display 25 characters at a time but has a maximum length of 50, you can
use the following code:
The element always renders a multiline text box. To specify how large the text box
should be, you can use the rows attribute, which specifies the height of the text box in number of
characters, and the cols attribute, which specifies the width in number of characters, similar to size
716 ❘ CHAPTER 19 Scripting Forms
for an element. Unlike , the initial value of a must be enclosed between
and , as shown here:
initial value
Also unlike the element, a cannot specify the maximum number of characters
allowed using HTML.
Despite the differences in markup, both types of text boxes store their contents in the value property.
The value can be used to read the text box value and to set the text box value, as in this example:
let textbox = document.forms[0].elements["textbox1"];
console.log(textbox.value);
textbox.value = "Some new value";
You should use the value property to read or write text box values rather than to use standard DOM
methods. For instance, don’t use setAttribute() to set the value attribute on an element,
and don’t try to modify the first child node of a element. Changes to the value property
aren’t always reflected in the DOM either, so it’s best to avoid using DOM methods when dealing
with text box values.
Text Selection
Both types of text boxes support a method called select(), which selects all of the text in a text box.
Most browsers automatically set focus to the text box when the select() method is called (Opera
does not). The method accepts no arguments and can be called at any time. Here’s an example:
let textbox = document.forms[0].elements["textbox1"];
textbox.select();
It’s quite common to select all of the text in a text box when it gets focus, especially if the text box
has a default value. The thinking is that it makes life easier for users when they don’t have to delete
text separately. This pattern is accomplished with the following code:
textbox.addEventListener("focus", (event) => {
event.target.select();
});
With this code applied to a text box, all of the text will be selected as soon as the text box gets focus.
This can greatly aid the usability of forms.
The select Event
To accompany the select() method, there is a select event. The select event fires when text is
selected in the text box. Exactly when the event fires differs from browser to browser. In Internet
Explorer 9+, Opera, Firefox, Chrome, and Safari, the select event fires once the user has finished
selecting text, whereas in Internet Explorer 8 and earlier it fires as soon as one letter is selected. The
select event also fires when the select() method is called. Here’s a simple example:
let textbox = document.forms[0].elements["textbox1"];
textbox.addEventListener("select", (event) => {
console.log('Text selected: ${textbox.value}');
});
Scripting Text Boxes ❘ 717
Retrieving Selected Text
Although useful for understanding when text is selected, the select event provides no information
about what text has been selected. HTML5 solved this issue by introducing some extensions to allow
for better retrieval of selected text. The specification approach adds two properties to text boxes:
selectionStart and selectionEnd. These properties contain zero-based numbers indicating the
text-selection boundaries (the offset of the beginning of text selection and the offset of end of text
selection, respectively). So, to get the selected text in a text box, you can use the following code:
function getSelectedText(textbox){
return textbox.value.substring(textbox.selectionStart,
textbox.selectionEnd);
}
Because the substring() method works on string offsets, the values from selectionStart and
selectionEnd can be passed in directly to retrieve the selected text.
This solution works for Internet Explorer 9+, Firefox, Safari, Chrome, and Opera. Internet Explorer 8
and earlier don’t support these properties, so a different approach is necessary.
Older versions of Internet Explorer have a document.selection object that contains text-selection
information for the entire document, which means you can’t be sure where the selected text is on the
page. When used in conjunction with the select event, however, you can be assured that the selec-
tion is inside the text box that fired the event. To get the selected text, you must first create a range
and then extract the text from it, as in the following:
function getSelectedText(textbox){
if (typeof textbox.selectionStart == "number"){
return textbox.value.substring(textbox.selectionStart,
textbox.selectionEnd);
} else if (document.selection){
return document.selection.createRange().text;
}
}
This function has been modified to determine whether to use the Internet Explorer approach to
selected text. Note that document.selection doesn’t need the textbox argument at all.
Partial Text Selection
HTML5 also specifies an addition to aid in partially selecting text in a text box. The
s etSelectionRange() method, originally implemented by Firefox, is now available on all text
boxes in addition to the select() method. This method takes two arguments: the index of the first
character to select and the index at which to stop the selection (the same as the string’s substring()
method). Here are some examples:
textbox.value = "Hello world!"
// select all text
textbox.setSelectionRange(0, textbox.value.length); // "Hello world!"
// select first three characters
textbox.setSelectionRange(0, 3); // "Hel"
718 ❘ CHAPTER 19 Scripting Forms
// select characters 4 through 6
textbox.setSelectionRange(4, 7); // "o w"
To see the selection, you must set focus to the text box either immediately before or after a call to
setSelectionRange(). This approach works for Internet Explorer 9, Firefox, Safari, Chrome,
and Opera.
Internet Explorer 8 and earlier allow partial text selection through the use of ranges. To select
part of the text in a text box, you must first create a range and place it in the correct position by
using the createTextRange() method that Internet Explorer provides on text boxes and using the
moveStart() and moveEnd() range methods to move the range into position. Before calling these
methods, however, you need to collapse the range to the start of the text box using collapse(). After
that, moveStart() moves both the starting and the end points of the range to the same position. You
can then pass in the total number of characters to select as the argument to moveEnd(). The last step
is to use the range’s select() method to select the text, as shown in these examples:
textbox.value = "Hello world!";
var range = textbox.createTextRange();
// select all text // "Hello world!"
range.collapse(true);
range.moveStart("character", 0);
range.moveEnd("character", textbox.value.length);
range.select();
// select first three characters
range.collapse(true);
range.moveStart("character", 0);
range.moveEnd("character", 3);
range.select(); // "Hel"
// select characters 4 through 6
range.collapse(true);
range.moveStart("character", 4);
range.moveEnd("character", 3);
range.select(); // "o w"
As with the other browsers, the text box must have focus in order for the selection to be visible.
Partial text selection is useful for implementing advanced text input boxes such as those that provide
autocomplete suggestions.
Input Filtering
It’s common for text boxes to expect a certain type of data or data format. Perhaps the data needs to
contain certain characters or must match a particular pattern. Because text boxes don’t offer much
in the way of validation by default, JavaScript must be used to accomplish such input filtering. Using
a combination of events and other DOM capabilities, you can turn a regular text box into one that
understands the data it is dealing with.
Scripting Text Boxes ❘ 719
Blocking Characters
Certain types of input require that specific characters be present or absent. For example, a text box
for the user’s phone number should not allow non-numeric values to be inserted. The keypress event
is responsible for inserting characters into a text box. Characters can be blocked by preventing this
event’s default behavior. For example, the following code blocks all key presses:
textbox.addEventListener("keypress", (event) => {
event.preventDefault();
});
Running this code causes the text box to effectively become read only, because all key presses are
blocked. To block only specific characters, you need to inspect the character code for the event and
determine the correct response. For example, the following code allows only numbers:
textbox.addEventListener("keypress", (event) => {
if (!/\d/.test(String.fromCharCode(event.charCode))){
event.preventDefault();
}
});
In this example, the character code is converted to a string using String.fromCharCode(), and the
result is tested against the regular expression /\d/, which matches all numeric characters. If that test
fails, then the event is blocked using preventDefault(). This ensures that the text box ignores non-
numeric keys.
Even though the keypress event should be fired only when a character key is pressed, some browsers
fire it for other keys as well. Firefox and Safari (versions prior to 3.1) fire keypress for keys such as
up, down, Backspace, and Delete; Safari versions 3.1 and later do not fire keypress events for these
keys. This means that simply blocking all characters that aren’t numbers isn’t good enough because
you’ll also be blocking these very useful and necessary keys. Fortunately, you can easily detect when
one of these keys is pressed. In Firefox, all noncharacter keys that fire the keypress event have a
character code of 0, whereas Safari versions prior to 3 give them all a character code of 8. To general-
ize the case, you don’t want to block any character codes lower than 10. The function can then be
updated as follows:
textbox.addEventListener("keypress", (event) => {
if (!/\d/.test(String.fromCharCode(event.charCode)) &&
event.charCode > 9){
event.preventDefault();
}
});
The event handler now behaves appropriately in all browsers, blocking nonnumeric characters but
allowing all basic keys that also fire keypress.
There is still one more issue to handle: copying, pasting, and any other functions that involve the Ctrl
key. In all browsers but Internet Explorer, the preceding code disallows the shortcut keystrokes of
Ctrl+C, Ctrl+V, and any other combinations using the Ctrl key. The last check, therefore, is to make
sure the Ctrl key is not pressed, as shown in the following example:
textbox.addEventListener("keypress", (event) => {
if (!/\d/.test(String.fromCharCode(event.charCode)) &&
event.charCode > 9 &&
720 ❘ CHAPTER 19 Scripting Forms
!event.ctrlKey){
event.preventDefault();
}
});
This final change ensures that all of the default text box behaviors work. This technique can be cus-
tomized to allow or disallow any characters in a text box.
Dealing with the Clipboard
Internet Explorer was the first browser to support events related to the clipboard and access to
clipboard data from JavaScript. The Internet Explorer implementation became a de facto standard as
Safari, Chrome, Opera, and Firefox implemented similar events and clipboard access, and clipboard
events were later added to HTML5. The following six events are related to the clipboard:
➤➤ beforecopy—Fires just before the copy operation takes place.
➤➤ copy—Fires when the copy operation takes place.
➤➤ beforecut—Fires just before the cut operation takes place.
➤➤ cut—Fires when the cut operation takes place.
➤➤ beforepaste—Fires just before the paste operation takes place.
➤➤ paste—Fires when the paste operation takes place.
Because this is a fairly new standard governing clipboard access, the behavior of the events and
related objects differs from browser to browser. In Safari, Chrome, and Firefox, the beforecopy,
beforecut, and beforepaste events fire only when the context menu for the text box is displayed
(in anticipation of a clipboard event), but Internet Explorer fires them in that case and immediately
before firing the copy, cut, and paste events. The copy, cut, and paste events all fire when you
would expect them to in all browsers, both when the selection is made from a context menu and
when using keyboard shortcuts.
The beforecopy, beforecut, and beforepaste events give you the opportunity to change the data
being sent to or retrieved from the clipboard before the actual event occurs. However, canceling these
events does not cancel the clipboard operation—you must cancel the copy, cut, or paste event to
prevent the operation from occurring.
Clipboard data is accessible via the clipboardData object that exists either on the window object
(in Internet Explorer) or on the event object (in Firefox, Safari, and Chrome). In Firefox, Safari, and
Chrome, the clipboardData object is available only during clipboard events to prevent unauthorized
clipboard access; Internet Explorer exposes the clipboardData object all the time. For cross-browser
compatibility, it’s best to use this object only during clipboard events.
There are three methods on the clipboardData object: getData(), setData(), and clearData().
The getData() method retrieves string data from the clipboard and accepts a single argument, which
is the format for the data to retrieve. Internet Explorer specifies two options: "text" and "URL". Fire-
fox, Safari, and Chrome expect a MIME type but will accept "text" as equivalent to "text/plain".
The setData() method is similar: its first argument is the data type, and its second argument is the
text to place on the clipboard. Once again, Internet Explorer supports "text" and "URL", whereas
Scripting Text Boxes ❘ 721
Safari and Chrome expect a MIME type. Unlike getData(), however, Safari and Chrome won’t
recognize the "text" type. Only Internet Explorer 8 and earlier allow honors calling setData();
other browsers simply ignore the call. To even out the differences, you can use the following cross-
browser methods:
function getClipboardText(event){
var clipboardData = (event.clipboardData || window.clipboardData);
return clipboardData.getData("text");
}
function setClipboardText (event, value){
if (event.clipboardData){
return event.clipboardData.setData("text/plain", value);
} else if (window.clipboardData){
return window.clipboardData.setData("text", value);
}
}
The getClipboardText() function is relatively simple. It needs only to identify the location of the
clipboardData object and then call getData() with a type of "text". Its companion method,
s etClipboardText(), is slightly more involved. Once the clipboardData object is located,
setData() is called with the appropriate type for each implementation ("text/plain" for Firefox,
Safari, and Chrome; "text" for Internet Explorer).
Reading text from the clipboard is helpful when you have a text box that expects only certain
characters or a certain format of text. For example, if a text box allows only numbers, then pasted
values must also be inspected to ensure that the value is valid. In the paste event, you can determine
if the text on the clipboard is invalid and, if so, cancel the default behavior, as shown in the follow-
ing example:
textbox.addEventListener("paste", (event) => {
let text = getClipboardText(event);
if (!/^\d*$/.test(text)){
event.preventDefault();
}
});
This onpaste handler ensures that only numeric values can be pasted into the text box. If the clip-
board value doesn’t match the pattern, then the paste is canceled. Firefox, Safari, and Chrome allow
access to the getData() method only in an onpaste event handler.
Because not all browsers support clipboard access, it’s often easier to block one or more of the
clipboard operations. In browsers that support the copy, cut, and paste events (Internet Explorer,
Safari, Chrome, and Firefox), it’s easy to prevent the event’s default behavior. For Opera, you need to
block the keystrokes that cause the events and block the context menu from being displayed.
Automatic Tab Forward
JavaScript can be used to increase the usability of form fields in a number of ways. One of the most
common is to automatically move the focus to the next field when the current field is complete.
This is frequently done when entering data whose appropriate length is already known, such as for
722 ❘ CHAPTER 19 Scripting Forms
telephone numbers. In the United States, telephone numbers are typically split into three parts: the
area code, the exchange, and then four more digits. It’s quite common for web pages to represent this
as three text boxes, such as the following:
To aid in usability and speed up the data-entry process, you can automatically move focus to the next
element as soon as the maximum number of characters has been entered. So once the user types three
characters in the first text box, the focus moves to the second, and once the user types three charac-
ters in the second text box, the focus moves to the third. This "tab forward" behavior can be accom-
plished using the following code:
function tabForward(event){
let target = event.target;
if (target.value.length == target.maxLength){
let form = target.form;
for (let i = 0, len = form.elements.length; i < len; i++) {
if (form.elements[i] == target) {
if (form.elements[i+1]) {
form.elements[i+1].focus();
}
return;
}
}
}
}
let inputIds = ["txtTel1", "txtTel2", "txtTel3"];
for (let id of inputIds) {
let textbox = document.getElementById(id);
textbox.addEventListener("keyup", tabForward);
}
let textbox1 = document.getElementById("txtTel1");
let textbox2 = document.getElementById("txtTel2");
let textbox3 = document.getElementById("txtTel3");
The tabForward() function is the key to this functionality. It checks to see if the text box’s maximum
length has been reached by comparing the value to the maxlength attribute. If they’re equal (because
the browser enforces the maximum, there’s no way it could be more), then the next form element
needs to be found by looping through the elements collection until the text box is found and then set-
ting focus to the element in the next position. This function is then assigned as the onkeyup handler
for each text box. Since the keyup event fires after a new character has been inserted into the text
box, this is the ideal time to check the length of the text box contents. When filling out this simple
form, the user will never have to press the Tab key to move between fields and submit the form.
Scripting Text Boxes ❘ 723
Keep in mind that this code is specific to the markup mentioned previously and doesn’t take into
account possible hidden fields.
HTML5 Constraint Validation API
HTML5 introduces the ability for browsers to validate data in forms before submitting to the server.
This capability enables basic validation even when JavaScript is unavailable or fails to load. The
browser itself handles performing the validation based on rules in the code and then displays appro-
priate error messages on its own (without needing additional JavaScript). This functionality works
only in browsers that support this part of HTML5, which includes all modern browsers (except for
Safari) and IE 10+.
Validation is applied to a form field only under certain conditions. You can use HTML markup to
specify constraints on a particular field that will result in the browser automatically performing form
validation.
Required Fields
The first condition is when a form field has a required attribute, as in this example:
Any field marked as required must have a value in order for the form to be submitted. This attribute
applies to , , and fields (Opera through version 11 doesn’t support
required on ). You can check to see if a form field is required in JavaScript by using the
corresponding required property on the element:
let isUsernameRequired = document.forms[0].elements["username"].required;
You can also test to see if the browser supports the required attribute using this code snippet:
let isRequiredSupported = "required" in document.createElement("input");
This code uses simple feature detection to determine if the property required exists on a newly created
element.
Keep in mind that different browsers behave differently when a form field is required. Firefox,
Chrome, IE, and Opera prevent the form from submitting and pop up a help box beneath the field,
while Safari does nothing and doesn’t prevent the form from submitting.
Alternate Input Types
HTML5 specifies several additional values for the type attribute on an element. These type
attributes not only provide additional information about the type of data expected but also provide
some default validation. The two new input types that are most widely supported are "email" and
"url", and each comes with a custom validation that the browser applies. For example:
The "email" type ensures that the input text matches the pattern for an e-mail address, while the
"url" type ensures that the input text matches the pattern for a URL. Note that the browsers
724 ❘ CHAPTER 19 Scripting Forms
As mentioned earlier in this section all have some issues with proper pattern matching. Most nota-
bly, the text "-@-" is considered a valid e-mail address. Such issues are still being addressed with
browser vendors.
You can detect if a browser supports these new types by creating an element in JavaScript and setting
the type property to "email" or "url" and then reading the value back. Older browsers automati-
cally set unknown values back to "text", while supporting browsers echo the correct value back.
For example:
let input = document.createElement("input");
input.type = "email";
let isEmailSupported = (input.type == "email");
Keep in mind that an empty field is also considered valid unless the required attribute is applied.
Also, specifying a special input type doesn’t prevent the user from entering an invalid value; it only
applies some default validation.
Numeric Ranges
In addition to "email" and "url", there are several other new input element types defined in
HTML5. These are all numeric types that expect some sort of numbers-based input: "number",
"range", "datetime", "datetime-local", "date", "month", "week", and "time". These types
are not supported across all major browsers and thus should be used carefully. Browser vendors are
working toward better cross-compatibility and more logical functionality at this time. Therefore, the
information in this section is more forward looking rather than explanatory of existing functionality.
For each of these numeric types, you can specify a min attribute (the smallest possible value), a max
attribute (the largest possible value), and a step attribute (the difference between individual steps
along the scale from min to max). For instance, to allow only multiples of 5 between 0 and 100, you
could use the following:
Depending on the browser, you may or may not see a spin control (up and down buttons) to auto-
matically increment or decrement the browser.
Each of the attributes have corresponding properties on the element that are accessible (and change-
able) using JavaScript. Additionally, there are two methods: stepUp() and stepDown(). These meth-
ods each accept an optional argument: the number to either subtract or add from the current value.
(By default, they increment or decrement by one.) The methods have not yet been implemented by
browsers but will be usable as in this example:
input.stepUp(); // increment by 1
input.stepUp(5); // increment by 5
input.stepDown(); // decrement by 1
input.stepDown(10); // decrement by 10
Input Patterns
The pattern attribute was introduced for text fields in HTML5. This attribute specifies a regular
expression with which the input value must match. For example, to allow only numbers in a text
field, the following code applies this constraint:
Scripting Text Boxes ❘ 725
Note that ^ and $ are assumed at the beginning and end of the pattern, respectively. That means the
input must exactly match the pattern from beginning to end.
As with the alternate input types, specifying a pattern does not prevent the user from entering
inv alid text. The pattern is applied to the value, and the browser then knows if the value is valid or
not. You can read the pattern by accessing the pattern property:
let pattern = document.forms[0].elements["count"].pattern;
You can also test to see if the browser supports the pattern attribute using this code snippet:
let isPatternSupported = "pattern" in document.createElement("input");
Checking Validity
You can check if any given field on the form is valid by using the checkValidity() method. This
method is provided on all elements and returns true if the field’s value is valid or false if not.
Whether or not a field is valid is based on the conditions previously mentioned in this section, so
a required field without a value is considered invalid, and a field whose value does not match the
pattern attribute is considered invalid. For example:
if (document.forms[0].elements[0].checkValidity()){
// field is valid, proceed
} else {
// field is invalid
}
To check if the entire form is valid, you can use the checkValidity() method on the form itself. This
method returns true if all form fields are valid and false if even one is not:
if(document.forms[0].checkValidity()){
// form is valid, proceed
} else {
// form field is invalid
}
While checkValidity() simply tells you if a field is valid or not, the validity property indi-
cates exactly why the field is valid or invalid. This object has a series of properties that return a
Boolean value:
➤➤ customError—true if setCustomValidity() was set, false if not.
➤➤ patternMismatch—true if the value doesn’t match the specified pattern attribute.
➤➤ rangeOverflow—true if the value is larger than the max value.
➤➤ rangeUnderflow—true if the value is smaller than the min value.
➤➤ stepMisMatch—true if the value isn’t correct given the step attribute in combination with
min and max.
➤➤ tooLong—true if the value has more characters than allowed by the maxlength property.
Some browsers, such as Firefox 4, automatically constrain the character count, and so this
value may always be false.
726 ❘ CHAPTER 19 Scripting Forms
➤➤ typeMismatch—value is not in the required format of either "email" or "url".
➤➤ valid—true if every other property is false. Same value that is required by
checkValidity().
➤➤ valueMissing—true if the field is marked as required and there is no value.
Therefore, you may wish to check the validity of a form field using validity to get more specific
information, as in the following code:
if (input.validity && !input.validity.valid){
if (input.validity.valueMissing){
console.log("Please specify a value.")
} else if (input.validity.typeMismatch){
console.log("Please enter an email address.");
} else {
console.log("Value is invalid.");
}
}
Disabling Validation
You can instruct a form not to apply any validation to a form by specifying the novalidate
attribute:
This value can also be retrieved or set by using the JavaScript property noValidate, which is set to
true if the attribute is present and false if the attribute is omitted:
document.forms[0].noValidate = true; //turn off validation
If there are multiple submit buttons in a form, you can specify that the form not validate when a
particular submit button is used by adding the formnovalidate attribute to the button itself:
value="Non-validating Submit">
In this example, the first submit button will cause the form to validate as usual while the second
disables validation when submitting. You can also set this property using JavaScript:
// turn off validation
document.forms[0].elements["btnNoValidate"].formNoValidate = true;
SCRIPTING SELECT BOXES
Select boxes are created using the and elements. To allow for easier interaction
with the control, the HTMLSelectElement type provides the following properties and methods in
addition to those that are available on all form fields:
➤➤ add(newOption, relOption)—Adds a new element to the control before the
related option.
Scripting Select Boxes ❘ 727
➤➤ multiple—A Boolean value indicating if multiple selections are allowed; equivalent to the
HTML multiple attribute.
➤➤ options—An HTMLCollection of elements in the control.
➤➤ remove(index)—Removes the option in the given position.
➤➤ selectedIndex—The zero-based index of the selected option or –1 if no options are
selected. For select boxes that allow multiple selections, this is always the first option in the
selection.
➤➤ size—The number of rows visible in the select box; equivalent to the HTML size attribute.
The type property for a select box is either "select-one" or "select-multiple", depending on
the absence or presence of the multiple attribute. The option that is currently selected determines a
select box’s value property according to the following rules:
➤➤ If there is no option selected, the value of a select box is an empty string.
➤➤ If an option is selected and it has a value attribute specified, then the select box’s value
is the value attribute of the selected option. This is true even if the value attribute is an
empty string.
➤➤ If an option is selected and it doesn’t have a value attribute specified, then the select box’s
value is the text of the option.
➤➤ If multiple options are selected, then the select box’s value is taken from the first selected
option according to the previous two rules.
Consider the following select box:
Sunnyvale
Los Angeles
Mountain View
China
Australia
If the first option in this select box is selected, the value of the field is "Sunnyvale, CA". If the option
with the text "China" is selected, then the field’s value is an empty string because the value attribute
is empty. If the last option is selected, then the value is "Australia" because there is no value attrib-
ute specified on the .
Each element is represented in the DOM by an HTMLOptionElement object. The
HTMLOptionElement type adds the following properties for easier data access:
➤➤ index—The option’s index inside the options collection.
➤➤ label—The option’s label; equivalent to the HTML label attribute.
➤➤ selected—A Boolean value used to indicate if the option is selected. Set this property to
true to select an option.
➤➤ text—The option’s text.
➤➤ value—The option’s value (equivalent to the HTML value attribute).
728 ❘ CHAPTER 19 Scripting Forms
Most of the properties are used for faster access to the option data. Normal DOM func-
tionality can be used to access this information, but it’s quite inefficient, as this example shows:
let selectbox = document.forms[0].elements["location"];
// not recommended // option text
let text = selectbox.options[0].firstChild.nodeValue; // option value
let value = selectbox.options[0].getAttribute("value");
This code gets the text and value of the first option in the select box using standard DOM techniques.
Compare this to using the special option properties:
let selectbox = document.forms[0].elements["location"];
// preferred // option text
let text = selectbox.options[0].text; // option value
let value = selectbox.options[0].value;
When dealing with options, it’s best to use the option-specific properties because they are well
supported across all browsers. The exact interactions of form controls may vary from browser to
browser when manipulating DOM nodes. It is not recommended to change the text or values of
elements by using standard DOM techniques.
As a final note, there is a difference in the way the change event is used for select boxes. As opposed
to other form fields, which fire the change event after the value has changed and the field loses focus,
the change event fires on select boxes as soon as an option is selected.
NOTE There are differences in what the value property returns across browsers.
The value property is always equal to the value attribute in all browsers. When
the value attribute is not specified, Internet Explorer 8 and earlier versions
return an empty string, whereas Internet Explorer 9+, Safari, Firefox, Chrome,
and Opera return the same value as text.
Options Selection
For a select box that allows only one option to be selected, the easiest way to access the selected
option is by using the select box’s selectedIndex property to retrieve the option, as shown in the
following example:
let selectedOption = selectbox.options[selectbox.selectedIndex];
This can be used to display all of the information about the selected option, as in this example:
let selectedIndex = selectbox.selectedIndex;
let selectedOption = selectbox.options[selectedIndex];
console.log('Selected index: $[selectedIndex}\n' +
'Selected text: ${selectedOption.text}\n' +
'Selected value: ${selectedOption.value}');
Here, a log message is displayed showing the selected index along with the text and value of the
selected option.
Scripting Select Boxes ❘ 729
When used in a select box that allows multiple selections, the selectedIndex property acts as if
only one selection was allowed. Setting selectedIndex removes all selections and selects just the
single option specified, whereas getting selectedIndex returns only the index of the first option that
was selected.
Options can also be selected by getting a reference to the option and setting its selected property to
true. For example, the following selects the first option in a select box:
selectbox.options[0].selected = true;
Unlike selectedIndex, setting the option’s selected property does not remove other selections
when used in a multiselect select box, allowing you to dynamically select any number of options. If
an option’s selected property is changed in a single-select select box, then all other selections are
removed. It’s worth noting that setting the selected property to false has no effect in a single-select
select box.
The selected property is helpful in determining which options in a select box are selected. To get
all of the selected options, you can loop over the options collection and test the selected property.
Consider this example:
function getSelectedOptions(selectbox){
let result = new Array();
for (let option of selectbox.options) {
if (option.selected) {
result.push(option);
}
}
return result;
}
This function returns an array of options that are selected in a given select box. First an array to
contain the results is created. Then a for loop iterates over the options, checking each option’s
selected property. If the option is selected, it is added to the result array. The last step is to return
the array of selected options. The getSelectedOptions() function can then be used to get informa-
tion about the selected options, like this:
let selectbox = document.getElementById("selLocation");
let selectedOptions = getSelectedOptions(selectbox);
let message = "";
for (let option of selectedOptions) {
message += 'Selected index: ${option.index}\n' +
'Selected text: ${option.text}\n' +
'Selected value: ${option.value}\n'
}
console.log(message);
In this example, the selected options are retrieved from a select box. A for loop is used to construct a
message containing information about all of the selected options, including each option’s index, text,
and value. This can be used for select boxes that allow single or multiple selection.
730 ❘ CHAPTER 19 Scripting Forms
Adding Options
There are several ways to create options dynamically and add them to select boxes using JavaScript.
The first way is to use the DOM as follows:
let newOption = document.createElement("option");
newOption.appendChild(document.createTextNode("Option text"));
newOption.setAttribute("value", "Option value");
selectbox.appendChild(newOption);
This code creates a new element, adds some text using a text node, sets its value attribute,
and then adds it to a select box. The new option shows up immediately after being created.
New options can also be created using the Option constructor, which is a holdover from pre-DOM
browsers. The Option constructor accepts two arguments, the text and the value, though the
second argument is optional. Even though this constructor is used to create an instance of Object,
DOM-compliant browsers return an element. This means you can still use appendChild()
to add the option to the select box. Consider the following:
let newOption = new Option("Option text", "Option value");
selectbox.appendChild(newOption); // problems in IE <= 8
This approach works as expected in all browsers except Internet Explorer 8 and earlier. Because of a
bug, the browser doesn’t correctly set the text of the new option when using this approach.
Another way to add a new option is to use the select box’s add() method. The DOM specifies that
this method accepts two arguments: the new option to add and the option before which the new
option should be inserted. To add an option at the end of the list, the second argument should be
null. The Internet Explorer 8 and earlier implementation of add() is slightly different in that the
second argument is optional, and it must be the index of the option before which to insert the new
option. DOM-compliant browsers require the second argument, so you can’t use just one argument
for a cross-browser approach (Internet Explorer 9 is DOM-compliant). Instead, passing undefined
as the second argument ensures that the option is added at the end of the list in all browsers. Here’s
an example:
let newOption = new Option("Option text", "Option value");
selectbox.add(newOption, undefined); // best solution
This code works appropriately in all versions of Internet Explorer and DOM-compliant browsers. If
you need to insert a new option into a position other than last, you should use the DOM technique
and insertBefore().
NOTE As in HTML, you are not required to assign a value for an option. The
Option constructor works with just one argument (the option text).
Removing Options
As with adding options, there are multiple ways to remove options. You can use the DOM
r emoveChild() method and pass in the option to remove, as shown here:
selectbox.removeChild(selectbox.options[0]); // remove first option
Scripting Select Boxes ❘ 731
The second way is to use the select box’s remove() method. This method accepts a single argument,
the index of the option to remove, as shown here:
selectbox.remove(0); // remove first option
The last way is to simply set the option equal to null. This is also a holdover from pre-DOM
browsers. Here’s an example:
selectbox.options[0] = null; // remove first option
To clear a select box of all options, you need to iterate over the options and remove each one, as in
this example:
function clearSelectbox(selectbox) {
for (let option of selectbox.options) {
selectbox.remove(0);
}
}
This function simply removes the first option in a select box repeatedly. Because removing the first
option automatically moves all of the options up one spot, this removes all options.
Moving and Reordering Options
Before the DOM, moving options from one select box to another was a rather arduous process that
involved removing the option from the first select box, creating a new option with the same name
and value, and then adding that new option to the second select box. Using DOM methods, it’s
possible to literally move an option from the first select box into the second select box by using the
appendChild() method. If you pass an element that is already in the document into this method,
the element is removed from its parent and put into the position specified. For example, the following
code moves the first option from one select box into another select box.
let selectbox1 = document.getElementById("selLocations1");
let selectbox2 = document.getElementById("selLocations2");
selectbox2.appendChild(selectbox1.options[0]);
Moving options is the same as removing them in that the index property of each option is reset.
Reordering options is very similar, and DOM methods are the best way to accomplish this. To move
an option to a particular location in the select box, the insertBefore() method is most appropriate,
though the appendChild() method can be used to move any option to the last position. To move an
option up one spot in the select box, you can use the following code:
let optionToMove = selectbox.options[1];
selectbox.insertBefore(optionToMove,
selectbox.options[optionToMove.index-1]);
In this code, an option is selected to move and then inserted before the option that is in the previous
index. The second line of code is generic enough to work with any option in the select box except the
first. The following similar code can be used to move an option down one spot:
let optionToMove = selectbox.options[1];
selectbox.insertBefore(optionToMove,
selectbox.options[optionToMove.index+2]);
This code works for all options in a select box, including the last one.
732 ❘ CHAPTER 19 Scripting Forms
FORM SERIALIZATION
With the emergence of Ajax (discussed further in Chapter 21), form serialization has become a
common requirement. A form can be serialized in JavaScript using the type property of form fields
in conjunction with the name and value properties. Before writing the code, you need to understand
how the browser determines what gets sent to the server during a form submission:
➤➤ Field names and values are URL-encoded and delimited using an ampersand.
➤➤ Disabled fields aren’t sent at all.
➤➤ A check box or radio field is sent only if it is checked.
➤➤ Buttons of type "reset" or "button" are never sent.
➤➤ Multiselect fields have an entry for each value selected.
➤➤ When the form is submitted by clicking a submit button, that submit button is sent;
otherwise no submit buttons are sent. Any elements with a type of "image" are
treated the same as submit buttons.
➤➤ The value of a element is the value attribute of the selected element.
If the element doesn’t have a value attribute, then the value is the text of the
element.
Form serialization typically doesn’t include any button fields, because the resulting string will most
likely be submitted in another way. All of the other rules should be followed. The code to accomplish
form serialization is as follows:
function serialize(form) {
let parts = [];
let optValue;
for (let field of form.elements) {
switch(field.type) {
case "select-one":
case "select-multiple":
if (field.name.length) {
for (let option of field.options) {
if (option.selected) {
if (option.hasAttribute){
optValue = (option.hasAttribute("value") ?
option.value : option.text);
} else {
optValue = (option.attributes["value"].specified ?
option.value : option.text);
}
parts.push(encodeURIComponent(field.name)} + "=" +
encodeURIComponent(optValue));
}
}
}
Form Serialization ❘ 733
break;
case undefined: // fieldset
case "file": // file input
case "submit": // submit button
case "reset": // reset button
case "button": // custom button
break;
case "radio": // radio button
case "checkbox": // checkbox
if (!field.checked) {
break;
}
default:
// don't include form fields without names
if (field.name.length) {
parts.push('${encodeURIComponent(field.name)}=' +
'${encodeURIComponent(field.value)}');
}
}
return parts.join("&");
}
The serialize() function begins by defining an array called parts to hold the parts of the string
that will be created. Next, a for loop iterates over each form field, storing it in the field variable.
Once a field reference is obtained, its type is checked using a switch statement. The most involved
field to serialize is the element, in either single-select or multiselect mode. Serialization is
done by looping over all of the options in the control and adding a value if the option is selected. For
single-select controls, there will be only one option selected, whereas multiselect controls may have
zero or more options selected. The same code can be used for both select types, because the restric-
tion on the number of selections is enforced by the browser. When an option is selected, you need to
determine which value to use. If the value attribute is not present, the text should be used instead,
although a value attribute with an empty string is completely valid. To check this, you’ll need to use
hasAttribute() in DOM-compliant browsers and the attribute’s specified property in Internet
Explorer 8 and earlier.
If a
element is in the form, it appears in the elements collection but has no type prop-erty. So if type is undefined, no serialization is necessary. The same is true for all types of buttons
and file input fields. (File input fields contain the content of the file in form submissions; however,
these fields can’t be mimicked, so they are typically omitted in serialization.) For radio and check box
controls, the checked property is inspected and if it is set to false, the switch statement is exited. If
checked is true, then the code continues executing in the default statement, which encodes the name
and value of the field and adds it to the parts array. Note that in all cases form fields without names
are not included as part of the serialization to mimic browser form submission behavior. The last part
of the function uses join() to format the string correctly with ampersands between fields.
The serialize() function outputs the string in query string format, though it can easily be adapted
to serialize the form into another format.
734 ❘ CHAPTER 19 Scripting Forms
RICH TEXT EDITING
One of the most requested features for web applications was the ability to edit rich text on a web
page (also called what you see is what you get, or WYSIWYG, editing). Though no specification
covers this, a de facto standard has emerged from functionality originally introduced by Internet
Explorer and now supported by Opera, Safari, Chrome, and Firefox. The basic technique is to embed
an iframe containing a blank HTML file in the page. Through the designMode property, this blank
document can be made editable, at which point you’re editing the HTML of the page’s
element. The designMode property has two possible values: "off" (the default) and "on". When set
to "on", an entire document becomes editable (showing a caret), allowing you to edit text as if you
were using a word processor complete with keystrokes for making text bold, italic, and so forth.
A very simple, blank HTML page is used as the source of the iframe. Here’s an example:
Blank Page for Rich Text EditingThis page is loaded inside an iframe as any other page would be. To allow it to be edited, you
must set designMode to "on", but this can happen only after the document is fully loaded. In the
containing page, you’ll need to use the onload event handler to indicate the appropriate time to set
designMode, as shown in the following example:
window.addEventListener("load", () => {
frames["richedit"].document.designMode = "on";
});
Once this code is loaded, you’ll see what looks like a text box on the page. The box has the same
default styling as any web page, though this can be adjusted by applying CSS to the blank page.
Using contenteditable
Another way to interact with rich text, also first implemented by Internet Explorer, is through the use
of a special attribute called contenteditable. The contenteditable attribute can be applied to any
element on a page and instantly makes that element editable by the user. This approach has gained
favor because it doesn’t require the overhead of an iframe, blank page, and JavaScript. Instead, you
can just add the attribute to an element:
Rich Text Editing ❘ 735
Any text already contained within the element is automatically made editable by the user, making
it behave similarly to the element. You can also toggle the editing mode on or off by
setting the contentEditable property on an element:
let div = document.getElementById("richedit");
richedit.contentEditable = "true";
There are three possible values for contentEditable: "true" to turn on, "false" to turn off, or
"inherit" to inherit the setting from a parent (required because elements can be created/destroyed
inside of a contenteditable element). The contentEditable attribute is supported in Internet
Explorer, Firefox, Chrome, Safari, and Opera, and on all major mobile browsers.
NOTE contenteditable is an extremely versatile attribute. For example, you’re
able to convert your browser window into a notepad by visiting the pseudo-URL
data:text/html, . This creates an ad-hoc DOM with
the entire document set to editable.
Interacting with Rich Text
The primary method of interacting with a rich text editor is through the use of document
.execCommand(). This method executes named commands on the document and can be used to
apply most formatting changes. There are three possible arguments for document.execCommand():
the name of the command to execute, a Boolean value indicating if the browser should provide a
user interface for the command, and a value necessary for the command to work (or null if none is
necessary). The second argument should always be false for cross-browser compatibility, because
Firefox throws an error when true is passed in.
Each browser supports a different set of commands. The most commonly supported commands are
listed in the following table.
COMMAND VALUE (THIRD ARGUMENT) DESCRIPTION
backcolor A color string Sets the background color of
the document.
bold null
copy null Toggles bold text for the text selection.
createlink A URL string Executes a clipboard copy on the text
selection.
cut null
Turns the current text selection into a
link that goes to the given URL.
Executes a clipboard cut on the text
selection.
continues
736 ❘ CHAPTER 19 Scripting Forms
(continued) VALUE (THIRD ARGUMENT) DESCRIPTION
COMMAND null Deletes the currently selected text.
The font name
delete Changes the text selection to use the
fontname 1 through 7 given font name.
fontsize A color string Changes the font size for the text
selection.
forecolor The HTML tag to
surround the block with; Changes the text color for the text
formatblock for example,
selection.
null
indent null Formats the entire text box around the
inserthorizontalrule selection with a particular HTML tag.
The image URL
insertimage null Indents the text.
insertorderedlist
null Inserts an
element at the
insertparagraph caret location.
null
insertunorderedlist Inserts an image at the caret location.
null
italic null Inserts an
- element at the
justifycenter caret location.
null
justifyleft Inserts a
element at the
null caret location.
outdent null
paste Inserts a
- element at the
null caret location.
removeformat
Toggles italic text for the text selection.
Centers the block of text in which the
caret is positioned.
Left-aligns the block of text in which the
caret is positioned.
Outdents the text.
Executes a clipboard paste on the text
selection.
Removes block formatting from the
block in which the caret is positioned.
This is the opposite of formatblock.
Rich Text Editing ❘ 737
COMMAND VALUE (THIRD ARGUMENT) DESCRIPTION
selectall null Selects all of the text in the document.
underline null
Toggles underlined text for the text
unlink null selection.
Removes a text link. This is the opposite
of createlink.
The clipboard commands are very browser-dependent. Note that even though these commands aren’t
all available via document.execCommand(), they still work with the appropriate keyboard shortcuts.
These commands can be used at any time to modify the appearance of the iframe rich text area, as in
this example:
// toggle bold text in an iframe
frames["richedit"].document.execCommand("bold", false, null);
// toggle italic text in an iframe
frames["richedit"].document.execCommand("italic", false, null);
// create link to www.wrox.com in an iframe
frames["richedit"].document.execCommand("createlink", false,
"http://www.wrox.com");
// format as first-level heading in an iframe
frames["richedit"].document.execCommand("formatblock", false, "
");
You can use the same methods to act on a contenteditable section of the page; just use the
document object of the current window instead of referencing the iframe:
// toggle bold text
document.execCommand("bold", false, null);
// toggle italic text
document.execCommand("italic", false, null);
// create link to www.wrox.com
document.execCommand("createlink", false, "http://www.wrox.com");
// format as first-level heading
document.execCommand("formatblock", false, "
");
Note that even when commands are supported across all browsers, the HTML that the commands
produce is often very different. For instance, applying the bold command surrounds text with
in Internet Explorer and Opera, with in Safari and Chrome, and with a in
Firefox. You cannot rely on consistency in the HTML produced from a rich text editor, because of
both command implementation and the transformations done by innerHTML.
738 ❘ CHAPTER 19 Scripting Forms
There are some other methods related to commands. The first is queryCommandEnabled(), which
determines if a command can be executed given the current text selection or caret position. This
method accepts a single argument, the command name to check, and returns true if the command is
allowed given the state of the editable area or false if not. Consider this example:
let result = frames["richedit"].document.queryCommandEnabled("bold");
This code returns true if the "bold" command can be executed on the current selection. It’s worth
noting that queryCommandEnabled() indicates not if you are allowed to execute the
command but only if the current selection is appropriate for use with the command. In Firefox,
queryCommandEnabled("cut") returns true even though it isn’t allowed by default.
The queryCommandState() method lets you determine if a given command has been applied to the
current text selection. For example, to determine if the text in the current selection is bold, you can
use the following:
let isBold = frames["richedit"].document.queryCommandState("bold");
If the "bold" command was previously applied to the text selection, then this code returns true.
This is the method by which full-featured rich text editors are able to update buttons for bold, italic,
and so on.
The last method is queryCommandValue(), which is intended to return the value with which a
command was executed. (The third argument in execCommand is in the earlier example.) For instance,
a range of text that has the "fontsize" command applied with a value of 7 returns "7" from the
following:
let fontSize = frames["richedit"].document.queryCommandValue("fontsize");
This method can be used to determine how a command was applied to the text selection, allowing
you to determine whether the next command is appropriate to be executed.
Rich Text Selections
You can determine the exact selection in a rich text editor by using the getSelection() method of
the iframe. This method is available on both the document object and the window object and returns
a Selection object representing the currently selected text. Each Selection object has the following
properties:
➤➤ anchorNode—The node in which the selection begins.
➤➤ anchorOffset—The number of characters within the anchorNode that are skipped before
the selection begins.
➤➤ focusNode—The node in which the selection ends.
➤➤ focusOffset—The number of characters within the focusNode that are included in the
selection.
➤➤ isCollapsed—Boolean value indicating if the start and end of the selection are the same.
➤➤ rangeCount—The number of DOM ranges in the selection.
Rich Text Editing ❘ 739
The properties for a Selection don’t contain a lot of useful information. Fortunately, the following
methods provide more information and allow manipulation of the selection:
➤➤ addRange(range)—Adds the given DOM range to the selection.
➤➤ collapse(node, offset)—Collapses the selection to the given text offset within the
given node.
➤➤ collapseToEnd()—Collapses the selection to its end.
➤➤ collapseToStart()—Collapses the selection to its start.
➤➤ containsNode(node)—Determines if the given node is contained in the selection.
➤➤ deleteFromDocument()—Deletes the selection text from the document. This is the same as
execCommand("delete", false, null).
➤➤ extend(node, offset)—Extends the selection by moving the focusNode and focusOffset
to the values specified.
➤➤ getRangeAt(index)—Returns the DOM range at the given index in the selection.
➤➤ removeAllRanges()—Removes all DOM ranges from the selection. This effectively removes
the selection, because there must be at least one range in a selection.
➤➤ removeRange(range)—Removes the specified DOM range from the selection.
➤➤ selectAllChildren(node)—Clears the selection and then selects all child nodes of the
given node.
➤➤ toString()—Returns the text content of the selection.
The methods of a Selection object are extremely powerful and make extensive use of DOM ranges
to manage the selection. Access to DOM ranges allows you to modify the contents of the rich text
editor in even finer-grain detail than is available using execCommand() because you can directly
manipulate the DOM of the selected text. Consider the following example:
let selection = frames["richedit"].getSelection();
// get selected text
let selectedText = selection.toString();
// get the range representing the selection
let range = selection.getRangeAt(0);
// highlight the selected text
let span = frames["richedit"].document.createElement("span");
span.style.backgroundColor = "yellow";
range.surroundContents(span);
This code places a yellow highlight around the selected text in a rich text editor. Using the DOM
range in the default selection, the surroundContents() method surrounds the selection with a
element whose background color is yellow.
740 ❘ CHAPTER 19 Scripting Forms
The getSelection() method was standardized in HTML5 and is implemented in Internet Explorer 9
and all modern versions of Firefox, Safari, Chrome, and Opera.
Internet Explorer 8 and earlier versions don’t support DOM ranges, but they do allow interaction
with the selected text via the proprietary selection object. The selection object is a property of
document, as discussed earlier in this chapter. To get the selected text in a rich text editor, you must
first create a text range and then use the text property as follows:
let range = frames["richedit"].document.selection.createRange();
let selectedText = range.text;
Performing HTML manipulations using Internet Explorer text ranges is not as safe as using DOM
ranges, but it is possible. To achieve the same highlighting effect as described using DOM ranges, you
can use a combination of the htmlText property and the pasteHTML() method:
let range = frames["richedit"].document.selection.createRange();
range.pasteHTML(
'${range.htmlText}');
This code retrieves the HTML of the current selection using htmlText and then surrounds it with a
and inserts it back into the selection using pasteHTML().
Rich Text in Forms
Because rich text editing is implemented using an iframe or a contenteditable element instead
of a form control, a rich text editor is technically not part of a form. That means the HTML will
not be submitted to the server unless you extract the HTML manually and submit it yourself. This
is typically done by having a hidden form field that is updated with the HTML from the iframe or
the contenteditable element. Just before the form is submitted, the HTML is extracted from the
iframe or element and inserted into the hidden field. For example, the following may be done in the
form’s onsubmit event handler when using an iframe:
form.addEventListener("submit", (event) => {
let target = event.target;
target.elements["comments"].value =
frames["richedit"].document.body.innerHTML;
});
Here, the HTML is retrieved from the iframe using the innerHTML property of the document’s
body and inserted into a form field named "comments". Doing so ensures that the "comments"
field is filled in just before the form is submitted. If you are submitting the form manually using the
s ubmit() method, take care to perform this operation beforehand. You can perform a similar operation
with a contenteditable element:
form.addEventListener("submit", (event) => {
let target = event.target;
target.elements["comments"].value =
document.getElementById("richedit").innerHTML;
});
Summary ❘ 741
SUMMARY
Even though HTML and web applications have changed dramatically since their inception, web
forms have remained mostly unchanged. JavaScript can be used to augment existing form fields to
provide new functionality and usability enhancements. To aid in this, forms and form fields have
properties, methods, and events for JavaScript usage. Here are some of the concepts introduced in
this chapter:
➤➤ It’s possible to select all of the text in a text box or just part of the text using a variety of
standard and nonstandard methods.
➤➤ All browsers have adopted Firefox’s way of interacting with text selection, making it a
true standard.
➤➤ Text boxes can be changed to allow or disallow certain characters by listening for keyboard
events and inspecting the characters being inserted.
All browsers support events for the clipboard, including copy, cut, and paste. Clipboard event
implementations across the other browsers vary widely between browser vendors.
Hooking into clipboard events is useful for blocking paste events when the contents of a text box
must be limited to certain characters.
Select boxes are also frequently controlled using JavaScript. Thanks to the DOM, manipulating select
boxes is much easier than it was previously. Options can be added, removed, moved from one select box
to another, or reordered using standard DOM techniques.
Rich text editing is handled by using an iframe containing a blank HTML document. By setting
the document’s designMode property to "on", you make the page editable and it acts like a word
processor. You can also use an element set as contenteditable. By default, you can toggle font
styles such as bold and italic and use clipboard actions. JavaScript can access some of this func-
tionality by using the execCommand() method and can get information about the text selection by
using the queryCommandEnabled(), queryCommandState(), and queryCommandValue() methods.
Because building a rich text editor in this manner does not create a form field, it’s necessary to copy
the HTML from the iframe or contenteditable element into a form field if it is to be submitted to
the server.
20
JavaScript APIs
WHAT’S IN THIS CHAPTER?
➤➤ Atomics and SharedArrayBuffer
➤➤ Cross-context messaging
➤➤ Encoding API
➤➤ File and Blob API
➤➤ Drag and drop
➤➤ Notifications API
➤➤ Page Visibility API
➤➤ Streams API
➤➤ Timing APIs
➤➤ Web components
➤➤ Web Cryptography API
The increasing versatility of web browsers is accompanied by a dizzying increase in complex-
ity. In many ways, the modern web browser has become a Swiss army knife of different APIs
detailed in a broad collection of specifications. This browser specification ecosystem is messy
and volatile. Some specifications like HTML5 are a bundle of APIs and browser features that
enhance an existing standard. Other specifications define an API for a single feature, such as the
Web Cryptography API or the Notifications API. Depending on the browser, adoption of these
newer APIs can sometimes be partial or nonexistent.
Ultimately, the decision to use newer APIs involves a tradeoff between supporting more brows-
ers and enabling more modern features. Some APIs can be emulated using a polyfill, but poly-
fills can often incur a performance hit or bloat your site’s JS payloads.
Professional JavaScript® for Web Developers, Fourth Edition. Matt Frisbie.
© 2020 John Wiley & Sons, Inc. Published 2020 by John Wiley & Sons, Inc.
744 ❘ CHAPTER 20 JavaScript APIs
NOTE The number of web APIs is mind-bogglingly huge (https://developer.
mozilla.org/en-US/docs/Web/API). This chapter’s API coverage is limited to
APIs that are relevant to most developers, supported by multiple browser ven-
dors, and not covered elsewhere in this book.
ATOMICS AND SharedArrayBuffer
When a SharedArrayBuffer is accessed by multiple contexts, race conditions can occur when opera-
tions on the buffer are performed simultaneously. The Atomics API allows multiple contexts to safely
read and write to a single SharedArrayBuffer by forcing buffer operations to occur only one at a
time. The Atomics API was defined in the ES2017 specification.
You will notice that the Atomics API in many ways resembles a stripped-down instruction set archi-
tecture (ISA)—this is no accident. The nature of atomic operations precludes some optimizations
that the operating system or computer hardware would normally perform automatically (such as
instruction reordering). Atomic operations also make concurrent memory access impossible, which
obviously can slow program execution when improperly applied. Therefore, the Atomics API was
designed to enable sophisticated multithreaded JavaScript programs to be architected out of a mini-
mal yet robust collection of atomic behaviors.
SharedArrayBuffer
A SharedArrayBuffer features an identical API to an ArrayBuffer. The primary difference is that,
whereas a reference to an ArrayBuffer must be handed off between execution contexts, a reference
to a SharedArrayBuffer can be used simultaneously by any number of execution contexts.
Sharing memory between multiple execution contexts means that concurrent thread operations
become a possibility. Traditional JavaScript operations offer no protection from race conditions
resulting from concurrent memory access. The following example demonstrates a race condition
between four dedicated workers accessing the same SharedArrayBuffer:
const workerScript = `
self.onmessage = ({data}) => {
const view = new Uint32Array(data);
// Perform 1000000 add operations
for (let i = 0; i < 1E6; ++i) {
// Thread-unsafe add operation introduces race condition
view[0] += 1;
}
self.postMessage(null);
};
`;
const workerScriptBlobUrl = URL.createObjectURL(new Blob([workerScript]));
Atomics and SharedArrayBuffer ❘ 745
// Create worker pool of size 4
const workers = [];
for (let i = 0; i < 4; ++i) {
workers.push(new Worker(workerScriptBlobUrl));
}
// Log the final value after the last worker completes
let responseCount = 0;
for (const worker of workers) {
worker.onmessage = () => {
if (++responseCount == workers.length) {
console.log(`Final buffer value: ${view[0]}`);
}
};
}
// Initialize the SharedArrayBuffer
const sharedArrayBuffer = new SharedArrayBuffer(4);
const view = new Uint32Array(sharedArrayBuffer);
view[0] = 1;
// Send the SharedArrayBuffer to each worker
for (const worker of workers) {
worker.postMessage(sharedArrayBuffer);
}
// (Expected result is 4000001. Actual output will be something like:)
// Final buffer value: 2145106
To address this problem, the Atomics API was introduced to allow for thread-safe JavaScript opera-
tions on a SharedArrayBuffer.
NOTE The SharedArrayBuffer API is identical to the ArrayBuffer API, which
is covered in the “Collection Reference Types” chapter. For details on how to use
a SharedArrayBuffer across multiple contexts, refer to the “Workers” chapter.
Atomics Basics
The Atomics object exists on all global contexts, and it exposes a suite of static methods for perform-
ing thread-safe operations. Most of these methods take a TypedArray instance (referencing a Shar-
edArrayBuffer) as the first argument and the relevant operands as subsequent arguments.
Atomic Arithmetic and Bitwise Methods
The Atomics API offers a simple suite of methods for performing an in-place modification. In the
ECMA specification, these methods are defined as AtomicReadModifyWrite operations. Under the
hood, each of these methods is performing a read from a location in the SharedArrayBuffer, an
arithmetic or bitwise operation, and a write to the same location. The atomic nature of these opera-
tors means that these three operations will be performed in sequence and without interruption by
another thread.
746 ❘ CHAPTER 20 JavaScript APIs
All the arithmetic methods are demonstrated here:
// Create buffer of size 1
let sharedArrayBuffer = new SharedArrayBuffer(1);
// Create Uint8Array from buffer
let typedArray = new Uint8Array(sharedArrayBuffer);
// All ArrayBuffers are initialized to 0
console.log(typedArray); // Uint8Array[0]
const index = 0;
const increment = 5;
// Atomic add 5 to value at index 0
Atomics.add(typedArray, index, increment);
console.log(typedArray); // Uint8Array[5]
// Atomic subtract 5 to value at index 0
Atomics.sub(typedArray, index, increment);
console.log(typedArray); // Uint8Array[0]
All the bitwise methods are demonstrated here:
// Create buffer of size 1
let sharedArrayBuffer = new SharedArrayBuffer(1);
// Create Uint8Array from buffer
let typedArray = new Uint8Array(sharedArrayBuffer);
// All ArrayBuffers are initialized to 0
console.log(typedArray); // Uint8Array[0]
const index = 0;
// Atomic or 0b1111 to value at index 0
Atomics.or(typedArray, index, 0b1111);
console.log(typedArray); // Uint8Array[15]
// Atomic and 0b1100 to value at index 0
Atomics.and(typedArray, index, 0b1100);
console.log(typedArray); // Uint8Array[12]
// Atomic xor 0b1111 to value at index 0
Atomics.xor(typedArray, index, 0b1111);
console.log(typedArray); // Uint8Array[3]
The thread-unsafe example from earlier can be corrected as follows:
const workerScript = `
self.onmessage = ({data}) => {
Atomics and SharedArrayBuffer ❘ 747
const view = new Uint32Array(data);
// Perform 1000000 add operations
for (let i = 0; i < 1E6; ++i) {
// Thread-safe add operation
Atomics.add(view, 0, 1);
}
self.postMessage(null);
};
`;
const workerScriptBlobUrl = URL.createObjectURL(new Blob([workerScript]));
// Create worker pool of size 4
const workers = [];
for (let i = 0; i < 4; ++i) {
workers.push(new Worker(workerScriptBlobUrl));
}
// Log the final value after the last worker completes
let responseCount = 0;
for (const worker of workers) {
worker.onmessage = () => {
if (++responseCount == workers.length) {
console.log(`Final buffer value: ${view[0]}`);
}
};
}
// Initialize the SharedArrayBuffer
const sharedArrayBuffer = new SharedArrayBuffer(4);
const view = new Uint32Array(sharedArrayBuffer);
view[0] = 1;
// Send the SharedArrayBuffer to each worker
for (const worker of workers) {
worker.postMessage(sharedArrayBuffer);
}
// (Expected result is 4000001)
// Final buffer value: 4000001
Atomic Reads and Writes
Both the browser’s JavaScript compiler and the CPU architecture itself are given license to reorder
instructions if they detect it will increase the overall throughput of program execution. Normally, the
single-threaded nature of JavaScript means this optimization should be welcomed with open arms.
However, instruction reordering across multiple threads can yield race conditions that are extremely
difficult to debug.
748 ❘ CHAPTER 20 JavaScript APIs
The Atomics API addresses this problem in two primary ways:
➤➤ All Atomics instructions are never reordered with respect to one another.
➤➤ Using an Atomic read or Atomic write guarantees that all instructions (both Atomic and
non-Atomic) will never be reordered with respect to that Atomic read/write. This means
that all instructions before an Atomic read/write will finish before the Atomic read/write
occurs, and all instructions after the Atomic read/write will not begin until the Atomic
read/write completes.
In addition to reading and writing values to a buffer, Atomics.load() and Atomics.store() behave
as “code fences.” The JavaScript engine guarantees that, although non-Atomic instructions may be
locally reordered relative to a load() or store(), the reordering will never violate the Atomic read/
write boundary. The following code annotates this behavior:
const sharedArrayBuffer = new SharedArrayBuffer(4);
const view = new Uint32Array(sharedArrayBuffer);
// Perform non-Atomic write
view[0] = 1;
// Non-Atomic write is guaranteed to occur before this read,
// so this is guaranteed to read 1
console.log(Atomics.load(view, 0)); // 1
// Perform Atomic write
Atomics.store(view, 0, 2);
// Non-Atomic read is guaranteed to occur after Atomic write,
// so this is guaranteed to read 2
console.log(view[0]); // 2
Atomic Exchanges
The Atomics API offers two types of methods that guarantee a sequential and uninterrupted
read-then-write: exchange() and compareExchange(). Atomics.exchange() performs a simple
swap, guaranteeing that no other threads will interrupt the value swap:
const sharedArrayBuffer = new SharedArrayBuffer(4);
const view = new Uint32Array(sharedArrayBuffer);
// Write 3 to 0-index
Atomics.store(view, 0, 3);
// Read value out of 0-index and then write 4 to 0-index
console.log(Atomics.exchange(view, 0, 4)); // 3
// Read value at 0-index // 4
console.log(Atomics.load(view, 0));
One thread in a multithreaded program might want to perform a write to a shared buffer only if
another thread has not modified a specific value since it was last read. If the value has not changed,
Atomics and SharedArrayBuffer ❘ 749
it can safely write the update value. If the value has changed, performing a write would trample the
value calculated by another thread. For this task, the Atomics API features the compareExchange()
method. This method only performs a write if the value at the intended index matches an expected
value. Consider the following example:
const sharedArrayBuffer = new SharedArrayBuffer(4);
const view = new Uint32Array(sharedArrayBuffer);
// Write 5 to 0-index
Atomics.store(view, 0, 5);
// Read the value out of the buffer
let initial = Atomics.load(view, 0);
// Perform a non-atomic operation on that value
let result = initial ** 2;
// Write that value back into the buffer only if the buffer has not changed
Atomics.compareExchange(view, 0, initial, result);
// Check that the write succeeded
console.log(Atomics.load(view, 0)); // 25
If the value does not match, the compareExchange() call will simply behave as a passthrough:
const sharedArrayBuffer = new SharedArrayBuffer(4);
const view = new Uint32Array(sharedArrayBuffer);
// Write 5 to 0-index
Atomics.store(view, 0, 5);
// Read the value out of the buffer
let initial = Atomics.load(view, 0);
// Perform a non-atomic operation on that value
let result = initial ** 2;
// Write that value back into the buffer only if the buffer has not changed
Atomics.compareExchange(view, 0, -1, result);
// Check that the write failed
console.log(Atomics.load(view, 0)); // 5
Atomics Futex Operations and Locks
Multithreaded programs wouldn’t amount to much without some sort of locking construct. To
address this need, the Atomics API offers several methods modeled on the Linux futex (a portmanteau
of fast user-space mutex). The methods are fairly rudimentary, but they are intended to be used as a
fundamental building block for more elaborate locking constructs.
NOTE All Atomics futex operations only work with an Int32Array view. Fur-
thermore, they can only be used inside workers.
750 ❘ CHAPTER 20 JavaScript APIs
Atomics.wait() and Atomics.notify() are best understood by example. The following rudimen-
tary example spawns four workers to operate on an Int32Array of length 1. The spawned workers
will take turns obtaining the lock and performing their add operation:
const workerScript = `
self.onmessage = ({data}) => {
const view = new Int32Array(data);
console.log('Waiting to obtain lock');
// Halt when encountering the initial value, timeout at 10000ms
Atomics.wait(view, 0, 0, 1E5);
console.log('Obtained lock');
// Add 1 to data index
Atomics.add(view, 0, 1);
console.log('Releasing lock');
// Allow exactly one worker to continue execution
Atomics.notify(view, 0, 1);
self.postMessage(null);
};
`;
const workerScriptBlobUrl = URL.createObjectURL(new Blob([workerScript]));
const workers = [];
for (let i = 0; i < 4; ++i) {
workers.push(new Worker(workerScriptBlobUrl));
}
// Log the final value after the last worker completes
let responseCount = 0;
for (const worker of workers) {
worker.onmessage = () => {
if (++responseCount == workers.length) {
console.log(`Final buffer value: ${view[0]}`);
}
};
}
// Initialize the SharedArrayBuffer
const sharedArrayBuffer = new SharedArrayBuffer(8);
const view = new Int32Array(sharedArrayBuffer);
// Send the SharedArrayBuffer to each worker
for (const worker of workers) {
worker.postMessage(sharedArrayBuffer);
}
Cross-Context Messaging ❘ 751
// Release first lock in 1000ms
setTimeout(() => Atomics.notify(view, 0, 1), 1000);
// Waiting to obtain lock
// Waiting to obtain lock
// Waiting to obtain lock
// Waiting to obtain lock
// Obtained lock
// Releasing lock
// Obtained lock
// Releasing lock
// Obtained lock
// Releasing lock
// Obtained lock
// Releasing lock
// Final buffer value: 4
Because the SharedArrayBuffer is initialized with 0s, each worker will arrive at the Atomics.
wait() and halt execution. In the halted state, the thread of execution exists inside a wait queue,
remaining paused until the specified timeout elapses or until Atomics.notify() is invoked for that
index. After 1000 milliseconds, the top-level execution context will call Atomics.notify() to release
exactly one of the waiting threads. This thread will finish execution and call Atomics.notify() once
again, releasing yet another thread. This continues until all the threads have completed execution and
transmitted their final postMessage().
The Atomics API also features the Atomics.isLockFree() method. It is almost certain that you will
never need to use this method, as it is designed for high-performance algorithms to decide whether or
not obtaining a lock is necessary. The specification offers this description:
Atomics.isLockFree() is an optimization primitive. The intuition is that if the
atomic step of an atomic primitive (compareExchange, load, store, add, sub, and,
or, xor, or exchange) on a datum of size n bytes will be performed without the
calling agent acquiring a lock outside the n bytes comprising the datum, then Atom-
ics.isLockFree(n) will return true. High-performance algorithms will use Atom-
ics.isLockFree to determine whether to use locks or atomic operations in critical
sections. If an atomic primitive is not lock-free then it is often more efficient for an
algorithm to provide its own locking.
Atomics.isLockFree(4) always returns true as that can be supported on
all known relevant hardware. Being able to assume this will generally sim-
plify programs.
CROSS-CONTEXT MESSAGING
Cross-document messaging, sometimes abbreviated as XDM, is the capability to pass informa-
tion between different execution contexts, such as web workers or pages from different origins. For
example, a page on www.wrox.com wants to communicate with a page from p2p.wrox.com that is
752 ❘ CHAPTER 20 JavaScript APIs
contained in an iframe. Prior to XDM, achieving this communication in a secure manner took a lot of
work. XDM formalizes this functionality in a way that is both secure and easy to use.
NOTE Cross-context messaging is used for communication between windows
and communication with workers. This section focuses on using postMessage()
to communicate with other windows. For coverage on worker messaging, Mes-
sageChannel, and BroadcastChannel, refer to the “Workers” chapter.
At the heart of XDM is the postMessage() method. This method name is used in many parts
of HTML5 in addition to XDM and is always used for the same purpose: to pass data into
another location.
The postMessage() method accepts three arguments: a message, a string indicating the intended
recipient origin, and an optional array of transferable objects (only relevant to web workers). The
second argument is very important for security reasons and restricts where the browser will deliver
the message. Consider this example:
let iframeWindow = document.getElementById("myframe").contentWindow;
iframeWindow.postMessage("A secret", "http://www.wrox.com");
The last line attempts to send a message into the iframe and specifies that the origin must be
"http://www.wrox.com". If the origin matches, then the message will be delivered into the iframe;
otherwise, postMessage() silently does nothing. This restriction protects your information should
the location of the window change without your knowledge. It is possible to allow posting to any
origin by passing in "*" as the second argument to postMessage(), but this is not recommended.
A message event is fired on window when an XDM message is received. This message is fired asyn-
chronously so there may be a delay between the time at which the message was sent and the time
at which the message event is fired in the receiving window. The event object that is passed to an
onmessage event handler has three important pieces of information:
➤➤ data—The string data that was passed as the first argument to postMessage().
➤➤ origin—The origin of the document that sent the message, for example, "http://www.
wrox.com".
➤➤ source—A proxy for the window object of the document that sent the message. This
proxy object is used primarily to execute the postMessage() method on the window that
sent the last message. If the sending window has the same origin, this may be the actual
window object.
It’s very important when receiving a message to verify the origin of the sending window. Just as
specifying the second argument to postMessage() ensures that data doesn’t get passed unintention-
ally to an unknown page, checking the origin during onmessage ensures that the data being passed is
coming from the right place. The basic pattern is as follows:
window.addEventListener("message", (event) => {
// ensure the sender is expected
if (event.origin == "http://www.wrox.com") {
Encoding API ❘ 753
// do something with the data
processMessage(event.data);
// optional: send a message back to the original window
event.source.postMessage("Received!", "http://p2p.wrox.com");
}
});
Keep in mind that event.source is a proxy for a window in most cases, not the actual window object,
so you can’t access all of the window information. It’s best to just use postMessage(), which is
always present and always callable.
There are a few quirks with XDM. First, the first argument of postMessage() was initially imple-
mented as always being a string. The definition of that first argument changed to allow any struc-
tured data to be passed in; however, not all browsers have implemented this change. For this reason,
it’s best to always pass a string using postMessage(). If you need to pass structured data, then the
best approach is to call JSON.stringify() on the data, passing the string to postMessage(), and
then call JSON.parse() in the onmessage event handler.
XDM is extremely useful when trying to sandbox content using an iframe to a different domain. This
approach is frequently used in mashups and social networking applications. The containing page is
able to keep itself secure against malicious content by only communicating into an embedded iframe
via XDM. XDM can also be used with pages from the same domain.
ENCODING API
The Encoding API allows for converting between strings and typed arrays. The specification intro-
duces four global classes for performing these conversions: TextEncoder, TextEncoderStream,
TextDecoder, and TextDecoderStream.
NOTE Support for stream encoding/decoding is much narrower than bulk
encoding/decoding.
Encoding Text
The Encoding API affords two ways of converting a string into its typed array binary equivalent:
a bulk encoding, and a stream encoding. When going from string to typed array, the encoder will
always use UTF-8.
Bulk Encoding
The bulk designation means that the JavaScript engine will synchronously encode the entire string.
For very long strings, this can be a costly operation. Bulk encoding is accomplished using an instance
of a TextEncoder:
const textEncoder = new TextEncoder();
754 ❘ CHAPTER 20 JavaScript APIs
This instance exposes an encode() method, which accepts a string and returns each character’s
UTF-8 encoding inside a freshly created Uint8Array:
const textEncoder = new TextEncoder();
const decodedText = 'foo';
const encodedText = textEncoder.encode(decodedText);
// f encoded in utf-8 is 0x66 (102 in decimal)
// o encoded in utf-8 is 0x6F (111 in decimal)
console.log(encodedText); // Uint8Array(3) [102, 111, 111]
The encoder is equipped to handle characters, which will take up multiple indices in the eventual
array, such as emojis:
const textEncoder = new TextEncoder();
const decodedText = ' ';
const encodedText = textEncoder.encode(decodedText);
// encoded in UTF-8 is 0xF0 0x9F 0x98 0x8A (240, 159, 152, 138 in decimal)
console.log(encodedText); // Uint8Array(4) [240, 159, 152, 138]
The instance also exposes an encodeInto() method, which accepts a string and the destination
Uint8Array. This method returns a dictionary containing read and written properties, indicating
how many characters were successfully read from the source string and written to the destination
array, respectively. If the typed array has insufficient space, the encoding will terminate early and the
dictionary will indicate that result:
const textEncoder = new TextEncoder();
const fooArr = new Uint8Array(3);
const barArr = new Uint8Array(2);
const fooResult = textEncoder.encodeInto('foo', fooArr);
const barResult = textEncoder.encodeInto('bar', barArr);
console.log(fooArr); // Uint8Array(3) [102, 111, 111]
console.log(fooResult); // { read: 3, written: 3 }
console.log(barArr); // Uint8Array(2) [98, 97]
console.log(barResult); // { read: 2, written: 2 }
encode() must allocate a new Uint8Array, whereas encodeInto() does not. For performance-sensi-
tive applications, this distinction may have significant implications.
NOTE Text encoding will always utilize the UTF-8 format and must write into
a Uint8Array instance. Attempting to use a different typed array when calling
encodeInto() will throw an error.
Stream Encoding
A TextEncoderStream is merely a TextEncoder in the form of a TransformStream. Piping a
decoded text stream through the stream encoder will yield a stream of encoded text chunks:
async function* chars() {
const decodedText = 'foo';
Encoding API ❘ 755
for (let char of decodedText) {
yield await new Promise((resolve) => setTimeout(resolve, 1000, char));
}
}
const decodedTextStream = new ReadableStream({
async start(controller) {
for await (let chunk of chars()) {
controller.enqueue(chunk);
}
controller.close();
}
});
const encodedTextStream = decodedTextStream.pipeThrough(new TextEncoderStream());
const readableStreamDefaultReader = encodedTextStream.getReader();
(async function() {
while(true) {
const { done, value } = await readableStreamDefaultReader.read();
if (done) {
break;
} else {
console.log(value);
}
}
})();
// Uint8Array[102]
// Uint8Array[111]
// Uint8Array[111]
Decoding Text
The Encoding API affords two ways of converting a typed array into its string equivalent: a bulk
decoding, and a stream decoding. Unlike the encoder classes, when going from typed array to string,
the decoder supports a large number of string encodings, listed here: https://encoding.spec
.whatwg.org/#names-and-labels
The default character encoding is UTF-8.
Bulk Decoding
The bulk designation means that the JavaScript engine will synchronously decode the entire string.
For very long strings, this can be a costly operation. Bulk decoding is accomplished using an instance
of a DecoderEncoder:
const textDecoder = new TextDecoder();
756 ❘ CHAPTER 20 JavaScript APIs
This instance exposes a decode() method, which accepts a typed array and returns the
decoded string:
const textDecoder = new TextDecoder();
// f encoded in utf-8 is 0x66 (102 in decimal)
// o encoded in utf-8 is 0x6F (111 in decimal)
const encodedText = Uint8Array.of(102, 111, 111);
const decodedText = textDecoder.decode(encodedText);
console.log(decodedText); // foo
The decoder does not care which typed array it is passed, so it will dutifully decode the entire binary
representation. In this example, 32-bit values only containing 8-bit characters are decoded as UTF-8,
yielding extra empty characters:
const textDecoder = new TextDecoder();
// f encoded in utf-8 is 0x66 (102 in decimal)
// o encoded in utf-8 is 0x6F (111 in decimal)
const encodedText = Uint32Array.of(102, 111, 111);
const decodedText = textDecoder.decode(encodedText);
console.log(decodedText); // "f o o "
The decoder is equipped to handle characters that span multiple indices in the typed array, such
as emojis:
const textDecoder = new TextDecoder();
// encoded in UTF-8 is 0xF0 0x9F 0x98 0x8A (240, 159, 152, 138 in decimal)
const encodedText = Uint8Array.of(240, 159, 152, 138);
const decodedText = textDecoder.decode(encodedText);
console.log(decodedText); //
Unlike TextEncoder, TextDecoder is compatible with a wide number of character encodings.
Consider the following example, which uses UTF-16 encoding instead of the default UTF-8:
const textDecoder = new TextDecoder('utf-16');
// f encoded in utf-8 is 0x0066 (102 in decimal)
// o encoded in utf-8 is 0x006F (111 in decimal)
const encodedText = Uint16Array.of(102, 111, 111);
const decodedText = textDecoder.decode(encodedText);
console.log(decodedText); // foo
Stream Decoding
A TextDecoderStream is merely a TextDecoder in the form of a TransformStream. Piping an
encoded text stream through the stream decoder will yield a stream of decoded text chunks:
async function* chars() {
// Each chunk must exist as a typed array
const encodedText = [102, 111, 111].map((x) => Uint8Array.of(x));