mvc4.0 html.actionlink comfired,Professional JavaScript for Web Developers, 4th Edition 18-28

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 type

in 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­ ommon

way 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 is

equal 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 Editing

This 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

  1. 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));

This book provides a developer-level introduction along with more advanced and useful features of JavaScript. Coverage includes: JavaScript use with HTML to create dynamic webpages, language concepts including syntax and flow control statementsvariable handling given their loosely typed naturebuilt-in reference types such as object and arrayobject-oriented programingpowerful aspects of function expressionsBrowser Object Model allowing interaction with the browser itselfdetecting the client and its capabilitiesDocument Object Model (DOM) objects available in DOM Level 1how DOM Levels 2 and 3 augmented the DOMevents, legacy support, and how the DOM redefined how events should workenhancing form interactions and working around browser limitationsusing the tag to create on-the-fly graphicsJavaScript API changes in HTML5how browsers handle JavaScript errors and error handlingfeatures of JavaScript used to read and manipulate XML datathe JSON data format as an alternative to XMLAjax techniques including the use of XMLHttpRequest object and CORScomplex patterns including function currying, partial function application, and dynamic functionsoffline detection and storing data on the client machinetechniques for JavaScript in an enterprise environment for better maintainability This book is aimed at three groups of readers: Experienced object-oriented programming developers looking to learn JavaScript as it relates to traditional OO languages such as Java and C++; Web application developers attempting to enhance site usability; novice JavaScript developers. Nicholas C. Zakas worked with the Web for over a decade. He has worked on corporate intranet applications used by some of the largest companies in the world and large-scale consumer websites such as MyYahoo! and the Yahoo! homepage. He regularly gives talks at companies and conferences regarding front-end best practices and new technology.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值