Js Info-3
Js Info-3
Additional articles
Ilya Kantor
Built at July 10, 2019
We constantly work to improve the tutorial. If you find any mistakes, please write at
our github.
● Frames and windows
● Popups and window methods
● Cross-window communication
● The clickjacking attack
●
Binary data, files
● ArrayBuffer, binary arrays
● TextDecoder and TextEncoder
● Blob
● File and FileReader
● Network requests
●
Fetch
● FormData
● Fetch: Download progress
● Fetch: Abort
● Fetch: Cross-Origin Requests
● Fetch API
● URL objects
● XMLHttpRequest
●
Resumable file upload
● Long polling
●
WebSocket
● Server Sent Events
●
Storing data in the browser
●
Cookies, document.cookie
● LocalStorage, sessionStorage
●
IndexedDB
●
Animation
● Bezier curve
●
CSS-animations
●
JavaScript animations
● Web components
●
From the orbital height
●
Custom elements
● Shadow DOM
●
Template element
● Shadow DOM slots, composition
● Shadow DOM styling
●
Shadow DOM and events
● Regular expressions
●
Patterns and flags
●
Methods of RegExp and String
● Character classes
●
Escaping, special characters
●
Sets and ranges [...]
●
Quantifiers +, *, ? and {n}
● Greedy and lazy quantifiers
●
Capturing groups
●
Backreferences in pattern: \n and \k
● Alternation (OR) |
●
String start ^ and finish $
●
Multiline mode, flag "m"
● Lookahead and lookbehind
●
Infinite backtracking problem
●
Unicode: flag "u"
● Unicode character properties \p
●
Sticky flag "y", searching at position
Frames and windows
Popups and window methods
A popup window is one of the oldest methods to show additional document to user.
window.open('https://javascript.info/')
…And it will open a new window with given URL. Most modern browsers are
configured to open new tabs instead of separate windows.
Popups exist from really ancient times. The initial idea was to show another content
without closing the main window. As of now, there are other ways to do that: we can
load content dynamically with fetch and show it in a dynamically generated <div> .
So, popups is not something we use everyday.
Also, popups are tricky on mobile devices, that don’t show multiple windows
simultaneously.
Still, there are tasks where popups are still used, e.g. for OAuth authorization (login
with Google/Facebook/…), because:
Popup blocking
In the past, evil sites abused popups a lot. A bad page could open tons of popup
windows with ads. So now most browsers try to block popups and protect the user.
Most browsers block popups if they are called outside of user-triggered event
handlers like onclick .
For example:
// popup blocked
window.open('https://javascript.info');
// popup allowed
button.onclick = () => {
window.open('https://javascript.info');
};
This way users are somewhat protected from unwanted popups, but the functionality
is not disabled totally.
What if the popup opens from onclick , but after setTimeout ? That’s a bit
tricky.
The difference is that Firefox treats a timeout of 2000ms or less are acceptable, but
after it – removes the “trust”, assuming that now it’s “outside of the user action”. So
the first one is blocked, and the second one is not.
window.open
url
An URL to load into the new window.
name
A name of the new window. Each window has a window.name , and here we can
specify which window to use for the popup. If there’s already a window with such
name – the given URL opens in it, otherwise a new window is opened.
params
The configuration string for the new window. It contains settings, delimited by a
comma. There must be no spaces in params, for instance:
width:200,height=100 .
Let’s open a window with minimal set of features just to see which of them browser
allows to disable:
Here most “window features” are disabled and window is positioned offscreen. Run it
and see what really happens. Most browsers “fix” odd things like zero
width/height and offscreen left/top . For instance, Chrome open such a
window with full width/height, so that it occupies the full screen.
Let’s add normal positioning options and reasonable width , height , left ,
top coordinates:
The open call returns a reference to the new window. It can be used to manipulate
it’s properties, change location and even more.
newWin.document.write("Hello, world!");
newWindow.onload = function() {
let html = `<div style="font-size:30px">Welcome!</div>`;
newWindow.document.body.insertAdjacentHTML('afterbegin', html);
};
Please note: immediately after window.open , the new window isn’t loaded yet.
That’s demonstrated by alert in line (*) . So we wait for onload to modify it.
We could also use DOMContentLoaded handler for newWin.document .
⚠ Same origin policy
Windows may freely access content of each other only if they come from the
same origin (the same protocol://domain:port).
Otherwise, e.g. if the main window is from site.com , and the popup from
gmail.com , that’s impossible for user safety reasons. For the details, see
chapter Cross-window communication.
If you run the code below, it replaces the opener (current) window content with
“Test”:
newWin.document.write(
"<script>window.opener.document.body.innerHTML = 'Test'<\/script>"
);
So the connection between the windows is bidirectional: the main window and the
popup have a reference to each other.
Closing a popup
The closed property is true if the window is closed. That’s useful to check if the
popup (or the main window) is still open or not. A user can close it anytime, and our
code should take that possibility into account.
newWindow.onload = function() {
newWindow.close();
alert(newWindow.closed); // true
};
win.moveBy(x,y)
Move the window relative to current position x pixels to the right and y pixels
down. Negative values are allowed (to move left/up).
win.moveTo(x,y)
Move the window to coordinates (x,y) on the screen.
win.resizeBy(width,height)
Resize the window by given width/height relative to the current size. Negative
values are allowed.
win.resizeTo(width,height)
Resize the window to the given size.
⚠ Only popups
To prevent abuse, the browser usually blocks these methods. They only work
reliably on popups that we opened, that have no additional tabs.
⚠ No minification/maximization
JavaScript has no way to minify or maximize a window. These OS-level functions
are hidden from Frontend-developers.
Move/resize methods do not work for maximized/minimized windows.
Scrolling a window
We already talked about scrolling a window in the chapter Window sizes and
scrolling.
win.scrollBy(x,y)
Scroll the window x pixels right and y down relative the current scroll. Negative
values are allowed.
win.scrollTo(x,y)
Scroll the window to the given coordinates (x,y) .
elem.scrollIntoView(top = true)
Scroll the window to make elem show up at the top (the default) or at the bottom for
elem.scrollIntoView(false) .
Focus/blur on a window
In the past evil pages abused those. For instance, look at this code:
When a user attempts to switch out of the window ( blur ), it brings it back to focus.
The intention is to “lock” the user within the window .
So, there are limitations that forbid the code like that. There are many limitations to
protect the user from ads and evils pages. They depend on the browser.
For instance, a mobile browser usually ignores that call completely. Also focusing
doesn’t work when a popup opens in a separate tab rather than a new window.
Still, there are some things that can be done.
For instance:
●
When we open a popup, it’s might be a good idea to run a
newWindow.focus() on it. Just in case, for some OS/browser combinations it
ensures that the user is in the new window now.
●
If we want to track when a visitor actually uses our web-app, we can track
window.onfocus/onblur . That allows us to suspend/resume in-page
activities, animations etc. But please note that the blur event means that the
visitor switched out from the window, but they still may observe it. The window is
in the background, but still may be visible.
Summary
Popup windows are used rarely, as there are alternatives: loading and displaying
information in-page, or in iframe.
If we’re going to open a popup, a good practice is to inform the user about it. An
“opening window” icon near a link or button would allow the visitor to survive the
focus shift and keep both windows in mind.
●
A popup can be opened by the open(url, name, params) call. It returns the
reference to the newly opened window.
●
Browsers block open calls from the code outside of user actions. Usually a
notification appears, so that a user may allow them.
●
Browsers open a new tab by default, but if sizes are provided, then it’ll be a popup
window.
●
The popup may access the opener window using the window.opener property.
●
The main window and the popup can freely read and modify each other if they
havee the same origin. Otherwise, they can change location of each other and
[exchange messages.
To close the popup: use close() call. Also the user may close them (just like any
other windows). The window.closed is true after that.
● Methods focus() and blur() allow to focus/unfocus a window. But they don’t
work all the time.
●
Events focus and blur allow to track switching in and out of the window. But
please note that a window may still be visible even in the background state, after
blur .
Cross-window communication
The “Same Origin” (same site) policy limits access of windows and frames to each
other.
The idea is that if a user has two pages open: one from john-smith.com , and
another one is gmail.com , then they wouldn’t want a script from john-
smith.com to read our mail from gmail.com . So, the purpose of the “Same
Origin” policy is to protect users from information theft.
Same Origin
Two URLs are said to have the “same origin” if they have the same protocol, domain
and port.
These URLs all share the same origin:
●
http://site.com
●
http://site.com/
●
http://site.com/my/page.html
These ones do not:
● http://www.site.com (another domain: www. matters)
●
http://site.org (another domain: .org matters)
●
https://site.com (another protocol: https )
●
http://site.com:8080 (another port: 8080 )
In action: iframe
An <iframe> tag hosts a separate embedded window, with its own separate
document and window objects.
When we access something inside the embedded window, the browser checks if the
iframe has the same origin. If that’s not so then the access is denied (writing to
location is an exception, it’s still permitted).
For instance, let’s try reading and writing to <iframe> from another origin:
<script>
iframe.onload = function() {
// we can get the reference to the inner window
let iframeWindow = iframe.contentWindow; // OK
try {
// ...but not to the document inside it
let doc = iframe.contentDocument; // ERROR
} catch(e) {
alert(e); // Security Error (another origin)
}
// ...we can WRITE into location (and thus load something else into the iframe)
iframe.contentWindow.location = '/'; // OK
iframe.onload = null; // clear the handler, not to run it after the location cha
};
</script>
Contrary to that, if the <iframe> has the same origin, we can do anything with it:
<script>
iframe.onload = function() {
// just do anything
iframe.contentDocument.body.prepend("Hello, world!");
};
</script>
iframe.onload vs iframe.contentWindow.onload
The iframe.onload event (on the <iframe> tag) is essentially the same as
iframe.contentWindow.onload (on the embedded window object). It
triggers when the embedded window fully loads with all resources.
…But we can’t access iframe.contentWindow.onload for an iframe from
another origin, so using iframe.onload .
document.domain = 'site.com';
That’s all. Now they can interact without limitations. Again, that’s only possible for
pages with the same second-level domain.
When an iframe comes from the same origin, and we may access its document ,
there’s a pitfall. It’s not related to cross-domain things, but important to know.
Upon its creation an iframe immediately has a document. But that document is
different from the one that loads into it!
So if we do something with the document immediately, that will probably be lost.
Here, look:
<script>
let oldDoc = iframe.contentDocument;
iframe.onload = function() {
let newDoc = iframe.contentDocument;
// the loaded document is not the same as initial!
alert(oldDoc == newDoc); // false
};
</script>
We shouldn’t work with the document of a not-yet-loaded iframe, because that’s the
wrong document. If we set any event handlers on it, they will be ignored.
<script>
let oldDoc = iframe.contentDocument;
Collection: window.frames
An alternative way to get a window object for <iframe> – is to get it from the
named collection window.frames :
●
By number: window.frames[0] – the window object for the first frame in the
document.
● By name: window.frames.iframeName – the window object for the frame
with name="iframeName" .
For instance:
<script>
alert(iframe.contentWindow == frames[0]); // true
alert(iframe.contentWindow == frames.win); // true
</script>
An iframe may have other iframes inside. The corresponding window objects form
a hierarchy.
For instance:
window.frames[0].parent === window; // true
We can use the top property to check if the current document is open inside a
frame or not:
The sandbox attribute allows for the exclusion of certain actions inside an
<iframe> in order to prevent it executing untrusted code. It “sandboxes” the iframe
by treating it as coming from another origin and/or applying other limitations.
There’s a “default set” of restrictions applied for <iframe sandbox
src="..."> . But it can be relaxed if we provide a space-separated list of
restrictions that should not be applied as a value of the attribute, like this: <iframe
sandbox="allow-forms allow-popups"> .
allow-same-origin
By default "sandbox" forces the “different origin” policy for the iframe. In other
words, it makes the browser to treat the iframe as coming from another origin,
even if its src points to the same site. With all implied restrictions for scripts. This
option removes that feature.
allow-top-navigation
Allows the iframe to change parent.location .
allow-forms
Allows to submit forms from iframe .
allow-scripts
Allows to run scripts from the iframe .
allow-popups
Allows to window.open popups from the iframe
The example below demonstrates a sandboxed iframe with the default set of
restrictions: <iframe sandbox src="..."> . It has some JavaScript and a
form.
Please note that nothing works. So the default set is really harsh:
https://plnkr.co/edit/GAhzx0j3JwAB1TMzwyxL?p=preview
Please note:
The purpose of the "sandbox" attribute is only to add more restrictions. It
cannot remove them. In particular, it can’t relax same-origin restrictions if the
iframe comes from another origin.
Cross-window messaging
The postMessage interface allows windows to talk to each other no matter which
origin they are from.
So, it’s a way around the “Same Origin” policy. It allows a window from john-
smith.com to talk to gmail.com and exchange information, but only if they both
agree and call corresponding JavaScript functions. That makes it safe for users.
postMessage
The window that wants to send a message calls postMessage method of the
receiving window. In other words, if we want to send the message to win , we
should call win.postMessage(data, targetOrigin) .
Arguments:
data
The data to send. Can be any object, the data is cloned using the “structured cloning
algorithm”. IE supports only strings, so we should JSON.stringify complex
objects to support that browser.
targetOrigin
Specifies the origin for the target window, so that only a window from the given origin
will get the message.
<script>
let win = window.frames.example;
win.postMessage("message", "http://example.com");
</script>
<script>
let win = window.frames.example;
win.postMessage("message", "*");
</script>
onmessage
To receive a message, the target window should have a handler on the message
event. It triggers when postMessage is called (and targetOrigin check is
successful).
The event object has special properties:
data
The data from postMessage .
origin
The origin of the sender, for instance http://javascript.info .
source
The reference to the sender window. We can immediately
source.postMessage(...) back if we want.
To assign that handler, we should use addEventListener , a short syntax
window.onmessage does not work.
Here’s an example:
window.addEventListener("message", function(event) {
if (event.origin != 'http://javascript.info') {
// something from an unknown domain, let's ignore it
return;
}
https://plnkr.co/edit/ltrzlGvN8UPdpMtyxlI9?p=preview
There’s no delay
There’s totally no delay between postMessage and the message event. The
event triggers synchronously, faster than setTimeout(...,0) .
Summary
To call methods and access the content of another window, we should first have a
reference to it.
If windows share the same origin (host, port, protocol), then windows can do
whatever they want with each other.
Otherwise, only possible actions are:
●
Change the location of another window (write-only access).
● Post a message to it.
Exceptions are:
●
Windows that share the same second-level domain: a.site.com and
b.site.com . Then setting document.domain='site.com' in both of them
puts them into the “same origin” state.
● If an iframe has a sandbox attribute, it is forcefully put into the “different origin”
state, unless the allow-same-origin is specified in the attribute value. That
can be used to run untrusted code in iframes from the same site.
The postMessage interface allows two windows with any origins to talk:
3. If it is so, then targetWin triggers the message event with special properties:
●
origin – the origin of the sender window (like http://my.site.com )
● source – the reference to the sender window.
● data – the data, any object in everywhere except IE that supports only
strings.
We should use addEventListener to set the handler for this event inside the
target window.
The idea
The demo
Here’s how the evil page looks. To make things clear, the <iframe> is half-
transparent (in real evil pages it’s fully transparent):
<style>
iframe { /* iframe from the victim site */
width: 400px;
height: 100px;
position: absolute;
top:0; left:-20px;
opacity: 0.5; /* in real opacity:0 */
z-index: 1;
}
</style>
<button>Click here!</button>
https://plnkr.co/edit/GQKK8Zc7DXT3KdV7tQUu?p=preview
https://plnkr.co/edit/aebDnhU3B7c6d2QN5Rhy?p=preview
All we need to attack – is to position the <iframe> on the evil page in such a way
that the button is right over the link. So that when a user clicks the link, they actually
click the button. That’s usually doable with CSS.
But then there’s a problem. Everything that the visitor types will be hidden,
because the iframe is not visible.
People will usually stop typing when they can’t see their new characters printing
on the screen.
The oldest defence is a bit of JavaScript which forbids opening the page in a frame
(so-called “framebusting”).
That looks like this:
if (top != window) {
top.location = window.location;
}
That is: if the window finds out that it’s not on top, then it automatically makes itself
the top.
This not a reliable defence, because there are many ways to hack around it. Let’s
cover a few.
Blocking top-navigation
We can block the transition caused by changing top.location in beforeunload
event handler.
The top page (enclosing one, belonging to the hacker) sets a preventing handler to
it, like this:
window.onbeforeunload = function() {
return false;
};
When the iframe tries to change top.location , the visitor gets a message
asking them whether they want to leave.
In most cases the visitor would answer negatively because they don’t know about
the iframe – all they can see is the top page, there’s no reason to leave. So
top.location won’t change!
In action:
https://plnkr.co/edit/WCEMuiV3PmW1klyyf6FH?p=preview
Sandbox attribute
One of the things restricted by the sandbox attribute is navigation. A sandboxed
iframe may not change top.location .
There are other ways to work around that simple protection too.
X-Frame-Options
DENY
Never ever show the page inside a frame.
SAMEORIGIN
Allow inside a frame if the parent document comes from the same origin.
ALLOW-FROM domain
Allow inside a frame if the parent document is from the given domain.
So there are other solutions… For instance, we can “cover” the page with a <div>
with styles height: 100%; width: 100%; , so that it will intercept all clicks.
That <div> is to be removed if window == top or if we figure out that we don’t
need the protection.
Something like this:
<style>
#protector {
height: 100%;
width: 100%;
position: absolute;
left: 0;
top: 0;
z-index: 99999999;
}
</style>
<div id="protector">
<a href="/" target="_blank">Go to the site</a>
</div>
<script>
// there will be an error if top window is from the different origin
// but that's ok here
if (top.document.domain == document.domain) {
protector.remove();
}
</script>
The demo:
https://plnkr.co/edit/WuzXGKQamlVp1sE08svn?p=preview
A cookie with such attribute is only sent to a website if it’s opened directly, not via a
frame, or otherwise. More information in the chapter Cookies, document.cookie.
If the site, such as Facebook, had samesite attribute on its authentication cookie,
like this:
Set-Cookie: authorization=secret; samesite
…Then such cookie wouldn’t be sent when Facebook is open in iframe from another
site. So the attack would fail.
The samesite cookie attribute will not have an effect when cookies are not used.
This may allow other websites to easily show our public, unauthenticated pages in
iframes.
However, this may also allow clickjacking attacks to work in a few limited cases. An
anonymous polling website that prevents duplicate voting by checking IP addresses,
for example, would still be vulnerable to clickjacking because it does not authenticate
users using cookies.
Summary
Clickjacking is a way to “trick” users into clicking on a victim site without even
knowing what’s happening. That’s dangerous if there are important click-activated
actions.
A hacker can post a link to their evil page in a message, or lure visitors to their page
by some other means. There are many variations.
From one perspective – the attack is “not deep”: all a hacker is doing is intercepting
a single click. But from another perspective, if the hacker knows that after the click
another control will appear, then they may use cunning messages to coerce the user
into clicking on them as well.
The attack is quite dangerous, because when we engineer the UI we usually don’t
anticipate that a hacker may click on behalf of the visitor. So vulnerabilities can be
found in totally unexpected places.
● It is recommended to use X-Frame-Options: SAMEORIGIN on pages (or
whole websites) which are not intended to be viewed inside frames.
●
Use a covering <div> if we want to allow our pages to be shown in iframes, but
still stay safe.
This allocates a contiguous memory area of 16 bytes and pre-fills it with zeroes.
ArrayBuffer is a memory area. What’s stored in it? It has no clue. Just a raw
sequence of bytes.
To manipulate an ArrayBuffer , we need to use a “view” object.
A view object does not store anything on it’s own. It’s the “eyeglasses” that give an
interpretation of the bytes stored in the ArrayBuffer .
For instance:
●
Uint8Array – treats each byte in ArrayBuffer as a separate number, with
possible values are from 0 to 255 (a byte is 8-bit, so it can hold only that much).
Such value is called a “8-bit unsigned integer”.
● Uint16Array – treats every 2 bytes as an integer, with possible values from 0
to 65535. That’s called a “16-bit unsigned integer”.
● Uint32Array – treats every 4 bytes as an integer, with possible values from 0
to 4294967295. That’s called a “32-bit unsigned integer”.
● Float64Array – treats every 8 bytes as a floating point number with possible
values from 5.0x10-324 to 1.8x10308 .
ArrayBuffer is the core object, the root of everything, the raw binary data.
But if we’re going to write into it, or iterate over it, basically for almost any operation –
we must use a view, e.g:
TypedArray
The common term for all these views ( Uint8Array , Uint32Array , etc) is
TypedArray . They share the same set of methods and properities.
They are much more like regular arrays: have indexes and iterable.
A typed array constructor (be it Int8Array or Float64Array , doesn’t matter)
behaves differently depending on argument types.
There are 5 variants of arguments:
2. If an Array , or any array-like object is given, it creates a typed array of the same
length and copies the content.
We can use it to pre-fill the array with the data:
4. For a numeric argument length – creates the typed array to contain that many
elements. Its byte length will be length multiplied by the number of bytes in a
single item TypedArray.BYTES_PER_ELEMENT :
Please note, despite of the names like Int8Array , there’s no single-value type
like int , or int8 in JavaScript.
Out-of-bounds behavior
What if we attempt to write an out-of-bounds value into a typed array? There will be
no error. But extra bits are cut-off.
For instance, let’s try to put 256 into Uint8Array . In binary form, 256 is
100000000 (9 bits), but Uint8Array only provides 8 bits per value, that makes
the available range from 0 to 255.
For bigger numbers, only the rightmost (less significant) 8 bits are stored, and the
rest is cut off:
uint8array[0] = 256;
uint8array[1] = 257;
alert(uint8array[0]); // 0
alert(uint8array[1]); // 1
TypedArray methods
These methods allow us to copy typed arrays, mix them, create new arrays from
existing ones, and so on.
DataView
The syntax:
For instance, here we extract numbers in different formats from the same buffer:
dataView.setUint32(0, 0); // set 4-byte number to zero, thus setting all bytes to 0
DataView is great when we store mixed-format data in the same buffer. E.g we
store a sequence of pairs (16-bit integer, 32-bit float). Then DataView allows to
access them easily.
Summary
There are also two additional terms, that are used in descriptions of methods that
operate on binary data:
● ArrayBufferView is an umbrella term for all these kinds of views.
● BufferSource is an umbrella term for ArrayBuffer or
ArrayBufferView .
We’ll see these terms in the next chapters. BufferSource is one of the most
common terms, as it means “any kind of binary data” – an ArrayBuffer or a view
over it.
Here’s a cheatsheet:
✔ Tasks
To solution
●
label – the encoding, utf-8 by default, but big5 , windows-1251 and
many other are also supported.
● options – optional object:
● fatal – boolean, if true then throw an exception for invalid (non-
decodable) characters, otherwise (default) replace them with character
\uFFFD .
● ignoreBOM – boolean, if true then ignore BOM (an optional byte-order
unicode mark), rarely needed.
For instance:
We can decode a part of the buffer by creating a subarray view for it:
let uint8Array = new Uint8Array([0, 72, 101, 108, 108, 111, 0]);
TextEncoder
Blob
ArrayBuffer and views are a part of ECMA standard, a part of JavaScript.
In the browser, there are additional higher-level objects, described in File API , in
particular Blob .
For example:
●
byteStart – the starting byte, by default 0.
● byteEnd – the last byte (exclusive, by default till the end).
● contentType – the type of the new blob, by default the same as the source.
The arguments are similar to array.slice , negative numbers are allowed too.
Blob as URL
A Blob can be easily used as an URL for <a> , <img> or other tags, to show its
contents.
Thanks to type , we can allso download/upload blobs, and it naturally becomes
Content-Type in network requests.
Let’s start with a simple example. By clicking on a link you download a dynamically-
generated blob with hello world contents as a file:
<!-- download attribute forces the browser to download instead of navigating -->
<a download="hello.txt" href='#' id="link">Download</a>
<script>
let blob = new Blob(["Hello, world!"], {type: 'text/plain'});
link.href = URL.createObjectURL(blob);
</script>
Here’s the similar code that causes user to download the dynamicallly created Blob,
without any HTML:
link.href = URL.createObjectURL(blob);
link.click();
URL.revokeObjectURL(link.href);
URL.createObjectURL takes a blob and creates an unique URL for it, in the
form blob:<origin>/<uuid> .
blob:https://javascript.info/1e67e00e-860d-40a5-89ae-6ab0cbee6273
The browser for each url generated by URL.createObjectURL stores an the url
→ blob mapping internally. So such urls are short, but allow to access the blob.
A generated url (and hence the link with it) is only valid within the current document,
while it’s open. And it allows to reference the blob in <img> , <a> , basically any
other object that expects an url.
There’s a side-effect though. While there’s an mapping for a blob, the blob itself
resides in the memory. The browser can’t free it.
The mapping is automatically cleared on document unload, so blobs are freed then.
But if an app is long-living, then that doesn’t happen soon.
So if we create an URL, that blob will hang in memory, even if not needed any
more.
URL.revokeObjectURL(url) removes the reference from the internal mapping,
thus allowing the blob to be deleted (if there are no other references), and the
memory to be freed.
In the last example, we intend the blob to be used only once, for instant
downloading, so we call URL.revokeObjectURL(link.href) immediately.
In the previous example though, with the clickable HTML-link, we don’t call
URL.revokeObjectURL(link.href) , because that would make the blob url
invalid. After the revocation, as the mapping is removed, the url doesn’t work any
more.
Blob to base64
<img src="data:image/png;base64,R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5
The browser will decode the string and show the image:
To transform a blob into base64, we’ll use the built-in FileReader object. It can
read data from Blobs in multiple formats. In the next chapter we’ll cover it more in-
depth.
Here’s the demo of downloading a blob, now via base-64:
reader.onload = function() {
link.href = reader.result; // data url
link.click();
};
Image to blob
We can create a blob of an image, an image part, or even make a page screenshot.
That’s handy to upload it somewhere.
Image operations are done via <canvas> element:
In the example below, an image is just copied, but we could cut from it, or transform
it on canvas prior to making a blob:
link.href = URL.createObjectURL(blob);
link.click();
// delete the internal blob reference, to let the browser clear memory from it
URL.revokeObjectURL(link.href);
}, 'image/png');
The Blob constructor allows to create a blob from almost anything, including any
BufferSource .
fileReader.readAsArrayBuffer(blob);
fileReader.onload = function(event) {
let arrayBuffer = fileReader.result;
};
Summary
While ArrayBuffer , Uint8Array and other BufferSource are “binary data”,
a Blob represents “binary data with type”.
That makes Blobs convenient for upload/download operations, that are so common
in the browser.
Methods that perform web-requests, such as XMLHttpRequest, fetch and so on, can
work with Blob natively, as well as with other binary types.
We can easily convert betweeen Blob and low-level binary data types:
●
We can make a Blob from a typed array using new Blob(...) constructor.
● We can get back ArrayBuffer from a Blob using FileReader , and then
create a view over it for low-level binary processing.
As File inherits from Blob , File objects have the same properties, plus:
● name – the file name,
● lastModified – the timestamp of last modification.
<script>
function showFile(input) {
let file = input.files[0];
Please note:
The input may select multiple files, so input.files is an array-like object with
them. Here we have only one file, so we just take input.files[0] .
FileReader
FileReader is an object with the sole purpose of reading data from Blob (and
hence File too) objects.
It delivers the data using events, as reading from disk may take time.
The constructor:
The choice of read* method depends on which format we prefer, how we’re going
to use the data.
● readAsArrayBuffer – for binary files, to do low-level binary operations. For
high-level operations, like slicing, File inherits from Blob , so we can call them
directly, without reading.
●
readAsText – for text files, when we’d like to get a string.
● readAsDataURL – when we’d like to use this data in src for img or another
tag. There’s an alternative to reading a file for that, as discussed in chapter Blob:
URL.createObjectURL(file) .
As the reading proceeds, there are events:
●
loadstart – loading started.
● progress – occurs during reading.
●
load – no errors, reading complete.
● abort – abort() called.
● error – error has occurred.
●
loadend – reading finished with either success or failure.
The most widely used events are for sure load and error .
<script>
function readFile(input) {
let file = input.files[0];
reader.readAsText(file);
reader.onload = function() {
console.log(reader.result);
};
reader.onerror = function() {
console.log(reader.error);
};
}
</script>
FileReader for blobs
As mentioned in the chapter Blob, FileReader can read not just files, but any
blobs.
We can use it to convert a blob to another format:
● readAsArrayBuffer(blob) – to ArrayBuffer ,
● readAsText(blob, [encoding]) – to string (an alternative to
TextDecoder ),
● readAsDataURL(blob) – to base64 data url.
Its reading methods read* do not generate events, but rather return a result,
as regular functions do.
That’s only inside a Web Worker though, because delays in synchronous calls,
that are possible while reading from files, in Web Workers are less important.
They do not affect the page.
Summary
In addition to Blob methods and properties, File objects also have name and
lastModified properties, plus the internal ability to read from filesystem. We
usually get File objects from user input, like <input> or Drag’n’Drop events
( ondragend ).
FileReader objects can read from a file or a blob, in one of three formats:
● String ( readAsText ).
● ArrayBuffer ( readAsArrayBuffer ).
● Data url, base-64 encoded ( readAsDataURL ).
In many cases though, we don’t have to read the file contents. Just as we did with
blobs, we can create a short url with URL.createObjectURL(file) and assign
it to <a> or <img> . This way the file can be downloaded or shown up as an image,
as a part of canvas etc.
And if we’re going to send a File over a network, that’s also easy: network API like
XMLHttpRequest or fetch natively accepts File objects.
Network requests
Fetch
JavaScript can send network requests to the server and load new information
whenever is needed.
For example, we can:
● Submit an order,
● Load user information,
● Receive latest updates from the server,
● …etc.
The browser starts the request right away and returns a promise .
First, the promise resolves with an object of the built-in Response class
as soon as the server responds with headers.
So we can check HTTP status, to see whether it is successful or not, check headers,
but don’t have the body yet.
The promise rejects if the fetch was unable to make HTTP-request, e.g. network
problems, or there’s no such site. HTTP-errors, even such as 404 or 500, are
considered a normal flow.
We can see them in response properties:
● ok – boolean, true if the HTTP status code is 200-299.
● status – HTTP status code.
For example:
Second, to get the response body, we need to use an additional method call.
Response provides multiple promise-based methods to access the body in various
formats:
● response.json() – parse the response as JSON object,
● response.text() – return the response as text,
● response.formData() – return the response as FormData object
(form/multipart encoding, explained in the next chapter),
● response.blob() – return the response as Blob (binary data with type),
● response.arrayBuffer() – return the response as ArrayBuffer (pure binary
data),
● additionally, response.body is a ReadableStream object, it allows to read
the body chunk-by-chunk, we’ll see an example later.
For instance, let’s get a JSON-object with latest commits from GitHub:
let commits = await response.json(); // read response body and parse as JSON
alert(commits[0].author.login);
As a show-case for reading in binary format, let’s fetch and show an image (see
chapter Blob for details about operations on blobs):
// show it
img.src = URL.createObjectURL(blob);
⚠ Important:
We can choose only one body-parsing method.
If we got the response with response.text() , then response.json()
won’t work, as the body content has already been processed.
These headers ensure proper and safe HTTP, so they are controlled exclusively by
the browser.
POST requests
let user = {
name: 'John',
surname: 'Smith'
};
Sending an image
For example, here’s a <canvas> where we can draw by moving a mouse. A click
on the “submit” button sends the image to server:
<body style="margin:0">
<canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>
<script>
canvasElem.onmousemove = function(e) {
let ctx = canvasElem.getContext('2d');
ctx.lineTo(e.clientX, e.clientY);
ctx.stroke();
};
</script>
</body>
Submit
Here we also didn’t need to set Content-Type manually, because a Blob object
has a built-in type (here image/png , as generated by toBlob ).
function submit() {
canvasElem.toBlob(function(blob) {
fetch('/article/fetch/post/image', {
method: 'POST',
body: blob
})
.then(response => response.json())
.then(result => alert(JSON.stringify(result, null, 2)))
}, 'image/png');
}
Summary
Or, promise-style:
fetch(url, options)
.then(response => response.json())
.then(result => /* process result */)
Response properties:
● response.status – HTTP code of the response,
● response.ok – true is the status is 200-299.
● response.headers – Map-like object with HTTP headers.
✔ Tasks
The GitHub url with user informaiton for the given USERNAME is:
https://api.github.com/users/USERNAME .
Important details:
1. There should be one fetch request per user. And requests shouldn’t wait for
each other. So that the data arrives as soon as possible.
2. If any request fails, or if there’s no such user, the function should return null
in the resulting array.
To solution
FormData
This chapter is about sending HTML forms: with or without files, with additional fields
and so on. FormData objects can help with that.
If HTML form element is provided, it automatically captures its fields. As you may
have already guessed, FormData is an object to store and send form data.
The special thing about FormData is that network methods, such as fetch , can
accept a FormData object as a body. It’s encoded and sent out with Content-
Type: form/multipart . So, from the server point of view, that looks like a usual
form submission.
Sending a simple form
<form id="formElem">
<input type="text" name="name" value="John">
<input type="text" name="surname" value="Smith">
<input type="submit">
</form>
<script>
formElem.onsubmit = async (e) => {
e.preventDefault();
alert(result.message);
};
</script>
Here, the server accepts the POST request with the form and replies “User saved”.
FormData Methods
There’s also method set , with the same syntax as append . The difference is that
.set removes all fields with the given name , and then appends a new field. So it
makes sure there’s only field with such name :
● formData.set(name, value) ,
● formData.set(name, blob, fileName) .
<form id="formElem">
<input type="text" name="firstName" value="John">
Picture: <input type="file" name="picture" accept="image/*">
<input type="submit">
</form>
<script>
formElem.onsubmit = async (e) => {
e.preventDefault();
alert(result.message);
};
</script>
As we’ve seen in the chapter Fetch, sending a dynamically generated Blob , e.g. an
image, is easy. We can supply it directly as fetch parameter body .
In practice though, it’s often convenient to send an image not separately, but as a
part of the form, with additional fields, such as “name” and other metadata.
Also, servers are usually more suited to accept multipart-encoded forms, rather than
raw binary data.
This example submits an image from <canvas> , along with some other fields,
using FormData :
<body style="margin:0">
<canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>
<script>
canvasElem.onmousemove = function(e) {
let ctx = canvasElem.getContext('2d');
ctx.lineTo(e.clientX, e.clientY);
ctx.stroke();
};
</script>
</body>
Submit
Summary
FormData objects are used to capture HTML form and submit it using fetch or
another network method.
We can either create new FormData(form) from an HTML form, or create an
empty object, and then append fields with methods:
● formData.append(name, value)
● formData.append(name, blob, fileName)
● formData.set(name, value)
● formData.set(name, blob, fileName)
That’s it!
if (done) {
break;
}
To log the progress, we just need for every value add its length to the counter.
Here’s the full code to get response and log the progress, more explanations follow:
if (done) {
break;
}
chunks.push(value);
receivedLength += value.length;
// We're done!
let commits = JSON.parse(result);
alert(commits[0].author.login);
Please note, we can’t use both these methods to read the same response. Either
use a reader or a response method to get the result.
2. Prior to reading, we can figure out the full response length from the Content-
Length header.
We gather response chunks in the array. That’s important, because after the
response is consumed, we won’t be able to “re-read” it using
response.json() or another way (you can try, there’ll be an error).
To create a string, we need to interpret these bytes. The built-in TextDecoder does
exactly that. Then we can JSON.parse it.
What if we need binary content instead of JSON? That’s even simpler. Replace
steps 4 and 5 with a single call to a blob from all chunks:
At we end we have the result (as a string or a blob, whatever is convenient), and
progress-tracking in the process.
Once again, please note, that’s not for upload progress (no way now with fetch ),
only for download progress.
Fetch: Abort
Aborting a fetch is a little bit tricky. Remember, fetch returns a promise. And
JavaScript generally has no concept of “aborting” a promise. So how can we cancel
a fetch?
There’s a special built-in object for such purposes: AbortController .
Like this:
controller.abort(); // abort!
controller.abort();
We’re done: fetch gets the event from signal and aborts the request.
When a fetch is aborted, its promise rejects with an error named AbortError , so
we should handle it:
// abort in 1 second
let controller = new AbortController();
setTimeout(() => controller.abort(), 1000);
try {
let response = await fetch('/article/fetch-abort/demo/hang', {
signal: controller.signal
});
} catch(err) {
if (err.name == 'AbortError') { // handle abort()
alert("Aborted!");
} else {
throw err;
}
}
For instance, here we fetch many urls in parallel, and the controller aborts them
all:
let urls = [...]; // a list of urls to fetch in parallel
// from elsewhere:
// controller.abort() stops all fetches
If we have our own jobs, different from fetch , we can use a single
AbortController to stop those, together with fetches.
// from elsewhere:
// controller.abort() stops all fetches and ourJob
try {
await fetch('http://example.com');
} catch(err) {
alert(err); // Failed to fetch
}
Using forms
One way to communicate with another server was to submit a <form> there.
People submitted it into <iframe> , just to stay on the current page, like this:
So, it was possible to make a GET/POST request to another site, even without
networking methods. But as it’s forbidden to access the content of an <iframe>
from another site, it wasn’t possible to read the response.
As we can see, forms allowed to send data anywhere, but not receive the response.
To be precise, there wre actually tricks for that (required special scripts at both the
iframe and the page), but let these dinosaurs rest in peace.
Using scripts
Another trick was to use a <script src="http://another.com/…"> tag. A
script could have any src , from any domain. But again – it was impossible to
access the raw content of such script.
If another.com intended to expose data for this kind of access, then a so-called
“JSONP (JSON with padding)” protocol was used.
Let’s say we need to get the data from http://another.com this way:
4. When the remote script loads and executes, gotWeather runs, and, as it’s our
function, we have the data.
That works, and doesn’t violate security, because both sides agreed to pass the data
this way. And, when both sides agree, it’s definitely not a hack. There are still
services that provide such access, as it works even for very old browsers.
After a while, networking methods appeared, such as XMLHttpRequest .
Simple Requests are, well, simpler to make, so let’s start with them.
A simple request is a request that satisfies two conditions:
Any other request is considered “non-simple”. For instance, a request with PUT
method or with an API-Key HTTP-header does not fit the limitations.
So, even a very old server should be ready to accept a simple request.
Contrary to that, requests with non-standard headers or e.g. method DELETE can’t
be created this way. For a long time JavaScript was unable to do such requests. So
an old server may assume that such requests come from a privileged source,
“because a webpage is unable to send them”.
When we try to make a non-simple request, the browser sends a special “preflight”
request that asks the server – does it agree to accept such cross-origin requests, or
not?
And, unless the server explicitly confirms that with headers, a non-simple request is
not sent.
Now we’ll go into details. All of them serve a single purpose – to ensure that new
cross-origin capabilities are only accessible with an explicit permission from the
server.
GET /request
Host: anywhere.com
Origin: https://javascript.info
...
As you can see, Origin contains exactly the origin (domain/protocol/port), without
a path.
The server can inspect the Origin and, if it agrees to accept such a request, adds
a special header Access-Control-Allow-Origin to the response. That
header should contain the allowed origin (in our case
https://javascript.info ), or a star * . Then the response is successful,
otherwise an error.
The browser plays the role of a trusted mediator here:
1. It ensures that the corrent Origin is sent with a cross-domain request.
2. If checks for correct Access-Control-Allow-Origin in the response, if it is
so, then JavaScript access, otherwise forbids with an error.
200 OK
Content-Type:text/html; charset=UTF-8
Access-Control-Allow-Origin: https://javascript.info
Response headers
For cross-origin request, by default JavaScript may only access “simple response
headers”:
● Cache-Control
● Content-Language
● Content-Type
● Expires
●
Last-Modified
● Pragma
This header contains the full response length. So, if we’re downloading
something and would like to track the percentage of progress, then an additional
permission is required to access that header (see below).
To grant JavaScript access to any other response header, the server must list it in
the Access-Control-Expose-Headers header.
For example:
200 OK
Content-Type:text/html; charset=UTF-8
Content-Length: 12345
API-Key: 2c9de507f2c54aa1
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Expose-Headers: Content-Length,API-Key
“Non-simple” requests
We can use any HTTP-method: not just GET/POST , but also PATCH , DELETE and
others.
Some time ago no one could even assume that a webpage is able to do such
requests. So there may exist webservices that treat a non-standard method as a
signal: “That’s not a browser”. They can take it into account when checking access
rights.
So, to avoid misunderstandings, any “non-simple” request – that couldn’t be done in
the old times, the browser does not make such requests right away. Before it sends
a preliminary, so-called “preflight” request, asking for permission.
A preflight request uses method OPTIONS and has no body.
● Access-Control-Request-Method header has the requested method.
● Access-Control-Request-Headers header provides a comma-separated
list of non-simple HTTP-headers.
If the server agrees to serve the requests, then it should respond with status 200,
without body.
● The response header Access-Control-Allow-Methods must have the
allowed method.
● The response header Access-Control-Allow-Headers must have a list of
allowed headers.
● Additionally, the header Access-Control-Max-Age may specify a number of
seconds to cache the permissions. So the browser won’t have to send a preflight
for subsequent requests that satisfy given permissions.
Let’s see how it works step-by-step on example, for a cross-domain PATCH request
(this method is often used to update data):
There are three reasons why the request is not simple (one is enough):
● Method PATCH
● Content-Type is not one of: application/x-www-form-urlencoded ,
multipart/form-data , text/plain .
● “Non-simple” API-Key header.
OPTIONS /service.json
Host: site.com
Origin: https://javascript.info
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: Content-Type,API-Key
● Method: OPTIONS .
● The path – exactly the same as the main request: /service.json .
● Cross-origin special headers:
●
Origin – the source origin.
● Access-Control-Request-Method – requested method.
●
Access-Control-Request-Headers – a comma-separated list of “non-
simple” headers.
200 OK
Access-Control-Allow-Methods: PUT,PATCH,DELETE
Access-Control-Allow-Headers: API-Key,Content-Type,If-Modified-Since,Cache-Control
Access-Control-Max-Age: 86400
Now the browser can see that PATCH is in the list of allowed methods, and both
headers are in the list too, so it sends out the main request.
Besides, the preflight response is cached for time, specified by Access-Control-
Max-Age header (86400 seconds, one day), so subsequent requests will not cause
a preflight. Assuming that they fit the allowances, they will be sent directly.
PATCH /service.json
Host: site.com
Content-Type: application/json
API-Key: secret
Origin: https://javascript.info
Access-Control-Allow-Origin: https://javascript.info
Credentials
A cross-origin request by default does not bring any credentials (cookies or HTTP
authentication).
That’s uncommon for HTTP-requests. Usually, a request to http://site.com is
accompanied by all cookies from that domain. But cross-domain requests made by
JavaScript methods are an exception.
For example, fetch('http://another.com') does not send any cookies,
even those that belong to another.com domain.
Why?
That’s because a request with credentials is much more powerful than an
anonymous one. If allowed, it grants JavaScript the full power to act and access
sensitive information on behalf of a user.
Does the server really trust pages from Origin that much? Then it must explicitly
allow requests with credentials with an additional header.
To send credentials, we need to add the option credentials: "include" , like
this:
fetch('http://another.com', {
credentials: "include"
});
Now fetch sends cookies originating from another.com with out request to that
site.
If the server wishes to accept the request with credentials, it should add a header
Access-Control-Allow-Credentials: true to the response, in addition to
Access-Control-Allow-Origin .
For example:
200 OK
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Credentials: true
Summary
Networking methods split cross-origin requests into two kinds: “simple” and all the
others.
Simple requests must satisfy the following conditions:
● Method: GET, POST or HEAD.
● Headers – we can set only:
● Accept
● Accept-Language
● Content-Language
● Content-Type to the value application/x-www-form-urlencoded ,
multipart/form-data or text/plain .
The essential difference is that simple requests were doable since ancient times
using <form> or <script> tags, while non-simple were impossible for browsers
for a long time.
So, practical difference is that simple requests are sent right away, with Origin
header, but for other ones the browser makes a preliminary “preflight” request,
asking for permission.
For simple requests:
● → The browser sends Origin header with the origin.
● ← For requests without credentials (not sent default), the server should set:
●
Access-Control-Allow-Origin to * or same value as Origin
● ← For requests with credentials, the server should set:
● Access-Control-Allow-Origin to same value as Origin
● Access-Control-Allow-Credentials to true
✔ Tasks
As you probably know, there’s HTTP-header Referer , that usually contains an url
of the page which initiated a network request.
The questions:
To solution
Fetch API
So far, we know quite a bit about fetch .
Now let’s see the rest of API, to cover all its abilities.
Here’s the full list of all possible fetch options with their default values
(alternatives in comments):
referrer, referrerPolicy
That header contains the url of the page that made the request. In most scenarios, it
plays a very minor informational role, but sometimes, for security purposes, it makes
sense to remove or shorten it.
The referrer option allows to set any Referer within the current origin) or
disable it.
To send no referer, set an empty string:
fetch('/page', {
referrer: "" // no Referer header
});
fetch('/page', {
// assuming we're on https://javascript.info
// we can set any Referer header, but only within the current origin
referrer: "https://javascript.info/anotherpage"
});
Let’s say we have an admin zone with URL structure that shouldn’t be known from
outside of the site.
If we send a cross-origin fetch , then by default it sends the Referer header with
the full url of our page (except when we request from HTTPS to HTTP, then no
Referer ).
fetch('https://another.com/page', {
referrerPolicy: "no-referrer" // no Referer, same effect as referrer: ""
});
Otherwise, if we’d like the remote side to see only the domain where the request
comes from, but not the full URL, we can send only the “origin” part of it:
fetch('https://another.com/page', {
referrerPolicy: "strict-origin" // Referer: https://javascript.info
});
mode
That may be useful in contexts when the fetch url comes from 3rd-party, and we
want a “power off switch” to limit cross-origin capabilities.
credentials
The credentials option specifies whether fetch should send cookies and
HTTP-Authorization headers with the request.
● "same-origin" – the default, don’t send for cross-origin requests,
● "include" – always send, requires Accept-Control-Allow-
Credentials from cross-origin server,
● "omit" – never send, even for same-origin requests.
cache
By default, fetch requests make use of standard HTTP-caching. That is, it honors
Expires , Cache-Control headers, sends If-Modified-Since , and so on.
Just like regular HTTP-requests do.
The cache options allows to ignore HTTP-cache or fine-tune its usage:
● "default" – fetch uses standard HTTP-cache rules and headers;
● "no-store" – totally ignore HTTP-cache, this mode becomes the default if we
set a header If-Modified-Since , If-None-Match , If-Unmodified-
Since , If-Match , or If-Range ;
● "reload" – don’t take the result from HTTP-cache (if any), but populate cache
with the response (if response headers allow);
● "no-cache" – create a conditional request if there is a cached response, and a
normal request otherwise. Populate HTTP-cache with the response;
● "force-cache" – use a response from HTTP-cache, even if it’s stale. If there’s
no response in HTTP-cache, make a regular HTTP-request, behave normally;
● "only-if-cached" – use a response from HTTP-cache, even if it’s stale. If
there’s no response in HTTP-cache, then error. Only works when mode is
"same-origin" .
redirect
integrity
The integrity option allows to check if the response matches the known-ahead
checksum.
As described in the specification , supported hash-functions are SHA-256, SHA-
384, and SHA-512, there might be others depending on a browser.
For example, we’re downloading a file, and we know that it’s SHA-256 checksum is
“abc” (a real checksum is longer, of course).
We can put it in the integrity option, like this:
fetch('http://site.com/file', {
integrity: 'sha256-abd'
});
Then fetch will calculate SHA-256 on its own and compare it with our string. In
case of a mismatch, an error is triggered.
keepalive
The keepalive option indicates that the request may outlive the page.
For example, we gather statistics about how the current visitor uses our page
(mouse clicks, page fragments he views), to improve user experience.
When the visitor leaves our page – we’d like to save it on our server.
We can use window.onunload for that:
window.onunload = function() {
fetch('/analytics', {
method: 'POST',
body: "statistics",
keepalive: true
});
};
URL objects
The built-in URL class provides a convenient interface for creating and parsing
URLs.
There are no networking methods that require exactly an URL object, strings are
good enough. So technically we don’t have to use URL . But sometimes it can be
really helpful.
Creating an URL
● url – the URL string or path (if base is set, see below).
● base – an optional base, if set and url has only path, then the URL is
generated relative to base .
alert(url1); // https://javascript.info/profile/admin
alert(url2); // https://javascript.info/profile/admin
alert(testerUrl); // https://javascript.info/profile/tester
The URL object immediately allows us to access its components, so it’s a nice way
to parse the url, e.g.:
alert(url.protocol); // https:
alert(url.host); // javascript.info
alert(url.pathname); // /url
SearchParams “?…”
Let’s say we want to create an url with given search params, for instance,
https://google.com/search?query=JavaScript .
…But parameters need to be encoded if they contain spaces, non-latin letters, etc
(more about that below).
So there’s URL property for that: url.searchParams , an object of type
URLSearchParams .
For example:
alert(url); // https://google.com/search?query=test+me%21
alert(url); // https://google.com/search?q=test+me%21&tbs=qdr%3Ay
Encoding
There’s a standard RFC3986 that defines which characters are allowed and
which are not.
Those that are not allowed, must be encoded, for instance non-latin letters and
spaces – replaced with their UTF-8 codes, prefixed by % , such as %20 (a space
can be encoded by + , for historical reasons that’s allowed in URL too).
The good news is that URL objects handle all that automatically. We just supply all
parameters unencoded, and then convert the URL to the string:
url.searchParams.set('key', 'ъ');
alert(url); //https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D1%81%D1%82?key=%D1%8A
As you can see, both Тест in the url path and ъ in the parameter are encoded.
Encoding strings
If we’re using strings instead of URL objects, then we can encode manually using
built-in functions:
● encodeURI – encode URL as a whole.
●
encodeURI – decode it back.
●
encodeURIComponent – encode URL components, such as search
parameters, or a hash, or a pathname.
● decodeURIComponent – decodes it back.
That’s easy to understand if we look at the URL, that’s split into components in the
picture above:
http://site.com:8080/path/page?p1=v1&p2=v2#hash
…On the other hand, if we look at a single URL component, such as a search
parameter, we should encode more characters, e.g. ? , = and & are used for
formatting.
That’s what encodeURIComponent does. It encodes same characters as
encodeURI , plus a lot of others, to make the resulting value safe to use in any URL
component.
For example:
As we can see, encodeURI does not encode & , as this is a legit character in URL
as a whole.
But we should encode & inside a search parameter, otherwise, we get
q=Rock&Roll – that is actually q=Rock plus some obscure parameter Roll .
Not as intended.
So we should use only encodeURIComponent for each search parameter, to
correctly insert it in the URL string. The safest is to encode both name and value,
unless we’re absolutely sure that either has only allowed characters.
Why URL?
Lots of old code uses these functions, these are sometimes convenient, and by noo
means not dead.
But in modern code, it’s recommended to use classes URL and
URLSearchParams .
One of the reason is: they are based on the recent URI spec: RFC3986 , while
encode* functions are based on the obsolete version RFC2396 .
As we can see, encodeURI replaced square brackets [...] , that’s not correct,
the reason is: IPv6 urls did not exist at the time of RFC2396 (August 1998).
Such cases are rare, encode* functions work well most of the time, it’s just one of
the reason to prefer new APIs.
XMLHttpRequest
XMLHttpRequest is a built-in browser object that allows to make HTTP requests
in JavaScript.
Despite of having the word “XML” in its name, it can operate on any data, not only in
XML format. We can upload/download files, track progress and much more.
Right now, there’s another, more modern method fetch , that somewhat
deprecates XMLHttpRequest .
Does that sound familiar? If yes, then all right, go on with XMLHttpRequest .
Otherwise, please head on to Fetch.
The basics
1. Create XMLHttpRequest :
2. Initialize it:
Please note that open call, contrary to its name, does not open the connection. It
only configures the request, but the network activity only starts with the call of
send .
3. Send it out.
xhr.send([body])
This method opens the connection and sends the request to server. The optional
body parameter contains the request body.
Some request methods like GET do not have a body. And some of them like
POST use body to send the data to the server. We’ll see examples later.
xhr.onload = function() {
alert(`Loaded: ${xhr.status} ${xhr.response}`);
};
xhr.onprogress = function(event) {
if (event.lengthComputable) {
alert(`Received ${event.loaded} of ${event.total} bytes`);
} else {
alert(`Received ${event.loaded} bytes`); // no Content-Length
}
};
xhr.onerror = function() {
alert("Request failed");
};
Once the server has responded, we can receive the result in the following properties
of the request object:
status
HTTP status code (a number): 200 , 404 , 403 and so on, can be 0 in case of a
non-HTTP failure.
statusText
HTTP status message (a string): usually OK for 200 , Not Found for 404 ,
Forbidden for 403 and so on.
If the request does not succeed within the given time, it gets canceled and
timeout event triggers.
Response Type
xhr.responseType = 'json';
xhr.send();
Please note:
In the old scripts you may also find xhr.responseText and even
xhr.responseXML properties.
They exist for historical reasons, to get either a string or XML document.
Nowadays, we should set the format in xhr.responseType and get
xhr.response as demonstrated above.
Ready states
xhr.onreadystatechange = function() {
if (xhr.readyState == 3) {
// loading
}
if (xhr.readyState == 4) {
// request finished
}
};
You can find readystatechange listeners in really old code, it’s there for
historical reasons, as there was a time when there were no load and other events.
Aborting request
We can terminate the request at any time. The call to xhr.abort() does that:
Synchronous requests
If in the open method the third parameter async is set to false , the request is
made synchronously.
In other words, JavaScript execution pauses at send() and resumes when the
response is received. Somewhat like alert or prompt commands.
try {
xhr.send();
if (xhr.status != 200) {
alert(`Error ${xhr.status}: ${xhr.statusText}`);
} else {
alert(xhr.response);
}
} catch(err) { // instead of onerror
alert("Request failed");
}
It might look good, but synchronous calls are used rarely, because they block in-
page JavaScript till the loading is complete. In some browsers it becomes impossible
to scroll. If a synchronous call takes too much time, the browser may suggest to
close the “hanging” webpage.
Many advanced capabilities of XMLHttpRequest , like requesting from another
domain or specifying a timeout, are unavailable for synchronous requests. Also, as
you can see, no progress indication.
Because of all that, synchronous requests are used very sparingly, almost never. We
won’t talk about them any more.
HTTP-headers
XMLHttpRequest allows both to send custom headers and read headers from the
response.
There are 3 methods for HTTP-headers:
setRequestHeader(name, value)
Sets the request header with the given name and value .
For instance:
xhr.setRequestHeader('Content-Type', 'application/json');
⚠ Headers limitations
Several headers are managed exclusively by the browser, e.g. Referer and
Host . The full list is in the specification .
XMLHttpRequest is not allowed to change them, for the sake of user safety
and correctness of the request.
Once the header is set, it’s set. Additional calls add information to the header,
don’t overwrite it.
For instance:
xhr.setRequestHeader('X-Auth', '123');
xhr.setRequestHeader('X-Auth', '456');
For instance:
xhr.getResponseHeader('Content-Type')
getAllResponseHeaders()
Returns all response headers, except Set-Cookie and Set-Cookie2 .
Cache-Control: max-age=31536000
Content-Length: 4260
Content-Type: image/png
Date: Sat, 08 Sep 2012 16:53:16 GMT
The line break between headers is always "\r\n" (doesn’t depend on OS), so we
can easily split it into individual headers. The separator between the name and the
value is always a colon followed by a space ": " . That’s fixed in the specification.
So, if we want to get an object with name/value pairs, we need to throw in a bit JS.
Like this (assuming that if two headers have the same name, then the latter one
overwrites the former one):
POST, FormData
The syntax:
let formData = new FormData([form]); // creates an object, optionally fill from <for
formData.append(name, value); // appends a field
We create it, optionally from a form, append more fields if needed, and then:
For instance:
<form name="person">
<input name="name" value="John">
<input name="surname" value="Smith">
</form>
<script>
// pre-fill FormData from the form
let formData = new FormData(document.forms.person);
// send it out
let xhr = new XMLHttpRequest();
xhr.open("POST", "/article/xmlhttprequest/post/user");
xhr.send(formData);
</script>
xhr.open("POST", '/submit')
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
xhr.send(json);
The .send(body) method is pretty omnivore. It can send almost everything,
including Blob and BufferSource objects.
Upload progress
That is: if we POST something, XMLHttpRequest first uploads our data (the
request body), then downloads the response.
If we’re uploading something big, then we’re surely more interested in tracking the
upload progress. But xhr.onprogress doesn’t help here.
Example of handlers:
xhr.upload.onprogress = function(event) {
alert(`Uploaded ${event.loaded} of ${event.total} bytes`);
};
xhr.upload.onload = function() {
alert(`Upload finished successfully.`);
};
xhr.upload.onerror = function() {
alert(`Error during the upload: ${xhr.status}`);
};
xhr.open("POST", "/article/xmlhttprequest/post/upload");
xhr.send(file);
}
</script>
Cross-origin requests
XMLHttpRequest can make cross-domain requests, using the same CORS policy
as fetch.
Just like fetch , it doesn’t send cookies and HTTP-authorization to another origin
by default. To enable them, set xhr.withCredentials to true :
xhr.open('POST', 'http://anywhere.com/request');
...
See the chapter Fetch: Cross-Origin Requests for details about cross-origin headers.
Summary
xhr.open('GET', '/my/url');
xhr.send();
xhr.onload = function() {
if (xhr.status != 200) { // HTTP error?
// handle error
alert( 'Error: ' + xhr.status);
return;
}
xhr.onprogress = function(event) {
// report progress
alert(`Loaded ${event.loaded} of ${event.total}`);
};
xhr.onerror = function() {
// handle non-HTTP error (e.g. network down)
};
There are actually more events, the modern specification lists them (in the
lifecycle order):
● loadstart – the request has started.
● progress – a data packet of the response has arrived, the whole response
body at the moment is in responseText .
● abort – the request was canceled by the call xhr.abort() .
● error – connection error has occurred, e.g. wrong domain name. Doesn’t
happen for HTTP-errors like 404.
●
load – the request has finished successfully.
●
timeout – the request was canceled due to timeout (only happens if it was set).
●
loadend – triggers after load , error , timeout or abort .
The error , abort , timeout , and load events are mutually exclusive. Only
one of them may happen.
The most used events are load completion ( load ), load failure ( error ), or we can
use a single loadend handler and check the response to see what happened.
How to resume the upload after lost connection? There’s no built-in option for that,
but we have the pieces to implement it.
Resumable uploads should come with upload progress indication, as we expect big
files (if we may need to resume). So, as fetch doesn’t allow to track upload
progress, we’ll use XMLHttpRequest.
To resume upload, we need to know how much was uploaded till the connection was
lost.
There’s xhr.upload.onprogress to track upload progress.
Unfortunately, it’s useless here, as it triggers when the data is sent, but was it
received by the server? The browser doesn’t know.
Maybe it was buffered by a local network proxy, or maybe the remote server process
just died and couldn’t process them, or it was just lost in the middle when the
connection broke, and didn’t reach the receiver.
So, this event is only useful to show a nice progress bar.
To resume upload, we need to know exactly the number of bytes received by the
server. And only the server can tell that.
Algorithm
1. First, we create a file id, to uniquely identify the file we’re uploading, e.g.
That’s needed for resume upload, to tell the server what we’re resuming.
2. Send a request to the server, asking how many bytes it already has, like this:
This assumes that the server tracks file uploads by X-File-Id header. Should
be implemented at server-side.
3. Then, we can use Blob method slice to send the file from startByte :
// send file id, so that the server knows which file to resume
xhr.setRequestHeader('X-File-Id', fileId);
// send the byte we're resuming from, so the server knows we're resuming
xhr.setRequestHeader('X-Start-Byte', startByte);
Here we send the server both file id as X-File-Id , so it knows which file we’re
uploading, and the starting byte as X-Start-Byte , so it knows we’re not
uploading it initially, but resuming.
The server should check its records, and if there was an upload of that file, and
the current uploaded size is exactly X-Start-Byte , then append the data to it.
Here’s the demo with both client and server code, written on Node.js.
It works only partially on this site, as Node.js is behind another server named Nginx,
that buffers uploads, passing them to Node.js when fully complete.
But you can download it and run locally for the full demonstration:
https://plnkr.co/edit/uwIHsRek1zB1NhjxDjC9?p=preview
As you can see, modern networking methods are close to file managers in their
capabilities – control over headers, progress indicator, sending file parts, etc.
Long polling
Long polling is the simplest way of having persistent connection with server, that
doesn’t use any specific protocol like WebSocket or Server Side Events.
Being very easy to implement, it’s also good enough in a lot of cases.
Regular Polling
The simplest way to get new information from the server is polling.
That is, periodical requests to the server: “Hello, I’m here, do you have any
information for me?”. For example, once in 10 seconds.
In response, the server first takes a notice to itself that the client is online, and
second – sends a packet of messages it got till that moment.
That works, but there are downsides:
1. Messages are passed with a delay up to 10 seconds.
2. Even if there are no messages, the server is bombed with requests every 10
seconds. That’s quite a load to handle for backend, speaking performance-wise.
So, if we’re talking about a very small service, the approach may be viable.
But generally, it needs an improvement.
Long polling
The situation when the browser sent a request and has a pending connection with
the server, is standard for this method. Only when a message is delivered, the
connection is reestablished.
Even if the connection is lost, because of, say, a network error, the browser
immediately sends a new request.
A sketch of client-side code:
if (response.status == 502) {
// Connection timeout, happens when the connection was pending for too long
// let's reconnect
await subscribe();
} else if (response.status != 200) {
// Show Error
showMessage(response.statusText);
// Reconnect in one second
await new Promise(resolve => setTimeout(resolve, 1000));
await subscribe();
} else {
// Got message
let message = await response.text();
showMessage(message);
await subscribe();
}
}
subscribe();
The subscribe() function makes a fetch, then waits for the response, handles it
and calls itself again.
Demo: a chat
Here’s a demo:
https://plnkr.co/edit/p0VXeg5MIwpxPjq7LWdR?p=preview
Area of usage
WebSocket
The WebSocket protocol, described in the specification RFC 6455 provides a
way to exchange data between browser and server via a persistent connection.
Once a websocket connection is established, both client and server may send the
data to each other.
WebSocket is especially great for services that require continuous data exchange,
e.g. online games, real-time trading systems and so on.
A simple example
There’s also encrypted wss:// protocol. It’s like HTTPS for websockets.
The wss:// protocol not only encrypted, but also more reliable.
That’s because ws:// data is not encrypted, visible for any intermediary. Old
proxy servers do not know about WebSocket, they may see “strange” headers
and abort the connection.
On the other hand, wss:// is WebSocket over TLS, (same as HTTPS is HTTP
over TLS), the transport security layer encrypts the data at sender and decrypts
at the receiver, so it passes encrypted through proxies. They can’t see what’s
inside and let it through.
Once the socket is created, we should listen to events on it. There are totally 4
events:
● open – connection established,
● message – data received,
● error – websocket error,
● close – connection closed.
Here’s an example:
socket.onopen = function(e) {
alert("[open] Connection established, send -> server");
socket.send("My name is John");
};
socket.onmessage = function(event) {
alert(`[message] Data received: ${event.data} <- server`);
};
socket.onclose = function(event) {
if (event.wasClean) {
alert(`[close] Connection closed cleanly, code=${event.code} reason=${event.reas
} else {
// e.g. server process killed or network down
// event.code is usually 1006 in this case
alert('[close] Connection died');
}
};
socket.onerror = function(error) {
alert(`[error] ${error.message}`);
};
For demo purposes, there’s a small server server.js written in Node.js, for the
example above, running. It responds with “hello”, then waits 5 seconds and closes
the connection.
So you’ll see events open → message → close .
That’s actually it, we can talk WebSocket already. Quite simple, isn’t it?
Now let’s talk more in-depth.
Opening a websocket
When new WebSocket(url) is created, it starts connecting immediately.
During the connection the browser (using headers) asks the server: “Do you support
Websocket?” And if the server replies “yes”, then the talk continues in WebSocket
protocol, which is not HTTP at all.
GET /chat
Host: javascript.info
Origin: https://javascript.info
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
●
Origin – the origin of the client page, e.g. https://javascript.info .
WebSocket objects are cross-origin by nature. There are no special headers or
other limitations. Old servers are unable to handle WebSocket anyway, so there
are no compabitility issues. But Origin header is important, as it allows the
server to decide whether or not to talk WebSocket with this website.
●
Connection: Upgrade – signals that the client would like to change the
protocol.
● Upgrade: websocket – the requested protocol is “websocket”.
● Sec-WebSocket-Key – a random browser-generated key for security.
● Sec-WebSocket-Version – WebSocket protocol version, 13 is the current
one.
WebSocket handshake can’t be emulated
We can’t use XMLHttpRequest or fetch to make this kind of HTTP-request,
because JavaScript is not allowed to set these headers.
If the server agrees to switch to WebSocket, it should send code 101 response:
For instance:
● Sec-WebSocket-Extensions: deflate-frame means that the browser
supports data compression. An extension is something related to transferring the
data, not data itself.
●
Sec-WebSocket-Protocol: soap, wamp means that we’d like to transfer
not just any data, but the data in SOAP or WAMP (“The WebSocket
Application Messaging Protocol”) protocols. WebSocket subprotocols are
registered in the IANA catalogue .
The server should respond with a list of protocols and extensions that it agrees to
use.
For example, the request:
GET /chat
Host: javascript.info
Upgrade: websocket
Connection: Upgrade
Origin: https://javascript.info
Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: deflate-frame
Sec-WebSocket-Protocol: soap, wamp
Response:
Here the server responds that it supports the extension “deflate-frame”, and only
SOAP of the requested subprotocols.
WebSocket data
When we receive the data, text always comes as string. And for binary data, we
can choose between Blob and ArrayBuffer formats.
socket.bufferType = "arraybuffer";
socket.onmessage = (event) => {
// event.data is either a string (if text) or arraybuffer (if binary)
};
Rate limiting
Imagine, our app is generating a lot of data to send. But the user has a slow network
connection, maybe on a mobile, outside of a city.
We can call socket.send(data) again and again. But the data will be buffered
(stored) in memory and sent out only as fast as network speed allows.
The socket.bufferedAmount property stores how many bytes are buffered at
this moment, waiting to be sent over the network.
We can examine it to see whether the socket is actually available for transmission.
Connection close
Normally, when a party wants to close the connection (both browser and server have
equal rights), they send a “connection close frame” with a numeric code and a
textual reason.
The method for that is:
socket.close([code], [reason]);
●
code is a special WebSocket closing code (optional)
●
reason is a string that describes the reason of closing (optional)
Then the other party in close event handler gets the code and the reason, e.g.:
// closing party:
socket.close(1000, "Work complete");
WebSocket codes are somewhat like HTTP codes, but different. In particular, any
codes less than 1000 are reserved, there’ll be an error if we try to set such a code.
Connection state
Chat example
Let’s review a chat example using browser WebSocket API and Node.js WebSocket
module https://github.com/websockets/ws .
HTML: there’s a <form> to send messages and a <div> for incoming messages:
socket.send(outgoingMessage);
return false;
};
Server-side code is a little bit beyond our scope here. We’re using browser
WebSocket API, a server may have another library.
Still it can also be pretty simple. We’ll use Node.js with
https://github.com/websockets/ws module for websockets.
The server-side algorithm will be:
1. Create clients = new Set() – a set of sockets.
2. For each accepted websocket, clients.add(socket) and add message
event listener for its messages.
3. When a message received: iterate over clients and send it to everyone.
4. When a connection is closed: clients.delete(socket) .
function onSocketConnect(ws) {
clients.add(ws);
ws.on('message', function(message) {
message = message.slice(0, 50); // max message length will be 50
ws.on('close', function() {
clients.delete(ws);
});
}
Send
You can also download it (upper-right button in the iframe) and run locally. Just don’t
forget to install Node.js and npm install ws before running.
Summary
WebSocket is a modern way to have persistent browser-server connections.
● WebSockets don’t have cross-origin limitations.
●
They are well-supported in browsers.
●
Can send/receive strings and binary data.
Events:
●
open ,
● message ,
●
error ,
●
close .
WebSocket by itself does not include reconnection, authentication and many other
high-level mechanisms. So there are client/server libraries for that, and it’s also
possible to implement these capabilities manually.
Sometimes, to integrate WebSocket into existing project, people run WebSocket
server in parallel with the main HTTP-server, and they share a single database.
Requests to WebSocket use wss://ws.site.com , a subdomain that leads to
WebSocket server, while https://site.com goes to the main HTTP-server.
Surely, other ways of integration are also possible. Many servers (such as Node.js)
can support both HTTP and WebSocket protocols.
WebSocket EventSource
Bi-directional: both client and server can exchange messages One-directional: only server sends data
Getting messages
The browser will connect to url and keep the connection open, waiting for events.
The server should respond with status 200 and the header Content-Type:
text/event-stream , then keep the connection and write messages into it in the
special format, like this:
data: Message 1
data: Message 2
data: Message 3
data: of two lines
●
A message text goes after data: , the space after the semicolon is optional.
●
Messages are delimited with double line breaks \n\n .
●
To send a line break \n , we can immediately one more data: (3rd message
above).
…So we can assume that one data: holds exactly one message.
eventSource.onmessage = function(event) {
console.log("New message", event.data);
// will log 3 times for the data stream above
};
// or eventSource.addEventListener('message', ...)
Cross-domain requests
EventSource supports cross-origin requests, like fetch any other networking
methods. We can use any URL:
The remote server will get the Origin header and must respond with Access-
Control-Allow-Origin to proceed.
Please see the chapter Fetch: Cross-Origin Requests for more details about cross-
domain headers.
Reconnection
Upon creation, new EventSource connects to the server, and if the connection is
broken – reconnects.
That’s very convenient, as we don’t have to care about it.
There’s a small delay between reconnections, a few seconds by default.
The server can set the recommended delay using retry: in response (in
milliseconds):
retry: 15000
data: Hello, I set the reconnection delay to 15 seconds
The retry: may come both together with some data, or as a standalone message.
The browser should wait that much before reconnect. If the network connection is
lost, the browser may wait till it’s restored, and then retry.
●
If the server wants the browser to stop reconnecting, it should respond with HTTP
status 204.
●
If the browser wants to close the connection, it should call
eventSource.close() :
eventSource.close();
Please note:
There’s no way to “reopen” a closed connection. If we’d like to connect again,
just create a new EventSource .
Message id
When a connection breaks due to network problems, either side can’t be sure which
messages were received, and which weren’t.
To correctly resume the connection, each message should have an id field, like
this:
data: Message 1
id: 1
data: Message 2
id: 2
data: Message 3
data: of two lines
id: 3
Please note: the id: is appended below the message data, to ensure that
lastEventId is updated after the message data is received.
The EventSource object has readyState property, that has one of three
values:
Event types
The server may specify another type of event with event: ... at the event start.
For example:
event: join
data: Bob
data: Hello
event: leave
data: Bob
To handle custom events, we must use addEventListener , not onmessage :
Full example
Here’s the server that sends messages with 1 , 2 , 3 , then bye and breaks the
connection.
Then the browser automatically reconnects.
https://plnkr.co/edit/LmOdPFHJdD3yIrRGhkSF?p=preview
Summary
Overall cross-domain security is same as for fetch and other network methods.
readyState
The current connection state: either EventSource.CONNECTING (=0) ,
EventSource.OPEN (=1) or EventSource.CLOSED (=2) .
lastEventId
The last received id . Upon reconnection the browser sends it in the header Last-
Event-ID .
Methods
close()
Closes the connection соединение.
Events
message
Message received, the data is in event.data .
open
The connection is established.
error
In case of an error, including both lost connection (will auto-reconnect) and fatal
errors. We can check readyState to see if the reconnection is being attempted.
The server may set a custom event name in event: . Such events should be
handled using addEventListener , not on<event> .
We can also access cookies from the browser, using document.cookie property.
There are many tricky things about cookies and their options. In this chapter we’ll
cover them in detail.
Assuming you’re on a website, it’s possible to see the cookies from it, like this:
Writing to document.cookie
We can write to document.cookie . But it’s not a data property, it’s an accessor.
An assignment to it is treated specially.
A write operation to document.cookie passes through the browser that
updates cookies mentioned in it, but doesn’t touch other cookies.
For instance, this call sets a cookie with the name user and value John :
If you run it, then probably you’ll see multiple cookies. That’s because
document.cookie= operation does not overwrite all cookies. It only sets the
mentioned cookie user .
Technically, name and value can have any characters, but to keep the formatting
valid they should be escaped using a built-in encodeURIComponent function:
⚠ Limitations
There are few limitations:
● The name=value pair, after encodeURIComponent , should not exceed
4kb. So we can’t store anything huge in a cookie.
●
The total number of cookies per domain is limited to around 20+, the exact
limit depends on a browser.
Cookies have several options, many of them are important and should be set.
The options are listed after key=value , delimited by ; , like this:
path
● path=/mypath
The url path prefix, the cookie will be accessible for pages under that path. Must be
absolute. By default, it’s the current path.
If a cookie is set with path=/admin , it’s visible at pages /admin and
/admin/something , but not at /home or /adminpage .
Usually, we should set path to the root: path=/ to make the cookie accessible
from all website pages.
domain
●
domain=site.com
A domain where the cookie is accessible. In practice though, there are limitations.
We can’t set any domain.
By default, a cookie is accessible only at the domain that set it. So, if the cookie was
set by site.com , we won’t get it other.com .
…But what’s more tricky, we also won’t get the cookie at a subdomain
forum.site.com !
// at site.com
document.cookie = "user=John"
// at forum.site.com
alert(document.cookie); // no user
It’s a safety restriction, to allow us to store sensitive data in cookies, that should be
available only on one site.
…But if we’d like to allow subdomains like forum.site.com get a cookie, that’s
possible. When setting a cookie at site.com , we should explicitly set domain
option to the root domain: domain=site.com :
// at site.com
// make the cookie accessible on any subdomain *.site.com:
document.cookie = "user=John; domain=site.com"
// later
// at forum.site.com
alert(document.cookie); // has cookie user=John
expires, max-age
By default, if a cookie doesn’t have one of these options, it disappears when the
browser is closed. Such cookies are called “session cookies”
To let cookies survive browser close, we can set either expires or max-age
option.
●
expires=Tue, 19 Jan 2038 03:14:07 GMT
That is, cookies are domain-based, they do not distinguish between the protocols.
With this option, if a cookie is set by https://site.com , then it doesn’t appear
when the same site is accessed by HTTP, as http://site.com . So if a cookie
has sensitive content that should never be sent over unencrypted HTTP, then the
flag is the right thing.
samesite
That’s another security attribute somesite . It’s designed to protect from so-called
XSRF (cross-site request forgery) attacks.
To understand how it works and when it’s useful, let’s take a look at XSRF attacks.
XSRF attack
Imagine, you are logged into the site bank.com . That is: you have an
authentication cookie from that site. Your browser sends it to bank.com with every
request, so that it recognizes you and performs all sensitive financial operations.
Now, while browsing the web in another window, you occasionally come to another
site evil.com , that automatically submits a form <form
action="https://bank.com/pay"> to bank.com with input fields that initiate
a transaction to the hacker’s account.
The form is submitted from evil.com directly to the bank site, and your cookie is
also sent, just because it’s sent every time you visit bank.com . So the bank
recognizes you and actually performs the payment.
That’s called a cross-site request forgery (or XSRF) attack.
Real banks are protected from it of course. All forms generated by bank.com have
a special field, so called “xsrf protection token”, that an evil page can’t neither
generate, nor somehow extract from a remote page (it can submit a form there, but
can’t get the data back).
But that takes time to implement: we need to ensure that every form has the token
field, and we must also check all requests.
A cookie with samesite=strict is never sent if the user comes from outside the
site.
In other words, whether a user follows a link from their mail or submits a form from
evil.com , or does any operation that originates from another domain, the cookie
is not sent.
If authentication cookies have samesite option, then XSRF attack has no chances
to succeed, because a submission from evil.com comes without cookies. So
bank.com will not recognize the user and will not proceed with the payment.
The protection is quite reliable. Only operations that come from bank.com will send
the samesite cookie.
We could work around that by using two cookies: one for “general recognition”, only
for the purposes of saying: “Hello, John”, and the other one for data-changing
operations with samesite=strict . Then a person coming from outside of the site
will see a welcome, but payments must be initiated from the bank website.
●
samesite=lax
A more relaxed approach that also protects from XSRF and doesn’t break user
experience.
Lax mode, just like strict , forbids the browser to send cookies when coming from
outside the site, but adds an exception.
A samesite=lax cookie is sent if both of these conditions are true:
So, what samesite=lax does is basically allows a most common “go to URL”
operation to have cookies. E.g. opening a website link from notes satisfies these
conditions.
But anything more complicated, like AJAX request from another site or a form
submittion loses cookies.
If that’s fine for you, then adding samesite=lax will probably not break the user
experience and add protection.
Overall, samesite is a great option, but it has an important drawback:
●
samesite is ignored (not supported) by old browsers, year 2017 or so.
httpOnly
This option has nothing to do with JavaScript, but we have to mention it for
completeness.
The web-server uses Set-Cookie header to set a cookie. And it may set the
httpOnly option.
This option forbids any JavaScript access to the cookie. We can’t see such cookie or
manipulate it using document.cookie .
That’s used as a precaution measure, to protect from certain attacks when a hacker
injects his own JavaScript code into a page and waits for a user to visit that page.
That shouldn’t be possible at all, a hacker should not be able to inject their code into
our site, but there may be bugs that let hackers do it.
Normally, if such thing happens, and a user visits a web-page with hacker’s code,
then that code executes and gains access to document.cookie with user cookies
containing authentication information. That’s bad.
But if a cookie is httpOnly , then document.cookie doesn’t see it, so it is
protected.
Here’s a small set of functions to work with cookies, more convenient than a manual
modification of document.cookie .
There exist many cookie libraries for that, so these are for demo purposes. Fully
working though.
getCookie(name)
The shortest way to access cookie is to use a regular expression.
The function getCookie(name) returns the cookie with the given name :
options = {
path: '/',
// add other defaults here if necessary
...options
};
if (options.expires.toUTCString) {
options.expires = options.expires.toUTCString();
}
document.cookie = updatedCookie;
}
// Example of use:
setCookie('user', 'John', {secure: true, 'max-age': 3600});
deleteCookie(name)
To delete a cookie, we can call it with a negative expiration date:
function deleteCookie(name) {
setCookie(name, "", {
'max-age': -1
})
}
Together: cookie.js.
A cookie is called “third-party” if it’s placed by domain other than the user is visiting.
For instance:
1. A page at site.com loads a banner from another site: <img
src="https://ads.com/banner.png"> .
2. Along with the banner, the remote server at ads.com may set Set-Cookie
header with cookie like id=1234 . Such cookie originates from ads.com
domain, and will only be visible at ads.com :
3. Next time when ads.com is accessed, the remote server gets the id cookie
and recognizes the user:
4. What’s even more important, when the users moves from site.com to another
site other.com that also has a banner, then ads.com gets the cookie, as it
belongs to ads.com , thus recognizing the visitor and tracking him as he moves
between sites:
Third-party cookies are traditionally used for tracking and ads services, due to their
nature. They are bound to the originating domain, so ads.com can track the same
user between different sites, if they all access it.
Naturally, some people don’t like being tracked, so browsers allow to disable such
cookies.
Also, some modern browsers employ special policies for such cookies:
●
Safari does not allow third-party cookies at all.
● Firefox comes with a “black list” of third-party domains where it blocks third-party
cookies.
Please note:
If we load a script from a third-party domain, like <script
src="https://google-analytics.com/analytics.js"> , and that
script uses document.cookie to set a cookie, then such cookie is not third-
party.
If a script sets a cookie, then no matter where the script came from – it belongs
to the domain of the current webpage.
Appendix: GDPR
This topic is not related to JavaScript at all, just something to keep in mind when
setting cookies.
There’s a legislation in Europe called GDPR, that enforces a set of rules for websites
to respect users’ privacy. And one of such rules is to require an explicit permission
for tracking cookies from a user.
Please note, that’s only about tracking/identifying cookies.
So, if we set a cookie that just saves some information, but neither tracks nor
identifies the user, then we are free to do it.
But if we are going to set a cookie with an authentication session or a tracking id,
then a user must allow that.
Websites generally have two variants of following GDPR. You must have seen them
both already in the web:
1. If a website wants to set tracking cookies only for authenticated users.
To do so, the registration form should have a checkbox like “accept the privacy
policy”, the user must check it, and then the website is free to set auth cookies.
2. If a website wants to set tracking cookies for everyone.
To do so legally, a website shows a modal “splash screen” for newcomers, and
require them to agree for cookies. Then the website can set them and let people
see the content. That can be disturbing for new visitors though. No one likes to
see “must-click” modal splash screens instead of the content. But GDPR requires
an explicit agreement.
GDPR is not only about cookies, it’s about other privacy-related issues too, but that’s
too much beyond our scope.
Summary
Cookie options:
●
path=/ , by default current path, makes the cookie visible only under that path.
●
domain=site.com , by default a cookie is visible on current domain only, if set
explicitly to the domain, makes the cookie visible on subdomains.
●
expires or max-age sets cookie expiration time, without them the cookie dies
when the browser is closed.
●
secure makes the cookie HTTPS-only.
●
samesite forbids the browser to send the cookie with requests coming from
outside the site, helps to prevent XSRF attacks.
Additionally:
● Third-party cookies may be forbidden by the browser, e.g. Safari does that by
default.
●
When setting a tracking cookie for EU citizens, GDPR requires to ask for
permission.
LocalStorage, sessionStorage
Web storage objects localStorage and sessionStorage allow to save
key/value pairs in the browser.
What’s interesting about them is that the data survives a page refresh (for
sessionStorage ) and even a full browser restart (for localStorage ). We’ll
see that very soon.
We already have cookies. Why additional objects?
● Unlike cookies, web storage objects are not sent to server with each request.
Because of that, we can store much more. Most browsers allow at least 2
megabytes of data (or more) and have settings to configure that.
●
Also unlike cookies, the server can’t manipulate storage objects via HTTP
headers. Everything’s done in JavaScript.
● The storage is bound to the origin (domain/protocol/port triplet). That is, different
protocols or subdomains infer different storage objects, they can’t access data
from each other.
localStorage demo
localStorage.setItem('test', 1);
…And close/open the browser or just open the same page in a different window,
then you can get it like this:
alert( localStorage.getItem('test') ); // 1
We only have to be on the same origin (domain/port/protocol), the url path can be
different.
The localStorage is shared between all windows with the same origin, so if we
set the data in one window, the change becomes visible in another one.
Object-like access
We can also use a plain object way of getting/setting keys, like this:
// set key
localStorage.test = 2;
// get key
alert( localStorage.test ); // 2
// remove key
delete localStorage.test;
That’s allowed for historical reasons, and mostly works, but generally not
recommended for two reasons:
1. If the key is user-generated, it can be anything, like length or toString , or
another built-in method of localStorage . In that case getItem/setItem
work fine, while object-like access fails:
2. There’s a storage event, it triggers when we modify the data. That event does
not happen for object-like access. We’ll see that later in this chapter.
As we’ve seen, the methods provide “get/set/remove by key” functionality. But how to
get all saved values or keys?
Unfortunately, storage objects are not iterable.
One way is to loop over them as over an array:
// bad try
for(let key in localStorage) {
alert(key); // shows getItem, setItem and other built-in stuff
}
…So we need either to filter fields from the prototype with hasOwnProperty
check:
for(let key in localStorage) {
if (!localStorage.hasOwnProperty(key)) {
continue; // skip keys like "setItem", "getItem" etc
}
alert(`${key}: ${localStorage.getItem(key)}`);
}
…Or just get the “own” keys with Object.keys and then loop over them if
needed:
The latter works, because Object.keys only returns the keys that belong to the
object, ignoring the prototype.
Strings only
// sometime later
let user = JSON.parse( sessionStorage.user );
alert( user.name ); // John
Also it is possible to stringify the whole storage object, e.g. for debugging purposes:
Properties and methods are the same, but it’s much more limited:
●
The sessionStorage exists only within the current browser tab.
● Another tab with the same page will have a different storage.
● But it is shared between iframes in the tab (assuming they come from the same
origin).
● The data survives page refresh, but not closing/opening the tab.
sessionStorage.setItem('test', 1);
…Then refresh the page. Now you can still get the data:
…But if you open the same page in another tab, and try again there, the code above
returns null , meaning “nothing found”.
That’s exactly because sessionStorage is bound not only to the origin, but also
to the browser tab. For that reason, sessionStorage is used sparingly.
Storage event
The important thing is: the event triggers on all window objects where the storage
is accessible, except the one that caused it.
Let’s elaborate.
Imagine, you have two windows with the same site in each. So localStorage is
shared between them.
If both windows are listening for window.onstorage , then each one will react on
updates that happened in the other one.
localStorage.setItem('now', Date.now());
Please note that the event also contains: event.url – the url of the document
where the data was updated.
Also, event.storageArea contains the storage object – the event is the same
for both sessionStorage and localStorage , so storageArea references
the one that was modified. We may even want to set something back in it, to
“respond” to a change.
That allows different windows from the same origin to exchange messages.
Modern browsers also support Broadcast channel API , the special API for same-
origin inter-window communication, it’s more full featured, but less supported. There
are libraries that polyfill that API, based on localStorage , that make it available
everywhere.
Summary
localStorage sessionStorage
Shared between all tabs and windows with the Visible within a browser tab, including iframes from the
same origin same origin
Survives browser restart Survives page refresh (but not tab close)
API:
●
setItem(key, value) – store key/value pair.
● getItem(key) – get the value by key.
●
removeItem(key) – remove the key with its value.
● clear() – delete everything.
●
key(index) – get the key number index .
● length – the number of stored items.
● Use Object.keys to get all keys.
● We access keys as object properties, in that case storage event isn’t triggered.
Storage event:
● Triggers on setItem , removeItem , clear calls.
●
Contains all the data about the operation, the document url and the storage
object.
●
Triggers on all window objects that have access to the storage except the one
that generated it (within a tab for sessionStorage , globally for
localStorage ).
✔ Tasks
So, if the user occasionally closes the page, and opens it again, he’ll find his
unfinished input at place.
Like this:
Write here
Clear
To solution
IndexedDB
IndexedDB is a built-in database, much more powerful than localStorage .
●
Key/value storage: value can be (almost) anything, multiple key types.
● Supports transactions for reliability.
●
Supports key range queries, indexes.
● Can store much more data than localStorage .
We can also use async/await with the help of a promise-based wrapper, like
https://github.com/jakearchibald/idb . That’s pretty convenient, but the wrapper is
not perfect, it can’t replace events for all cases. So we’ll start with events, and then,
after we gain understanding of IndexedDb, we’ll use the wrapper.
Open database
●
name – a string, the database name.
●
version – a positive integer version, by default 1 (explained below).
We can have many databases with different names, but all of them exist within the
current origin (domain/protocol/port). Different websites can’t access databases of
each other.
After the call, we need to listen to events on openRequest object:
● success : database is ready, there’s the “database object” in
openRequest.result , that we should use it for further calls.
● error : opening failed.
●
upgradeneeded : database is ready, but its version is outdated (see below).
IndexedDB has a built-in mechanism of “schema versioning”, absent in server-
side databases.
Unlike server-side databases, IndexedDB is client-side, the data is stored in the
browser, so we, developers, don’t have direct access to it. But when we publish a
new version of our app, we may need to update the database.
If the local database version is less than specified in open , then a special event
upgradeneeded is triggered, and we can compare versions and upgrade data
structures as needed.
The event also triggers when the database did not exist yet, so we can perform
initialization.
When we first publish our app, we open it with version 1 and perform the
initialization in upgradeneeded handler:
openRequest.onupgradeneeded = function() {
// triggers if the client had no database
// ...perform initialization...
};
openRequest.onerror = function() {
console.error("Error", openRequest.error);
};
openRequest.onsuccess = function() {
let db = openRequest.result;
// continue to work with database using db object
};
openRequest.onupgradeneeded = function() {
// the existing database version is less than 2 (or it doesn't exist)
let db = openRequest.result;
switch(db.version) { // existing db version
case 0:
// version 0 means that the client had no database
// perform initialization
case 1:
// client had version 1
// update
}
};
So, in openRequest.onupgradeneeded we update the database. Soon we’ll
see how it’s done. And then, only if its handler finishes without errors,
openRequest.onsuccess triggers.
To delete a database:
Such thing may happen if the visitor loaded an outdated code, e.g. from a proxy
cache. We should check db.version , suggest him to reload the page. And
also re-check our caching headers to ensure that the visitor never gets old code.
The problem is that a database is shared between two tabs, as that’s the same site,
same origin. And it can’t be both version 1 and 2. To perform the update to version 2,
all connections to version 1 must be closed.
In order to organize that, the versionchange event triggers an open database
object when a parallel upgrade is attempted. We should listen to it, so that we should
close the database (and probably suggest the visitor to reload the page, to load the
updated code).
If we don’t close it, then the second, new connection will be blocked with blocked
event instead of success .
openRequest.onupgradeneeded = ...;
openRequest.onerror = ...;
openRequest.onsuccess = function() {
let db = openRequest.result;
db.onversionchange = function() {
db.close();
alert("Database is outdated, please reload the page.")
};
openRequest.onblocked = function() {
// there's another open connection to same database
// and it wasn't closed after db.onversionchange triggered for them
};
There are other variants. For example, we can take time to close things gracefully in
db.onversionchange , prompt the visitor to save the data before the connection
is closed. The new updating connection will be blocked immediatelly after
db.onversionchange finished without closing, and we can ask the visitor in the
new tab to close other tabs for the update.
Such update collision happens rarely, but we should at least have some handling for
it, e.g. onblocked handler, so that our script doesn’t surprise the user by dying
silently.
Object store
A key must have a type one of: number, date, string, binary, or array. It’s an unique
identifier: we can search/remove/update values by the key.
As we’ll see very soon, we can provide a key when we add a value to the store,
similar to localStorage . But when we store objects, IndexedDB allows to setup
an object property as the key, that’s much more convenient. Or we can auto-
generate keys.
But we need to create an object store first.
The syntax to create an object store:
db.createObjectStore(name[, keyOptions]);
db.deleteObjectStore('books')
Transactions
The term “transaction” is generic, used in many kinds of databases.
A transaction is a group operations, that should either all succeed or all fail.
For instance, when a person buys something, we need:
1. Subtract the money from their account.
2. Add the item to their inventory.
It would be pretty bad if we complete the 1st operation, and then something goes
wrong, e.g. lights out, and we fail to do the 2nd. Both should either succeed
(purchase complete, good!) or both fail (at least the person kept their money, so they
can retry).
Transactions can guarantee that.
db.transaction(store[, type]);
● store is a store name that the transaction is going to access, e.g. "books" .
Can be an array of store names if we’re going to access multiple stores.
● type – a transaction type, one of:
●
readonly – can only read, the default.
● readwrite – can only read and write the data, but not create/remove/alter
object stores.
Many readonly transactions are able to access concurrently the same store,
but readwrite transactions can’t. A readwrite transaction “locks” the store
for writing. The next transaction must wait before the previous one finishes
before accessing the same store.
After the transaction is created, we can add an item to the store, like this:
let transaction = db.transaction("books", "readwrite"); // (1)
let book = {
id: 'js',
price: 10,
created: new Date()
};
request.onerror = function() {
console.log("Error", request.error);
};
Transactions’ autocommit
In the example above we started the transaction and made add request. But as we
stated previously, a transaction may have multiple associated requests, that must
either all success or all fail. How do we mark the transaction as finished, no more
requests to come?
The short answer is: we don’t.
In the next version 3.0 of the specification, there will probably be a manual way to
finish the transaction, but right now in 2.0 there isn’t.
When all transaction requests are finished, and the microtasks queue is empty,
it is committed automatically.
Usually, we can assume that a transaction commits when all its requests are
complete, and the current code finishes.
So, in the example above no special call is needed to finish the transaction.
Transactions auto-commit principle has an important side effect. We can’t insert an
async operation like fetch , setTimeout in the middle of transaction. IndexedDB
will not keep the transaction waiting till these are done.
In the code below request2 in line (*) fails, because the transaction is already
committed, can’t make any request in it:
request1.onsuccess = function() {
fetch('/').then(response => {
let request2 = books.add(anotherBook); // (*)
request2.onerror = function() {
console.log(request2.error.name); // TransactionInactiveError
};
});
};
But it will be even better, if we’d like to keep the operations together, in one
transaction, to split apart IndexedDB transactions and “other” async stuff.
First, make fetch , prepare the data if needed, afterwards create a transaction and
perform all the database requests, it’ll work then.
To detect the moment of successful completion, we can listen to
transaction.oncomplete event:
// ...perform operations...
transaction.oncomplete = function() {
console.log("Transaction is complete");
};
transaction.abort();
Error handling
request.onerror = function(event) {
// ConstraintError occurs when an object with the same id already exists
if (request.error.name == "ConstraintError") {
console.log("Book with such id already exists"); // handle the error
event.preventDefault(); // don't abort the transaction
// use another key for the book?
} else {
// unexpected error, can't handle it
// the transaction will abort
}
};
transaction.onabort = function() {
console.log("Error", transaction.error);
};
Event delegation
Do we need onerror/onsuccess for every request? Not every time. We can use event
delegation instead.
IndexedDB events bubble: request → transaction → database .
All events are DOM events, with capturing and bubbling, but usually only bubbling
stage is used.
So we can catch all errors using db.onerror handler, for reporting or other
purposes:
db.onerror = function(event) {
let request = event.target; // the request that caused the error
console.log("Error", request.error);
};
…But what if an error is fully handled? We don’t want to report it in that case.
We can stop the bubbling and hence db.onerror by using
event.stopPropagation() in request.onerror .
request.onerror = function(event) {
if (request.error.name == "ConstraintError") {
console.log("Book with such id already exists"); // handle the error
event.preventDefault(); // don't abort the transaction
event.stopPropagation(); // don't bubble error up, "chew" it
} else {
// do nothing
// transaction will be aborted
// we can take care of error in transaction.onabort
}
};
Searching by keys
First let’s deal with the keys and key ranges (1) .
Methods that involve searching support either exact keys or so-called “range
queries” – IDBKeyRange objects that specify a “key range”.
All searching methods accept a query argument that can be either an exact key or
a key range:
●
store.get(query) – search for the first value by a key or a range.
●
store.getAll([query], [count]) – search for all values, limit by count
if given.
● store.getKey(query) – search for the first key that satisfies the query,
usually a range.
●
store.getAllKeys([query], [count]) – search for all keys that satisfy
the query, usually a range, up to count if given.
●
store.count([query]) – get the total count of keys that satisfy the query,
usually a range.
For instance, we have a lot of books in our store. Remember, the id field is the key,
so all these methods can search by id .
Request examples:
●
name – index name,
●
keyPath – path to the object field that the index should track (we’re going to
search by that field),
● option – an optional object with properties:
● unique – if true, then there may be only one object in the store with the given
value at the keyPath . The index will enforce that by generating an error if we
try to add a duplicate.
●
multiEntry – only used if the value on keyPath is an array. In that case,
by default, the index will treat the whole array as the key. But if multiEntry
is true, then the index will keep a list of store objects for each value in that
array. So array members become index keys.
openRequest.onupgradeneeded = function() {
// we must create the index here, in versionchange transaction
let books = db.createObjectStore('books', {keyPath: 'id'});
let index = inventory.createIndex('price_idx', 'price');
};
Imagine that our inventory has 4 books. Here’s the picture that shows exactly
what the index is:
As said, the index for each value of price (second argument) keeps the list of keys
that have that price.
The index keeps itself up to date automatically, we don’t have to care about it.
Now, when we want to search for a given price, we simply apply the same search
methods to the index:
request.onsuccess = function() {
if (request.result !== undefined) {
console.log("Books", request.result); // array of books with price=10
} else {
console.log("No such books");
}
};
We can also use IDBKeyRange to create ranges and looks for cheap/expensive
books:
Indexes are internally sorted by the tracked object field, price in our case. So
when we do the search, the results are also sorted by price .
The delete method looks up values to delete by a query, the call format is similar
to getAll :
● delete(query) – delete matching values by query.
For instance:
request.onsuccess = function() {
let id = request.result;
let deleteRequest = books.delete(id);
};
To delete everything:
Cursors
But an object storage can be huge, bigger than the available memory. Then
getAll will fail to get all records as an array.
What to do?
Cursors provide the means to work around that.
A cursor is a special object that traverses the object storage, given a query,
and returns one key/value at a time, thus saving memory.
As an object store is sorted internally by key, a cursor walks the store in key order
(ascending by default).
The syntax:
●
query is a key or a key range, same as for getAll .
● direction is an optional argument, which order to use:
●
"next" – the default, the cursor walks up from the record with the lowest key.
● "prev" – the reverse order: down from the record with the biggest key.
● "nextunique" , "prevunique" – same as above, but skip records with
the same key (only for cursors over indexes, e.g. for multiple books with
price=5 only the first one will be returned).
Whether there are more values matching the cursor or not – onsuccess gets
called, and then in result we can get the cursor pointing to the next record, or
undefined .
In the example above the cursor was made for the object store.
But we also can make a cursor over an index. As we remember, indexes allow to
search by an object field. Cursors over indexes to precisely the same as over object
stores – they save memory by returning one value at a time.
For cursors over indexes, cursor.key is the index key (e.g. price), and we should
use cursor.primaryKey property the object key:
Promise wrapper
try {
await books.add(...);
await books.add(...);
await transaction.complete;
console.log('jsbook saved');
} catch(err) {
console.log('error', err.message);
}
So we have all the sweet “plain async code” and “try…catch” stuff.
Error handling
If we don’t catch an error, then it falls through, till the closest outer try..catch .
await inventory.add({ id: 'js', price: 10, created: new Date() });
await inventory.add({ id: 'js', price: 10, created: new Date() }); // Error
The next inventory.add after fetch (*) fails with an “inactive transaction”
error, because the transaction is already committed and closed at that time.
The workaround is same as when working with native IndexedDB: either make a
new transaction or just split things apart.
1. Prepare the data and fetch all that’s needed first.
2. Then save in the database.
In few rare cases, when we need the original request object, we can access it as
promise.request property of the promise:
let promise = books.add(book); // get a promise (don't await for its result)
Summary
Animation
CSS and JavaScript animations.
Bezier curve
Bezier curves are used in computer graphics to draw shapes, for CSS animation and
in many other places.
They are a very simple thing, worth to study once and then feel comfortable in the
world of vector graphics and advanced animations.
Control points
3 4
1 2
As you can notice, the curve stretches along the tangential lines 1 → 2 and 3
→ 4.
After some practice it becomes obvious how to place points to get the needed curve.
And by connecting several curves we can get practically anything.
Here are some examples:
De Casteljau’s algorithm
There’s a mathematical formula for Bezier curves, but let’s cover it a bit later,
because De Casteljau’s algorithm it is identical to the mathematical definition and
visually shows how it is constructed.
First let’s see the 3-points example.
Here’s the demo, and the explanation follow.
Control points (1,2 and 3) can be moved by the mouse. Press the “play” button to run
it.
t:1
2
1 3
2. Build segments between control points 1 → 2 → 3. In the demo above they are
brown.
3. The parameter t moves from 0 to 1 . In the example above the step 0.05 is
used: the loop goes over 0, 0.05, 0.1, 0.15, ... 0.95, 1 .
For each of these values of t :
● On each brown segment we take a point located on the distance proportional to
t from its beginning. As there are two segments, we have two points.
For instance, for t=0 – both points will be at the beginning of segments, and
for t=0.25 – on the 25% of segment length from the beginning, for t=0.5 –
50%(the middle), for t=1 – in the end of segments.
● Connect the points. On the picture below the connecting segment is painted
blue.
4. Now in the blue segment take a point on the distance proportional to the same
value of t . That is, for t=0.25 (the left picture) we have a point at the end of
the left quarter of the segment, and for t=0.5 (the right picture) – in the middle
of the segment. On pictures above that point is red.
5. As t runs from 0 to 1 , every value of t adds a point to the curve. The set of
such points forms the Bezier curve. It’s red and parabolic on the pictures above.
That was a process for 3 points. But the same is for 4 points.
The demo for 4 points (points can be moved by a mouse):
t:1
3 4
1 2
The algorithm is recursive and can be generalized for any number of control points.
Given N of control points:
1. We connect them to get initially N-1 segments.
2. Then for each t from 0 to 1 , we take a point on each segment on the distance
proportional to t and connect them. There will be N-2 segments.
3. Repeat step 2 until there is only one point.
t:1
4
3 2
1 4
t:1
3 2
1 4
As the algorithm is recursive, we can build Bezier curves of any order, that is: using
5, 6 or more control points. But in practice many points are less useful. Usually we
take 2-3 points, and for complex lines glue several curves together. That’s simpler to
develop and calculate.
How to draw a curve through given points?
To specify a Bezier curve, control points are used. As we can see, they are not
on the curve, except the first and the last ones.
Sometimes we have another task: to draw a curve through several points, so that
all of them are on a single smooth curve. That task is called interpolation , and
here we don’t cover it.
There are mathematical formulas for such curves, for instance Lagrange
polynomial . In computer graphics spline interpolation is often used to build
smooth curves that connect many points.
Maths
These are vector equations. In other words, we can put x and y instead of P to
get corresponding coordinates.
For instance, the 3-point curve is formed by points (x,y) calculated as:
●
x = (1−t)2x1 + 2(1−t)tx2 + t2x3
●
y = (1−t)2y1 + 2(1−t)ty2 + t2y3
Instead of x1, y1, x2, y2, x3, y3 we should put coordinates of 3 control
points, and then as t moves from 0 to 1 , for each value of t we’ll have (x,y)
of the curve.
For instance, if control points are (0,0) , (0.5, 1) and (1, 0) , the equations
become:
●
x = (1−t)2 * 0 + 2(1−t)t * 0.5 + t2 * 1 = (1-t)t + t2 = t
●
y = (1−t)2 * 0 + 2(1−t)t * 1 + t2 * 0 = 2(1-t)t = –t2 + 2t
Now as t runs from 0 to 1 , the set of values (x,y) for each t forms the curve
for such control points.
Summary
Usage:
●
In computer graphics, modeling, vector graphic editors. Fonts are described by
Bezier curves.
● In web development – for graphics on Canvas and in the SVG format. By the way,
“live” examples above are written in SVG. They are actually a single SVG
document that is given different points as parameters. You can open it in a
separate window and see the source: demo.svg.
● In CSS animation to describe the path and speed of animation.
CSS-animations
CSS animations allow to do simple animations without JavaScript at all.
JavaScript can be used to control CSS animation and make it even better with a little
of code.
CSS transitions
The idea of CSS transitions is simple. We describe a property and how its changes
should be animated. When the property changes, the browser paints the animation.
That is: all we need is to change the property. And the fluent transition is made by
the browser.
For instance, the CSS below animates changes of background-color for 3
seconds:
.animated {
transition-property: background-color;
transition-duration: 3s;
}
<style>
#color {
transition-property: background-color;
transition-duration: 3s;
}
</style>
<script>
color.onclick = function() {
this.style.backgroundColor = 'red';
};
</script>
Click me
We’ll cover them in a moment, for now let’s note that the common transition
property allows to declare them together in the order: property duration
timing-function delay , and also animate multiple properties at once.
<style>
#growing {
transition: font-size 3s, color 2s;
}
</style>
<script>
growing.onclick = function() {
this.style.fontSize = '36px';
this.style.color = 'red';
};
</script>
Click me
transition-property
Not all properties can be animated, but many of them . The value all means
“animate all properties”.
transition-duration
transition-delay
https://plnkr.co/edit/tRHA6fkSPUe9cjk35zPL?p=preview
#stripe.animate {
transform: translate(-90%);
transition-property: transform;
transition-duration: 9s;
}
In the example above JavaScript adds the class .animate to the element – and
the animation starts:
stripe.classList.add('animate');
We can also start it “from the middle”, from the exact number, e.g. corresponding to
the current second, using the negative transition-delay .
Here if you click the digit – it starts the animation from the current second:
https://plnkr.co/edit/zpqja4CejOmTApUXPxwE?p=preview
stripe.onclick = function() {
let sec = new Date().getSeconds() % 10;
// for instance, -3s here starts the animation from the 3rd second
stripe.style.transitionDelay = '-' + sec + 's';
stripe.classList.add('animate');
};
transition-timing-function
Timing function describes how the animation process is distributed along the time.
Will it start slowly and then go fast or vise versa.
That’s the most complicated property from the first sight. But it becomes very simple
if we devote a bit time to it.
That property accepts two kinds of values: a Bezier curve or steps. Let’s start from
the curve, as it’s used more often.
Bezier curve
The timing function can be set as a Bezier curve with 4 control points that satisfies
the conditions:
1. First control point: (0,0) .
2. Last control point: (1,1) .
3. For intermediate points values of x must be in the interval 0..1 , y can be
anything.
The syntax for a Bezier curve in CSS: cubic-bezier(x2, y2, x3, y3) . Here
we need to specify only 2nd and 3rd control points, because the 1st one is fixed to
(0,0) and the 4th one is (1,1) .
The timing function describes how fast the animation process goes in time.
● The x axis is the time: 0 – the starting moment, 1 – the last moment of
transition-duration .
● The y axis specifies the completion of the process: 0 – the starting value of the
property, 1 – the final value.
The simplest variant is when the animation goes uniformly, with the same linear
speed. That can be specified by the curve cubic-bezier(0, 0, 1, 1) .
…As we can see, it’s just a straight line. As the time ( x ) passes, the completion ( y )
of the animation steadily goes from 0 to 1 .
The train in the example below goes from left to right with the permanent speed
(click it):
https://plnkr.co/edit/BKXxsW1mgxIZvhcpUcj4?p=preview
.train {
left: 0;
transition: left 5s cubic-bezier(0, 0, 1, 1);
/* JavaScript sets left to 450px */
}
…And how can we show a train slowing down?
We can use another Bezier curve: cubic-bezier(0.0, 0.5, 0.5 ,1.0) .
The graph:
As we can see, the process starts fast: the curve soars up high, and then slower and
slower.
Here’s the timing function in action (click the train):
https://plnkr.co/edit/EBBtkTnI1l5096SHLcq8?p=preview
CSS:
.train {
left: 0;
transition: left 5s cubic-bezier(0, .5, .5, 1);
/* JavaScript sets left to 450px */
}
There are several built-in curves: linear , ease , ease-in , ease-out and
ease-in-out .
.train {
left: 0;
transition: left 5s ease-out;
/* transition: left 5s cubic-bezier(0, .5, .5, 1); */
}
.train {
left: 100px;
transition: left 5s cubic-bezier(.5, -1, .5, 2);
/* JavaScript sets left to 400px */
}
https://plnkr.co/edit/TjXgcacdDDsFyYHb4Lnl?p=preview
Why it happens – pretty obvious if we look at the graph of the given Bezier curve:
We moved the y coordinate of the 2nd point below zero, and for the 3rd point we
made put it over 1 , so the curve goes out of the “regular” quadrant. The y is out of
the “standard” range 0..1 .
That’s a “soft” variant for sure. If we put y values like -99 and 99 then the train
would jump out of the range much more.
But how to make the Bezier curve for a specific task? There are many tools. For
instance, we can do it on the site http://cubic-bezier.com/ .
Steps
Timing function steps(number of steps[, start/end]) allows to split
animation into steps.
Let’s see that in an example with digits.
Here’s a list of digits, without any animations, just as a source:
https://plnkr.co/edit/iyY2pj0vD8CcbFCuqnsI?p=preview
We’ll make the digits appear in a discrete way by making the part of the list outside
of the red “window” invisible and shifting the list to the left with each step.
There will be 9 steps, a step-move for each digit:
#stripe.animate {
transform: translate(-90%);
transition: transform 9s steps(9, start);
}
In action:
https://plnkr.co/edit/6VBxPjYIojjUL5vX8UvS?p=preview
The first argument of steps(9, start) is the number of steps. The transform
will be split into 9 parts (10% each). The time interval is automatically divided into 9
parts as well, so transition: 9s gives us 9 seconds for the whole animation – 1
second per digit.
The second argument is one of two words: start or end .
The start means that in the beginning of animation we need to do make the first
step immediately.
We can observe that during the animation: when we click on the digit it changes to
1 (the first step) immediately, and then changes in the beginning of the next second.
The alternative value end would mean that the change should be applied not in the
beginning, but at the end of each second.
So the process would go like this:
●
0s – 0
● 1s – -10% (first change at the end of the 1st second)
● 2s – -20%
● …
●
9s – -90%
Here’s step(9, end) in action (note the pause between the first digit change):
https://plnkr.co/edit/I3SoddMNBYDKxH2HeFak?p=preview
Event transitionend
It is widely used to do an action after the animation is done. Also we can join
animations.
For instance, the ship in the example below starts to swim there and back on click,
each time farther and farther to the right:
The animation is initiated by the function go that re-runs each time when the
transition finishes and flips the direction:
boat.onclick = function() {
//...
let times = 1;
function go() {
if (times % 2) {
// swim to the right
boat.classList.remove('back');
boat.style.marginLeft = 100 * times + 200 + 'px';
} else {
// swim to the left
boat.classList.add('back');
boat.style.marginLeft = 100 * times - 200 + 'px';
}
go();
boat.addEventListener('transitionend', function() {
times++;
go();
});
};
event.propertyName
The property that has finished animating. Can be good if we animate multiple
properties simultaneously.
event.elapsedTime
The time (in seconds) that the animation took, without transition-delay .
Keyframes
We can join multiple simple animations together using the @keyframes CSS rule.
It specifies the “name” of the animation and rules: what, when and where to animate.
Then using the animation property we attach the animation to the element and
specify additional parameters for it.
Here’s an example with explanations:
<div class="progress"></div>
<style>
@keyframes go-left-right { /* give it a name: "go-left-right" */
from { left: 0px; } /* animate from left: 0px */
to { left: calc(100% - 50px); } /* animate to left: 100%-50px */
}
.progress {
animation: go-left-right 3s infinite alternate;
/* apply the animation "go-left-right" to the element
duration 3 seconds
number of times: infinite
alternate direction every time
*/
position: relative;
border: 2px solid green;
width: 50px;
height: 20px;
background: lime;
}
</style>
Probably you won’t need @keyframes often, unless everything is in the constant
move on your sites.
Summary
CSS animations allow to smoothly (or not) animate changes of one or multiple CSS
properties.
They are good for most animation tasks. We’re also able to use JavaScript for
animations, the next chapter is devoted to that.
Limitations of CSS animations compared to JavaScript animations:
Merits Demerits
●
Simple things done simply. ●
JavaScript animations are
● Fast and lightweight for CPU. flexible. They can implement
any animation logic, like an
“explosion” of an element.
● Not just property changes. We
can create new elements in
JavaScript for purposes of
animation.
But in the next chapter we’ll do some JavaScript animations to cover more complex
cases.
✔ Tasks
Show the animation like on the picture below (click the plane):
● The picture grows on click from 40x24px to 400x240px (10 times larger).
● The animation takes 3 seconds.
● At the end output: “Done!”.
● During the animation process, there may be more clicks on the plane. They
shouldn’t “break” anything.
To solution
Modify the solution of the previous task Animate a plane (CSS) to make the plane
grow more than it’s original size 400x240px (jump out), and then return to that size.
To solution
Animated circle
importance: 5
The source document has an example of a circle with right styles, so the task is
precisely to do the animation right.
Open a sandbox for the task.
To solution
JavaScript animations
JavaScript animations can handle things that CSS can’t.
For instance, moving along a complex path, with a timing function different from
Bezier curves, or an animation on a canvas.
Using setInterval
For instance, changing style.left from 0px to 100px moves the element.
And if we increase it in setInterval , changing by 2px with a tiny delay, like 50
times per second, then it looks smooth. That’s the same principle as in the cinema:
24 frames per second is enough to make it look smooth.
}, 20);
// as timePassed goes from 0 to 2000
// left gets values from 0px to 400px
function draw(timePassed) {
train.style.left = timePassed / 5 + 'px';
}
Using requestAnimationFrame
That’s because they have different starting time, so “every 20ms” differs between
different animations. The intervals are not aligned. So we’ll have several
independent runs within 20ms .
setInterval(function() {
animate1();
animate2();
animate3();
}, 20)
These several independent redraws should be grouped together, to make the redraw
easier for the browser and hence load less CPU load and look smoother.
There’s one more thing to keep in mind. Sometimes when CPU is overloaded, or
there are other reasons to redraw less often (like when the browser tab is hidden), so
we really shouldn’t run it every 20ms .
That schedules the callback function to run in the closest time when the browser
wants to do animation.
If we do changes in elements in callback then they will be grouped together with
other requestAnimationFrame callbacks and with CSS animations. So there
will be one geometry recalculation and repaint instead of many.
The callback gets one argument – the time passed from the beginning of the
page load in microseconds. This time can also be obtained by calling
performance.now() .
Usually callback runs very soon, unless the CPU is overloaded or the laptop
battery is almost discharged, or there’s another reason.
The code below shows the time between first 10 runs for
requestAnimationFrame . Usually it’s 10-20ms:
<script>
let prev = performance.now();
let times = 0;
requestAnimationFrame(function measure(time) {
document.body.insertAdjacentHTML("beforeEnd", Math.floor(time - prev) + " ");
prev = time;
Structured animation
requestAnimationFrame(function animate(time) {
// timeFraction goes from 0 to 1
let timeFraction = (time - start) / duration;
if (timeFraction > 1) timeFraction = 1;
draw(progress); // draw it
if (timeFraction < 1) {
requestAnimationFrame(animate);
}
});
}
duration
Total time of animation. Like, 1000 .
timing(timeFraction)
Timing function, like CSS-property transition-timing-function that gets the
fraction of time that passed ( 0 at start, 1 at the end) and returns the animation
completion (like y on the Bezier curve).
For instance, a linear function means that the animation goes on uniformly with the
same speed:
function linear(timeFraction) {
return timeFraction;
}
It’s graph:
draw(progress)
The function that takes the animation completion state and draws it. The value
progress=0 denotes the beginning animation state, and progress=1 – the end
state.
function draw(progress) {
train.style.left = progress + 'px';
}
Let’s animate the element width from 0 to 100% using our function.
https://plnkr.co/edit/5l241DCQmzPNofVr2wfk?p=preview
animate({
duration: 1000,
timing(timeFraction) {
return timeFraction;
},
draw(progress) {
elem.style.width = progress * 100 + '%';
}
});
Unlike CSS animation, we can make any timing function and any drawing function
here. The timing function is not limited by Bezier curves. And draw can go beyond
properties, create new elements for like fireworks animation or something.
Timing functions
Power of n
If we want to speed up the animation, we can use progress in the power n .
function quad(timeFraction) {
return Math.pow(timeFraction, 2)
}
The graph:
…Or the cubic curve or event greater n . Increasing the power makes it speed up
faster.
Here’s the graph for progress in the power 5 :
In action:
The arc
Function:
function circ(timeFraction) {
return 1 - Math.sin(Math.acos(timeFraction));
}
The graph:
The code:
Bounce
Imagine we are dropping a ball. It falls down, then bounces back a few times and
stops.
The bounce function does the same, but in the reverse order: “bouncing” starts
immediately. It uses few special coefficients for that:
function bounce(timeFraction) {
for (let a = 0, b = 1, result; 1; a += b, b /= 2) {
if (timeFraction >= (7 - 4 * a) / 11) {
return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2)
}
}
}
In action:
Elastic animation
One more “elastic” function that accepts an additional parameter x for the “initial
range”.
Reversal: ease*
Sometimes we need to show the animation in the reverse order. That’s done with the
“easeOut” transform.
easeOut
In the “easeOut” mode the timing function is put into a wrapper
timingEaseOut :
Then the bounce will be not in the beginning, but at the end of the animation. Looks
even better:
https://plnkr.co/edit/opuSRjafyQ8Y41QXOolD?p=preview
Here we can see how the transform changes the behavior of the function:
If there’s an animation effect in the beginning, like bouncing – it will be shown at the
end.
In the graph above the regular bounce has the red color, and the easeOut bounce is
blue.
● Regular bounce – the object bounces at the bottom, then at the end sharply jumps
to the top.
● After easeOut – it first jumps to the top, then bounces there.
easeInOut
We also can show the effect both in the beginning and the end of the animation. The
transform is called “easeInOut”.
Given the timing function, we calculate the animation state like this:
bounceEaseInOut = makeEaseInOut(bounce);
In action, bounceEaseInOut :
https://plnkr.co/edit/F7NuLTRQblC8EgZr8ltV?p=preview
The “easeInOut” transform joins two graphs into one: easeIn (regular) for the first
half of the animation and easeOut (reversed) – for the second part.
The effect is clearly seen if we compare the graphs of easeIn , easeOut and
easeInOut of the circ timing function:
As we can see, the graph of the first half of the animation is the scaled down
easeIn , and the second half is the scaled down easeOut . As a result, the
animation starts and finishes with the same effect.
Instead of moving the element we can do something else. All we need is to write the
write the proper draw .
For animations that CSS can’t handle well, or those that need tight control,
JavaScript can help. JavaScript animations should be implemented via
requestAnimationFrame . That built-in method allows to setup a callback
function to run when the browser will be preparing a repaint. Usually that’s very
soon, but the exact time depends on the browser.
When a page is in the background, there are no repaints at all, so the callback won’t
run: the animation will be suspended and won’t consume resources. That’s great.
Here’s the helper animate function to setup most animations:
requestAnimationFrame(function animate(time) {
// timeFraction goes from 0 to 1
let timeFraction = (time - start) / duration;
if (timeFraction > 1) timeFraction = 1;
draw(progress); // draw it
if (timeFraction < 1) {
requestAnimationFrame(animate);
}
});
}
Options:
● duration – the total animation time in ms.
●
timing – the function to calculate animation progress. Gets a time fraction from
0 to 1, returns the animation progress, usually from 0 to 1.
● draw – the function to draw the animation.
Surely we could improve it, add more bells and whistles, but JavaScript animations
are not applied on a daily basis. They are used to do something interesting and non-
standard. So you’d want to add the features that you need when you need them.
JavaScript animations can use any timing function. We covered a lot of examples
and transformations to make them even more versatile. Unlike CSS, we are not
limited to Bezier curves here.
The same is about draw : we can animate anything, not just CSS properties.
✔ Tasks
To solution
To solution
Web components
Web components is a set of standards to make self-contained components: custom
HTML-elements with their own properties and methods, encapsulated DOM and
styles.
As of now, these standards are under development. Some features are well-
supported and integrated into the modern HTML/DOM standard, while others are yet
in draft stage. You can try examples in any browser, Google Chrome is probably the
most up to date with these features. Guess, that’s because Google fellows are
behind many of the related specifications.
The whole component idea is nothing new. It’s used in many frameworks and
elsewhere.
Before we move to implementation details, take a look at this great achievement of
humanity:
That’s the International Space Station (ISS).
And this is how it’s made inside (approximately):
The International Space Station:
●
Consists of many components.
● Each component, in its turn, has many smaller details inside.
● The components are very complex, much more complicated than most websites.
●
Components are developed internationally, by teams from different countries,
speaking different languages.
Component architecture
The well known rule for developing complex software is: don’t make complex
software.
If something becomes complex – split it into simpler parts and connect in the most
obvious way.
A good architect is the one who can make the complex simple.
We can split user interface into visual components: each of them has own place on
the page, can “do” a well-described task, and is separate from the others.
Let’s take a look at a website, for example Twitter.
1. Top navigation.
2. User info.
3. Follow suggestions.
4. Submit form.
5. (and also 6, 7) – messages.
How do we decide, what is a component? That comes from intuition, experience and
common sense. Usually it’s a separate visual entity that we can describe in terms of
what it does and how it interacts with the page. In the case above, the page has
blocks, each of them plays its own role, it’s logical to make these components.
A component has:
● its own JavaScript class.
● DOM structure, managed solely by its class, outside code doesn’t access it
(“encapsulation” principle).
● CSS styles, applied to the component.
● API: events, class methods etc, to interact with other components.
There exist many frameworks and development methodologies to build them, each
one with its own bells and whistles. Usually, special CSS classes and conventions
are used to provide “component feel” – CSS scoping and DOM encapsulation.
“Web components” provide built-in browser capabilities for that, so we don’t have to
emulate them any more.
●
Custom elements – to define custom HTML elements.
●
Shadow DOM – to create an internal DOM for the component, hidden from the
others.
● CSS Scoping – to declare styles that only apply inside the Shadow DOM of the
component.
● Event retargeting and other minor stuff to make custom components better fit
the development.
In the next chapter we’ll go into details of “Custom Elements” – the fundamental and
well-supported feature of web components, good on its own.
Custom elements
We can create custom HTML elements, described by our class, with its own
methods and properties, events and so on.
Once an custom element is defined, we can use it on par with built-in HTML
elements.
That’s great, as HTML dictionary is rich, but not infinite. There are no <easy-
tabs> , <sliding-carousel> , <beautiful-upload> … Just think of any
other tag we might need.
We can define them with a special class, and then use as if they were always a part
of HTML.
First we’ll create autonomous elements, and then customized built-in ones.
To create a custom element, we need to tell the browser several details about it: how
to show it, what to do when the element is added or removed to page, etc.
That’s done by making a class with special methods. That’s easy, as there are only
few methods, and all of them are optional.
connectedCallback() {
// browser calls it when the element is added to the document
// (can be called many times if an element is repeatedly added/removed)
}
disconnectedCallback() {
// browser calls it when the element is removed from the document
// (can be called many times if an element is repeatedly added/removed)
}
adoptedCallback() {
// called when the element is moved to a new document
// (happens in document.adoptNode, very rarely used)
}
// let the browser know that <my-element> is served by our new class
customElements.define("my-element", MyElement);
Now for any HTML elements with tag <my-element> , an instance of MyElement
is created, and the aforementioned methods are called. We also can
document.createElement('my-element') in JavaScript.
Custom element name must contain a hyphen -
Custom element name must have a hyphen - , e.g. my-element and super-
button are valid names, but myelement is not.
That’s to ensure that there are no name conflicts between built-in and custom
HTML elements.
Example: “time-formatted”
For example, there already exists <time> element in HTML, for date/time. But it
doesn’t do any formatting by itself.
Let’s create <time-formatted> element that displays the time in a nice,
language-aware format:
<script>
class TimeFormatted extends HTMLElement { // (1)
connectedCallback() {
let date = new Date(this.getAttribute('datetime') || Date.now());
The reason is simple: when constructor is called, it’s yet too early. The
element instance is created, but not populated yet. The browser did not yet
process/assign attributes at this stage: calls to getAttribute would return
null . So we can’t really render there.
Besides, if you think about it, that’s better performance-wise – to delay the work
until it’s really needed.
Observing attributes
<script>
class TimeFormatted extends HTMLElement {
render() { // (1)
let date = new Date(this.getAttribute('datetime') || Date.now());
connectedCallback() { // (2)
if (!this.rendered) {
this.render();
this.rendered = true;
}
}
customElements.define("time-formatted", TimeFormatted);
</script>
<script>
setInterval(() => elem.setAttribute('datetime', new Date()), 1000); // (5)
</script>
7:14:48 PM
Rendering order
When HTML parser builds the DOM, elements are processed one after another,
parents before children. E.g. if we have <outer><inner></inner></outer> ,
then <outer> element is created and connected to DOM first, and then <inner> .
connectedCallback() {
alert(this.innerHTML); // empty (*)
}
});
</script>
<user-info>John</user-info>
That’s exactly because there are no children on that stage, the DOM is unfinished.
HTML parser connected the custom element <user-info> , and will now proceed
to its children, but just didn’t yet.
If we’d like to pass information to custom element, we can use attributes. They are
available immediately.
Or, if we really need the children, we can defer access to them with zero-delay
setTimeout .
This works:
<script>
customElements.define('user-info', class extends HTMLElement {
connectedCallback() {
setTimeout(() => alert(this.innerHTML)); // John (*)
}
});
</script>
<user-info>John</user-info>
Now the alert in line (*) shows “John”, as we run it asynchronously, after the
HTML parsing is complete. We can process children if needed and finish the
initialization.
On the other hand, this solution is also not perfect. If nested custom elements also
use setTimeout to initialize themselves, then they queue up: the outer
setTimeout triggers first, and then the inner one.
So the outer element finishes the initialization before the inner one.
Let’s demonstrate that on example:
<script>
customElements.define('user-info', class extends HTMLElement {
connectedCallback() {
alert(`${this.id} connected.`);
setTimeout(() => alert(`${this.id} initialized.`));
}
});
</script>
<user-info id="outer">
<user-info id="inner"></user-info>
</user-info>
Output order:
1. outer connected.
2. inner connected.
3. outer initialized.
4. inner initialized.
We can clearly see that the outer element does not wait for the inner one.
There’s no built-in callback that triggers after nested elements are ready. But we can
implement such thing on our own. For instance, inner elements can dispatch events
like initialized , and outer ones can listen and react on them.
But such things can be important. E.g, a search engine would be interested to know
that we actually show a time. And if we’re making a special kind of button, why not
reuse the existing <button> functionality?
We can extend and customize built-in elements by inheriting from their classes.
For example, buttons are instances of HTMLButtonElement , let’s build upon it.
There exist different tags that share the same class, that’s why it’s needed.
3. At the end, to use our custom element, insert a regular <button> tag, but add
is="hello-button" to it:
<button is="hello-button">...</button>
<script>
// The button that says "hello" on click
class HelloButton extends HTMLButtonElement {
constructor() {
super();
this.addEventListener('click', () => alert("Hello!"));
}
}
Click me Disabled
Our new button extends the built-in one. So it keeps the same styles and standard
features like disabled attribute.
References
● HTML Living Standard: https://html.spec.whatwg.org/#custom-elements .
● Compatiblity: https://caniuse.com/#feat=custom-elements .
Summary
Definition scheme:
class MyElement extends HTMLElement {
constructor() { super(); /* ... */ }
connectedCallback() { /* ... */ }
disconnectedCallback() { /* ... */ }
static get observedAttributes() { return [/* ... */]; }
attributeChangedCallback(name, oldValue, newValue) { /* ... */ }
adoptedCallback() { /* ... */ }
}
customElements.define('my-element', MyElement);
/* <my-element> */
Custom elements are well-supported among browsers. Edge is a bit behind, but
there’s a polyfill https://github.com/webcomponents/webcomponentsjs .
✔ Tasks
Usage:
<live-timer id="elem"></live-timer>
<script>
elem.addEventListener('tick', event => console.log(event.detail));
</script>
Demo:
7:14:48 PM
To solution
Shadow DOM
Shadow DOM serves for encapsulation. It allows a component to have its very own
“shadow” DOM tree, that can’t be accidentally accessed from the main document,
may have local style rules, and more.
Did you ever think how complex browser controls are created and styled?
Such as <input type="range"> :
The browser uses DOM/CSS internally to draw them. That DOM structure is
normally hidden from us, but we can see it in developer tools. E.g. in Chrome, we
need to enable in Dev Tools “Show user agent shadow DOM” option.
We can’t get built-in shadow DOM elements by regular JavaScript calls or selectors.
These are not regular children, but a powerful encapsulation technique.
In the example above, we can see a useful attribute pseudo . It’s non-standard,
exists for historical reasons. We can use it style subelements with CSS, like this:
<style>
/* make the slider track red */
input::-webkit-slider-runnable-track {
background: red;
}
</style>
<input type="range">
Further on, we’ll use the modern shadow DOM standard, covered by DOM spec
other related specifications.
Shadow tree
1. Light tree – a regular DOM subtree, made of HTML children. All subtrees that
we’ve seen in previous chapters were “light”.
2. Shadow tree – a hidden DOM subtree, not reflected in HTML, hidden from prying
eyes.
If an element has both, then the browser renders only the shadow tree. But we can
setup a kind of composition between shadow and light trees as well. We’ll see the
details later in the chapter Shadow DOM slots, composition.
Shadow tree can be used in Custom Elements to hide component internals and
apply component-local styles.
For example, this <show-hello> element hides its internal DOM in shadow tree:
<script>
customElements.define('show-hello', class extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({mode: 'open'});
shadow.innerHTML = `<p>
Hello, ${this.getAttribute('name')}
</p>`;
}
});
</script>
<show-hello name="John"></show-hello>
Hello, John
That’s how the resulting DOM looks in Chrome dev tools, all the content is under
“#shadow-root”:
The mode option sets the encapsulation level. It must have any of two values:
● "open" – the shadow root is available as elem.shadowRoot .
The element with a shadow root is called a “shadow tree host”, and is available as
the shadow root host property:
Encapsulation
For example:
<style>
/* document style won't apply to the shadow tree inside #elem (1) */
p { color: red; }
</style>
<div id="elem"></div>
<script>
elem.attachShadow({mode: 'open'});
// shadow tree has its own style (2)
elem.shadowRoot.innerHTML = `
<style> p { font-weight: bold; } </style>
<p>Hello, John!</p>
`;
// <p> is only visible from queries inside the shadow tree (3)
alert(document.querySelectorAll('p').length); // 0
alert(elem.shadowRoot.querySelectorAll('p').length); // 1
</script>
1. The style from the document does not affect the shadow tree.
2. …But the style from the inside works.
3. To get elements in shadow tree, we must query from inside the tree.
References
● DOM: https://dom.spec.whatwg.org/#shadow-trees
● Compatibility: https://caniuse.com/#feat=shadowdomv1
Summary
Shadow DOM, if exists, is rendered by the browser instead of so-called “light DOM”
(regular children). In the chapter Shadow DOM slots, composition we’ll see how to
compose them.
Template element
A built-in <template> element serves as a storage for HTML markup templates.
The browser ignores it contents, only checks for syntax validity, but we can access
and use it in JavaScript, to create other elements.
In theory, we could create any invisible element somewhere in HTML for HTML
markup storage purposes. What’s special about <template> ?
First, its content can be any valid HTML, even if it normally requires a proper
enclosing tag.
<template>
<tr>
<td>Contents</td>
</tr>
</template>
Usually, if we try to put <tr> inside, say, a <div> , the browser detects the invalid
DOM structure and “fixes” it, adds <table> around. That’s not what we want. On
the other hand, <template> keeps exactly what we place there.
<template>
<style>
p { font-weight: bold; }
</style>
<script>
alert("Hello");
</script>
</template>
The browser considers <template> content “out of the document”: styles are not
applied, scripts are not executed, <video autoplay> is not run, etc.
The content becomes live (styles apply, scripts run etc) when we insert it into the
document.
Inserting template
We can treat it as any other DOM node, except one special property: when we insert
it somewhere, its children are inserted instead.
For example:
<template id="tmpl">
<script>
alert("Hello");
</script>
<div class="message">Hello, world!</div>
</template>
<script>
let elem = document.createElement('div');
document.body.append(elem);
// Now the script from <template> runs
</script>
Let’s rewrite a Shadow DOM example from the previous chapter using
<template> :
<template id="tmpl">
<style> p { font-weight: bold; } </style>
<p id="message"></p>
</template>
elem.shadowRoot.append(tmpl.content.cloneNode(true)); // (*)
Click me
<div id="elem">
#shadow-root
<style> p { font-weight: bold; } </style>
<p id="message"></p>
</div>
Summary
To summarize:
●
<template> content can be any syntactically correct HTML.
● <template> content is considered “out of the document”, so it doesn’t affect
anything.
● We can access template.content from JavaScript, clone it to reuse in a new
component.
The <template> element does not feature any iteration mechanisms, data binding
or variable substitutions, but we can implement those on top of it.
Shadow DOM slots, composition
Many types of components, such as tabs, menus, image galleries, and so on, need
the content to render.
Just like built-in browser <select> expects <option> items, our <custom-
tabs> may expect the actual tab content to be passed. And a <custom-menu>
may expect menu items.
The code that makes use of <custom-menu> can look like this:
<custom-menu>
<title>Candy menu</title>
<item>Lollipop</item>
<item>Fruit Toast</item>
<item>Cup Cake</item>
</custom-menu>
…Then our component should render it properly, as a nice menu with given title and
items, handle menu events, etc.
How to implement it?
We could try to analyze the element content and dynamically copy-rearrange DOM
nodes. That’s possible, but if we’re moving elements to shadow DOM, then CSS
styles from the document do not apply in there, so the visual styling may be lost.
Also that requires some coding.
Luckily, we don’t have to. Shadow DOM supports <slot> elements, that are
automatically filled by the content from light DOM.
Named slots
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<div>Name:
<slot name="username"></slot>
</div>
<div>Birthday:
<slot name="birthday"></slot>
</div>
`;
}
});
</script>
<user-card>
<span slot="username">John Smith</span>
<span slot="birthday">01.01.2001</span>
</user-card>
Then the browser performs “composition”: it takes elements from the light DOM and
renders them in corresponding slots of the shadow DOM. At the end, we have
exactly what we want – a generic component that can be filled with data.
Here’s the DOM structure after the script, not taking composition into account:
<user-card>
#shadow-root
<div>Name:
<slot name="username"></slot>
</div>
<div>Birthday:
<slot name="birthday"></slot>
</div>
<span slot="username">John Smith</span>
<span slot="birthday">01.01.2001</span>
</user-card>
There’s nothing odd here. We created the shadow DOM, so here it is. Now the
element has both light and shadow DOM.
For rendering purposes, for each <slot name="..."> in shadow DOM, the
browser looks for slot="..." with the same name in the light DOM. These
elements are rendered inside the slots:
The result is called “flattened” DOM:
<user-card>
#shadow-root
<div>Name:
<slot name="username">
<!-- slotted element is inserted into the slot as a whole -->
<span slot="username">John Smith</span>
</slot>
</div>
<div>Birthday:
<slot name="birthday">
<span slot="birthday">01.01.2001</span>
</slot>
</div>
</user-card>
…But the “flattened” DOM is only created for rendering and event-handling
purposes. That’s how things are shown. The nodes are actually not moved around!
That can be easily checked if we run querySelector : nodes are still at their
places.
// light DOM <span> nodes are still at the same place, under `<user-card>`
alert( document.querySelector('user-card span').length ); // 2
It may look bizarre, but for shadow DOM with slots we have one more “DOM level”,
the “flattened” DOM – result of slot insertion. The browser renders it and uses for
style inheritance, event propagation. But JavaScript still sees the document “as is”,
before flattening.
⚠ Only top-level children may have slot="…" attribute
The slot="..." attribute is only valid for direct children of the shadow host (in
our example, <user-card> element). For nested elements it’s ignored.
For example, the second <span> here is ignored (as it’s not a top-level child of
<user-card> ):
<user-card>
<span slot="username">John Smith</span>
<div>
<!-- bad slot, not top-level: -->
<span slot="birthday">01.01.2001</span>
</div>
</user-card>
If we put something inside a <slot> , it becomes the fallback content. The browser
shows it if there’s no corresponding filler in light DOM.
<div>Name:
<slot name="username">Anonymous</slot>
</div>
Default slot
The first <slot> in shadow DOM that doesn’t have a name is a “default” slot. It
gets all nodes from the light DOM that aren’t slotted elsewhere.
For example, let’s add the default slot to our <user-card> that collects any
unslotted information about the user:
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<div>Name:
<slot name="username"></slot>
</div>
<div>Birthday:
<slot name="birthday"></slot>
</div>
<fieldset>
<legend>Other information</legend>
<slot></slot>
</fieldset>
`;
}
});
</script>
<user-card>
<div>I like to swim.</div>
<span slot="username">John Smith</span>
<span slot="birthday">01.01.2001</span>
<div>...And play volleyball too!</div>
</user-card>
All the unslotted light DOM content gets into the “Other information” fieldset.
Elements are appended to a slot one after another, so both unslotted pieces of
information are in the default slot together.
<user-card>
#shadow-root
<div>Name:
<slot name="username">
<span slot="username">John Smith</span>
</slot>
</div>
<div>Birthday:
<slot name="birthday">
<span slot="birthday">01.01.2001</span>
</slot>
</div>
<fieldset>
<legend>About me</legend>
<slot>
<div>Hello</div>
<div>I am John!</div>
</slot>
</fieldset>
</user-card>
Menu example
<custom-menu>
<span slot="title">Candy menu</span>
<li slot="item">Lollipop</li>
<li slot="item">Fruit Toast</li>
<li slot="item">Cup Cake</li>
</custom-menu>
<template id="tmpl">
<style> /* menu styles */ </style>
<div class="menu">
<slot name="title"></slot>
<ul><slot name="item"></slot></ul>
</div>
</template>
<custom-menu>
#shadow-root
<style> /* menu styles */ </style>
<div class="menu">
<slot name="title">
<span slot="title">Candy menu</span>
</slot>
<ul>
<slot name="item">
<li slot="item">Lollipop</li>
<li slot="item">Fruit Toast</li>
<li slot="item">Cup Cake</li>
</slot>
</ul>
</div>
</custom-menu>
One might notice that, in a valid DOM, <li> must be a direct child of <ul> . But
that’s flattened DOM, it describes how the component is rendered, such thing
happens naturally here.
We just need to add a click handler to open/close the list, and the <custom-
menu> is ready:
// we can't select light DOM nodes, so let's handle clicks on the slot
this.shadowRoot.querySelector('slot[name="title"]').onclick = () => {
// open/close the menu
this.shadowRoot.querySelector('.menu').classList.toggle('closed');
};
}
});
Candy menu
Lollipop
Fruit Toast
Cup Cake
Of course, we can add more functionality to it: events, methods and so on.
Monitoring slots
For example, here the menu item is inserted dynamically after 1 second, and the title
changes after 2 seconds:
<custom-menu id="menu">
<span slot="title">Candy menu</span>
</custom-menu>
<script>
customElements.define('custom-menu', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<div class="menu">
<slot name="title"></slot>
<ul><slot name="item"></slot></ul>
</div>`;
setTimeout(() => {
menu.insertAdjacentHTML('beforeEnd', '<li slot="item">Lollipop</li>')
}, 1000);
setTimeout(() => {
menu.querySelector('[slot="title"]').innerHTML = "New menu";
}, 2000);
</script>
1. At initialization:
Please note: there’s no slotchange event after 2 seconds, when the content of
slot="title" is modified. That’s because there’s no slot change. We modify the
content inside the slotted element, that’s another thing.
If we’d like to track internal modifications of light DOM from JavaScript, that’s also
possible using a more generic mechanism: MutationObserver.
Slot API
As we’ve seen before, JavaScript looks at the “real” DOM, without flattening. But, if
the shadow tree has {mode: 'open'} , then we can figure out which elements
assigned to a slot and, vise-versa, the slot by the element inside it:
● node.assignedSlot – returns the <slot> element that the node is
assigned to.
● slot.assignedNodes({flatten: true/false}) – DOM nodes,
assigned to the slot. The flatten option is false by default. If explicitly set to
true , then it looks more deeply into the flattened DOM, returning nested slots in
case of nested components and the fallback content if no node assigned.
●
slot.assignedElements({flatten: true/false}) – DOM elements,
assigned to the slot (same as above, but only element nodes).
These methods are useful when we need not just show the slotted content, but also
track it in JavaScript.
<custom-menu id="menu">
<span slot="title">Candy menu</span>
<li slot="item">Lollipop</li>
<li slot="item">Fruit Toast</li>
</custom-menu>
<script>
customElements.define('custom-menu', class extends HTMLElement {
items = []
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<div class="menu">
<slot name="title"></slot>
<ul><slot name="item"></slot></ul>
</div>`;
// slottable is added/removed/replaced
this.shadowRoot.firstElementChild.addEventListener('slotchange', e => {
let slot = e.target;
if (slot.name == 'item') {
this.items = slot.assignedElements().map(elem => elem.textContent);
alert("Items: " + this.items);
}
});
}
});
Summary
The process of rendering slotted elements inside their slots is called “composition”.
The result is called a “flattened DOM”.
Composition does not really move nodes, from JavaScript point of view the DOM is
still same.
JavaScript can access slots using methods:
● slot.assignedNodes/Elements() – returns nodes/elements inside the
slot .
●
node.assignedSlot – the reverse meethod, returns slot by a node.
If we’d like to know what we’re showing, we can track slot contents using:
●
slotchange event – triggers the first time a slot is filled, and on any
add/remove/replace operation of the slotted element, but not its children. The slot
is event.target .
● MutationObserver to go deeper into slot content, watch changes inside it.
Now, as we have elements from light DOM in the shadow DOM, let’s see how to
style them properly. The basic rule is that shadow elements are styled inside, and
light elements – outside, but there are notable exceptions.
We’ll see the details in the next chapter.
As a general rule, local styles work only inside the shadow tree, and document styles
work outside of it. But there are few exceptions.
:host
The :host selector allows to select the shadow host (the element containing the
shadow tree).
For instance, we’re making <custom-dialog> element that should be centered.
For that we need to style the <custom-dialog> element itself.
<template id="tmpl">
<style>
/* the style will be applied from inside to the custom-dialog element */
:host {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: inline-block;
border: 1px solid red;
padding: 10px;
}
</style>
<slot></slot>
</template>
<script>
customElements.define('custom-dialog', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true));
}
});
</script>
<custom-dialog>
Hello!
</custom-dialog>
Hello!
Cascading
The shadow host ( <custom-dialog> itself) resides in the light DOM, so it’s
affected by the main CSS cascade.
If there’s a property styled both in :host locally, and in the document, then the
document style takes precedence.
For instance, if in the document we had:
<style>
custom-dialog {
padding: 0;
}
</style>
It’s very convenient, as we can setup “default” styles in the component :host rule,
and then easily override them in the document.
:host(selector)
Same as :host , but applied only if the shadow host matches the selector .
For example, we’d like to center the <custom-dialog> only if it has centered
attribute:
<template id="tmpl">
<style>
:host([centered]) {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
:host {
display: inline-block;
border: 1px solid red;
padding: 10px;
}
</style>
<slot></slot>
</template>
<script>
customElements.define('custom-dialog', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true));
}
});
</script>
<custom-dialog centered>
Centered!
</custom-dialog>
<custom-dialog>
Not centered.
</custom-dialog>
Not centered.
Centered!
Now the additional centering styles are only applied to the first dialog <custom-
dialog centered> .
:host-context(selector)
Same as :host , but applied only if the shadow host or any of its ancestors in the
outer document matches the selector .
To summarize, we can use :host -family of selectors to style the main element of
the component, depending on the context. These styles (unless !important ) can
be overridden by the document.
Slotted elements come from light DOM, so they use document styles. Local styles do
not affect slotted content.
In the example below, slotted <span> is bold, as per document style, but does not
take background from the local style:
<style>
span { font-weight: bold }
</style>
<user-card>
<div slot="username"><span>John Smith</span></div>
</user-card>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<style>
span { background: red; }
</style>
Name: <slot name="username"></slot>
`;
}
});
</script>
Name:
John Smith
The result is bold, but not red.
If we’d like to style slotted elements in our component, there are two choices.
First, we can style the <slot> itself and rely on CSS inheritance:
<user-card>
<div slot="username"><span>John Smith</span></div>
</user-card>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<style>
slot[name="username"] { font-weight: bold; }
</style>
Name: <slot name="username"></slot>
`;
}
});
</script>
Name:
John Smith
<user-card>
<div slot="username">
<div>John Smith</div>
</div>
</user-card>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<style>
::slotted(div) { border: 1px solid red; }
</style>
Name: <slot name="username"></slot>
`;
}
});
</script>
Name:
John Smith
Please note, ::slotted selector can’t descend any further into the slot. These
selectors are invalid:
::slotted(div span) {
/* our slotted <div> does not match this */
}
::slotted(div) p {
/* can't go inside light DOM */
}
Then, we can declare this property in the outer document for <user-card> :
user-card {
--user-card-field-color: green;
}
Custom CSS properties pierce through shadow DOM, they are visible everywhere,
so the inner .field rule will make use of it.
<style>
user-card {
--user-card-field-color: green;
}
</style>
<template id="tmpl">
<style>
.field {
color: var(--user-card-field-color, black);
}
</style>
<div class="field">Name: <slot name="username"></slot></div>
<div class="field">Birthday: <slot name="birthday"></slot></div>
</template>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.append(document.getElementById('tmpl').content.cloneNode(true))
}
});
</script>
<user-card>
<span slot="username">John Smith</span>
<span slot="birthday">01.01.2001</span>
</user-card>
Summary
When CSS properties conflict, normally document styles have precedence, unless
the property is labelled as !important . Then local styles have precedence.
CSS custom properties pierce through shadow DOM. They are used as “hooks” to
style the component:
1. The component uses a custom CSS property to style key elements, such as
var(--component-name-title, <default value>) .
2. Component author publishes these properties for developers, they are same
important as other public component methods.
3. When a developer wants to style a title, they assign --component-name-
title CSS property for the shadow host or above.
4. Profit!
Events that happen in shadow DOM have the host element as the target, when
caught outside of the component.
<user-card></user-card>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<p>
<button>Click me</button>
</p>`;
this.shadowRoot.firstElementChild.onclick =
e => alert("Inner target: " + e.target.tagName);
}
});
document.onclick =
e => alert("Outer target: " + e.target.tagName);
</script>
Click me
Event retargeting is a great thing to have, because the outer document doesn’t have
no know about component internals. From its point of view, the event happened on
<user-card> .
Retargeting does not occur if the event occurs on a slotted element, that
physically lives in the light DOM.
<user-card id="userCard">
<span slot="username">John Smith</span>
</user-card>
<script>
customElements.define('user-card', class extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `<div>
<b>Name:</b> <slot name="username"></slot>
</div>`;
this.shadowRoot.firstElementChild.onclick =
e => alert("Inner target: " + e.target.tagName);
}
});
If a click happens on "John Smith" , for both inner and outer handlers the target
is <span slot="username"> . That’s an element from the light DOM, so no
retargeting.
On the other hand, if the click occurs on an element originating from shadow DOM,
e.g. on <b>Name</b> , then, as it bubbles out of the shadow DOM, its
event.target is reset to <user-card> .
Bubbling, event.composedPath()
The full path to the original event target, with all the shadow elements, can be
obtained using event.composedPath() . As we can see from the name of the
method, that path is taken after the composition.
<user-card id="userCard">
#shadow-root
<div>
<b>Name:</b>
<slot name="username">
<span slot="username">John Smith</span>
</slot>
</div>
</user-card>
If the shadow tree was created with {mode: 'closed'} , then the composed
path starts from the host: user-card and upwards.
That’s the similar principle as for other methods that work with shadow DOM.
Internals of closed trees are completely hidden.
event.composed
Most events successfully bubble through a shadow DOM boundary. There are few
events that do not.
This is governed by the composed event object property. If it’s true , then the
event does cross the boundary. Otherwise, it only can be caught from inside the
shadow DOM.
All touch events and pointer events also have composed: true .
Custom events
When we dispatch custom events, we need to set both bubbles and composed
properties to true for it to bubble up and out of the component.
For example, here we create div#inner in the shadow DOM of div#outer and
trigger two events on it. Only the one with composed: true makes it outside to
the document:
<div id="outer"></div>
<script>
outer.attachShadow({mode: 'open'});
/*
div(id=outer)
#shadow-dom
div(id=inner)
*/
inner.dispatchEvent(new CustomEvent('test', {
bubbles: true,
composed: true,
detail: "composed"
}));
inner.dispatchEvent(new CustomEvent('test', {
bubbles: true,
composed: false,
detail: "not composed"
}));
</script>
Summary
Events only cross shadow DOM boundaries if their composed flag is set to true .
These events can be caught only on elements within the same DOM.
If we dispatch a CustomEvent , then we should explicitly set composed: true .
Please note that in case of nested components, one shadow DOM may be nested
into another. In that case composed events bubble through all shadow DOM
boundaries. So, if an event is intended only for the immediate enclosing component,
we can also dispatch it on the shadow host and set composed: false . Then it’s
out of the component shadow DOM, but won’t bubble up to higher-level DOM.
Regular expressions
Regular expressions is a powerful way of doing search and replace in strings.
In JavaScript regular expressions are implemented using objects of a built-in
RegExp class and integrated with strings.
Please note that regular expressions vary between programming languages. In this
tutorial we concentrate on JavaScript. Of course there’s a lot in common, but they
are a somewhat different in Perl, Ruby, PHP etc.
Slashes "/" tell JavaScript that we are creating a regular expression. They play the
same role as quotes for strings.
Usage
Here’s an example:
The str.search method looks for the pattern /love/ and returns the position
inside the string. As we might guess, /love/ is the simplest possible pattern. What
it does is a simple substring search.
The code above is the same as:
But that’s only for now. Soon we’ll create more complex regular expressions with
much more searching power.
Colors
From here on the color scheme is:
● regexp – red
●
string (where we search) – blue
●
result – green
When to use new RegExp ?
Normally we use the short syntax /.../ . But it does not support variable
insertions ${...} .
On the other hand, new RegExp allows to construct a pattern dynamically from
a string, so it’s more flexible.
Here’s an example of a dynamically generated regexp:
Flags
i
With this flag the search is case-insensitive: no difference between A and a (see
the example below).
g
With this flag the search looks for all matches, without it – only the first one (we’ll see
uses in the next chapter).
m
Multiline mode (covered in the chapter Multiline mode, flag "m").
s
“Dotall” mode, allows . to match newlines (covered in the chapter Character
classes).
u
Enables full unicode support. The flag enables correct processing of surrogate pairs.
More about that in the chapter Unicode: flag "u".
y
Sticky mode (covered in the chapter Sticky flag "y", searching at position)
So the i flag already makes regular expressions more powerful than a simple
substring search. But there’s so much more. We’ll cover other flags and features in
the next chapters.
Summary
● A regular expression consists of a pattern and optional flags: g , i , m , u , s ,
y.
● Without flags and special symbols that we’ll study later, the search by a regexp is
the same as a substring search.
●
The method str.search(regexp) returns the index where the match is found
or -1 if there’s no match. In the next chapter we’ll see other methods.
Recipes
Methods become much easier to understand if we separate them by their use in real-
life tasks.
So, here are general recipes, the details to follow:
Now you can continue reading this chapter to get the details about every method…
But if you’re reading for the first time, then you probably want to know more about
regexps. So you can move to the next chapter, and then return here if something
about a method is unclear.
str.search(reg)
We’ve seen this method already. It returns the position of the first match or -1 if
none found:
We can’t find next matches using search , there’s just no syntax for that. But there
are other methods that can.
The behavior of str.match varies depending on whether reg has g flag or not.
First, if there’s no g flag, then str.match(reg) looks for the first match only.
The result is an array with that match and additional properties:
● index – the position of the match inside the string,
● input – the subject string.
For instance:
For instance:
Due to the i flag the search is case-insensitive, so it finds JavaScript . The part
of the match that corresponds to SCRIPT becomes a separate array item.
So, this method is used to find one full match with all details.
When there’s a "g" flag, then str.match returns an array of all matches. There
are no additional properties in that array, and parentheses do not create any
elements.
For instance:
So, with g flag str.match returns a simple array of all matches, without
details.
If we want to get information about match positions and contents of parentheses
then we should use matchAll method that we’ll cover below.
Please note, that’s important. If there are no matches, the result is not an empty
array, but null .
str.matchAll(regexp)
The method str.matchAll(regexp) is used to find all matches with all details.
For instance:
In practice, if we need all matches, then for..of works, so it’s not a problem.
alert(firstMatch); // Javascript
str.split(regexp|substr, limit)
Splits the string using the regexp (or a substring) as a delimiter.
We already used split with strings, like this:
str.replace(str|reg, str|func)
This is a generic method for searching and replacing, one of most useful ones. The
swiss army knife for searching and replacing.
We can use it without regexps, to search and replace a substring:
When the first argument of replace is a string, it only looks for the first
match.
You can see that in the example above: only the first "-" is replaced by ":" .
To find all dashes, we need to use not the string "-" , but a regexp /-/g , with an
obligatory g flag:
The second argument is a replacement string. We can use special characters in it:
Symbol Inserts
$$ "$"
if n is a 1-2 digit number, then it means the contents of n-th parentheses counting from left to right,
$n
otherwise it means a parentheses with the given name
For instance if we use $& in the replacement string, that means “put the whole
match here”.
Let’s use it to prepend all entries of "John" with "Mr." :
Quite often we’d like to reuse parts of the source string, recombine them in the
replacement or wrap into something.
To do so, we should:
For instance:
For situations that require “smart” replacements, the second argument can be
a function.
It will be called for each match, and its result will be inserted as a replacement.
For instance:
let i = 0;
The function is called with arguments func(str, p1, p2, ..., pn,
offset, input, groups) :
If there are no parentheses in the regexp, then there are only 3 arguments:
func(str, offset, input) .
In the example below there are two parentheses, so replacer is called with 5
arguments: str is the full match, then parentheses, and then offset and
input :
regexp.exec(str)
The regexp.exec method is the most flexible searching method of all. Unlike
previous methods, exec should be called on a regexp, rather than on a string.
We could use it to get all matches with their positions and parentheses groups in a
loop, instead of matchAll :
let result;
let result;
Now, starting from the given position 13 , there’s only one match.
regexp.test(str)
For instance, here we call regexp.test twice on the same text, and the
second time fails:
Summary
Their abilities and methods overlap quite a bit, we can do the same by different calls.
Sometimes that may cause confusion when starting to learn the language.
Then please refer to the recipes at the beginning of this chapter, as they provide
solutions for the majority of regexp-related tasks.
Character classes
Consider a practical task – we have a phone number "+7(903)-123-45-67" ,
and we need to turn it into pure numbers: 79035419441 .
To do so, we can find and remove anything that’s not a number. Character classes
can help with that.
A character class is a special notation that matches any symbol from a certain set.
For the start, let’s explore a “digit” class. It’s written as \d . We put it in the pattern,
that means “any single digit”.
For instance, the let’s find the first digit in the phone number:
alert( str.match(reg) ); // 7
Without the flag g , the regular expression only looks for the first match, that is the
first digit \d .
That was a character class for digits. There are other character classes as well.
For instance, CSS\d matches a string CSS with a digit after it:
let str = "CSS4 is cool";
let reg = /CSS\d/
Word boundary: \b
For instance, \bJava\b matches Java in the string Hello, Java! , but not in
the script Hello, JavaScript! .
The boundary has “zero width” in a sense that usually a character class means a
character in the result (like a wordly character or a digit), but not in this case.
When the pattern contains \b , it tests that the position in string is a word boundary,
that is one of three variants:
● Immediately before is \w , and immediately after – not \w , or vise versa.
●
At string start, and the first string character is \w .
●
At string end, and the last string character is \w .
For instance, in the string Hello, Java! the following positions match \b :
So it matches \bHello\b , because:
Pattern \bJava\b also matches. But not \bHell\b (because there’s no word
boundary after l ) and not Java!\b (because the exclamation sign is not a wordly
character, so there’s no word boundary after it).
Once again let’s note that \b makes the searching engine to test for the boundary,
so that Java\b finds Java only when followed by a word boundary, but it does not
add a letter to the result.
Later we’ll come by Unicode character classes that allow to solve the similar task
for different languages.
Inverse classes
For every character class there exists an “inverse class”, denoted with the same
letter, but uppercased.
The “reverse” means that it matches all other characters, for instance:
\D
Non-digit: any character except \d , for instance a letter.
\S
Non-space: any character except \s , for instance a letter.
\W
Non-wordly character: anything but \w .
\B
Non-boundary: a test reverse to \b .
In the beginning of the chapter we saw how to get all digits from the phone
+7(903)-123-45-67 .
An alternative, shorter way is to find non-digits \D and remove them from the string:
Usually we pay little attention to spaces. For us strings 1-5 and 1 - 5 are nearly
identical.
But if a regexp doesn’t take spaces into account, it may fail to work.
alert( "1-5".match(/\d - \d/) ); // null, because the string 1-5 has no spaces
The dot "." is a special character class that matches “any character except a
newline”.
For instance:
alert( "Z".match(/./) ); // Z
Please note that the dot means “any character”, but not the “absense of a character”.
There must be a character to match it:
For instance, A.B matches A , and then B with any character between them,
except a newline.
This doesn’t match:
Summary
The Unicode encoding, used by JavaScript for strings, provides many properties for
characters, like: which language the letter belongs to (if a letter) it is it a punctuation
sign, etc.
Modern JavaScript allows to use these properties in regexps to look for characters,
for instance:
● A cyrillic letter is: \p{Script=Cyrillic} or \p{sc=Cyrillic} .
● A dash (be it a small hyphen - or a long dash — ): \p{Dash_Punctuation}
or \p{pd} .
● A currency symbol, such as $ , € or another: \p{Currency_Symbol} or
\p{sc} .
● …And much more. Unicode has a lot of character categories that we can select
from.
These patterns require 'u' regexp flag to work. More about that in the chapter
Unicode: flag "u".
✔ Tasks
The time has a format: hours:minutes . Both hours and minutes has two digits,
like 09:00 .
Make a regexp to find time in the string: Breakfast at 09:00 in the room
123:456.
P.S. In this task there’s no need to check time correctness yet, so 25:99 can also
be a valid result. P.P.S. The regexp shouldn’t match 123:456 .
To solution
Don’t try to remember the list – soon we’ll deal with each of them separately and
you’ll know them by heart automatically.
Escaping
Let’s say we want to find a dot literally. Not “any character”, but just a dot.
To use a special character as a regular one, prepend it with a backslash: \. .
For example:
If we’re looking for a backslash \ , it’s a special character in both regular strings and
regexps, so we should double it.
A slash
A slash symbol '/' is not a special character, but in JavaScript it is used to open
and close the regexp: /...pattern.../ , so we should escape it too.
On the other hand, if we’re not using /.../ , but create a regexp using new
RegExp , then we don’t need to escape it:
new RegExp
If we are creating a regular expression with new RegExp , then we don’t have to
escape / , but need to do some other escaping.
The search worked with /\d\.\d/ , but with new RegExp("\d\.\d") it doesn’t
work, why?
The reason is that backslashes are “consumed” by a string. Remember, regular
strings have their own special characters like \n , and a backslash is used for
escaping.
Please, take a look, what “\d.\d” really is:
alert("\d\.\d"); // d.d
So the call to new RegExp gets a string without backslashes. That’s why the
search doesn’t work!
Summary
● To search special characters [ \ ^ $ . | ? * + ( ) literally, we need to
prepend them with \ (“escape them”).
● We also need to escape / if we’re inside /.../ (but not inside new RegExp ).
● When passing a string new RegExp , we need to double backslashes \\ , cause
strings consume one of them.
Sets
For instance, [eao] means any of the 3 characters: 'a' , 'e' , or 'o' .
That’s called a set. Sets can be used in a regexp along with regular characters:
Please note that although there are multiple characters in the set, they correspond to
exactly one character in the match.
So the example below gives no matches:
Ranges
In the example below we’re searching for "x" followed by two digits or letters from
A to F :
Please note that in the word Exception there’s a substring xce . It didn’t match
the pattern, because the letters are lowercase, while in the set [0-9A-F] they are
uppercase.
If we want to find it too, then we can add a range a-f : [0-9A-Fa-f] . The i flag
would allow lowercase too.
For instance, we want to match all wordly characters or a dash, for words like
“twenty-third”. We can’t do it with \w+ , because \w class does not include a dash.
But we can use [\w-] .
We also can use several classes, for example [\s\S] matches spaces or non-
spaces – any character. That’s wider than a dot "." , because the dot matches any
character except a newline (unless s flag is set).
Excluding ranges
Besides normal ranges, there are “excluding” ranges that look like [^…] .
They are denoted by a caret character ^ at the start and match any character
except the given ones.
For instance:
● [^aeyo] – any character except 'a' , 'e' , 'y' or 'o' .
● [^0-9] – any character except a digit, the same as \D .
● [^\s] – any non-space character, same as \S .
The example below looks for any characters except letters, digits and spaces:
No escaping in […]
Usually when we want to find exactly the dot character, we need to escape it like
\. . And if we need a backslash, then we use \\ .
In square brackets the vast majority of special characters can be used without
escaping:
● A dot '.' .
● A plus '+' .
●
Parentheses '( )' .
●
Dash '-' in the beginning or the end (where it does not define a range).
● A caret '^' if not in the beginning (where it means exclusion).
● And the opening square bracket '[' .
In other words, all special characters are allowed except where they mean
something for square brackets.
A dot "." inside square brackets means just a dot. The pattern [.,] would look
for one of characters: either a dot or a comma.
In the example below the regexp [-().^+] looks for one of the characters -
().^+ :
// No need to escape
let reg = /[-().^+]/g;
…But if you decide to escape them “just in case”, then there would be no harm:
// Escaped everything
let reg = /[\-\(\)\.\^\+]/g;
✔ Tasks
Java[^script]
To solution
To solution
Quantity {n}
alert(numbers); // 7,903,123,45,67
Shorthands
+
Means “one or more”, the same as {1,} .
?
Means “zero or one”, the same as {0,1} . In other words, it makes the symbol
optional.
For instance, the pattern ou?r looks for o followed by zero or one u , and then r .
*
Means “zero or more”, the same as {0,} . That is, the character may repeat any
times or be absent.
For example, \d0* looks for a digit followed by any number of zeroes:
alert( "100 10 1".match(/\d0*/g) ); // 100, 10, 1
More examples
Quantifiers are used very often. They serve as the main “building block” of complex
regular expressions, so let’s see more examples.
We look for character '<' followed by one or more Latin letters, and then '>' .
For instance, for HTML tags we could use a simpler regexp: <\w+> .
…But because \w means any Latin letter or a digit or '_' , the regexp also
matches non-tags, for instance <_> . So it’s much simpler than <[a-z][a-z0-
9]*> , but less reliable.
In real life both variants are acceptable. Depends on how tolerant we can be to
“extra” matches and whether it’s difficult or not to filter them out by other means.
✔ Tasks
Check it:
To solution
An example of use:
To solution
The first thing to do is to locate quoted strings, and then we can replace them.
A regular expression like /".+"/g (a quote, then something, then the other quote)
may seem like a good fit, but it isn’t!
Greedy search
To find a match, the regular expression engine uses the following algorithm:
● For every position in the string
● Match the pattern at that position.
● If there’s no match, go to the next position.
These common words do not make it obvious why the regexp fails, so let’s elaborate
how the search works for the pattern ".+" .
The regular expression engine tries to find it at the zero position of the source
string a "witch" and her "broom" is one , but there’s a there, so
there’s immediately no match.
Then it advances: goes to the next positions in the source string and tries to find
the first character of the pattern there, and finally finds the quote at the 3rd
position:
2. The quote is detected, and then the engine tries to find a match for the rest of the
pattern. It tries to see if the rest of the subject string conforms to .+" .
In our case the next pattern character is . (a dot). It denotes “any character
except a newline”, so the next string letter 'w' fits:
3. Then the dot repeats because of the quantifier .+ . The regular expression
engine builds the match by taking characters one by one while it is possible.
…When does it become impossible? All characters match the dot, so it only stops
when it reaches the end of the string:
4. Now the engine finished repeating for .+ and tries to find the next character of
the pattern. It’s the quote " . But there’s a problem: the string has finished, there
are no more characters!
The regular expression engine understands that it took too many .+ and starts to
backtrack.
In other words, it shortens the match for the quantifier by one character:
Now it assumes that .+ ends one character before the end and tries to match the
rest of the pattern from that position.
If there were a quote there, then that would be the end, but the last character is
'e' , so there’s no match.
6. The engine keep backtracking: it decreases the count of repetition for '.' until
the rest of the pattern (in our case '"' ) matches:
In the greedy mode (by default) the quantifier is repeated as many times as
possible.
The regexp engine tries to fetch as many characters as it can by .+ , and then
shortens that one by one.
For our task we want another thing. That’s what the lazy quantifier mode is for.
Lazy mode
The lazy mode of quantifier is an opposite to the greedy mode. It means: “repeat
minimal number of times”.
We can enable it by putting a question mark '?' after the quantifier, so that it
becomes *? or +? or even ?? for '?' .
To clearly understand the change, let’s trace the search step by step.
1. The first step is the same: it finds the pattern start '"' at the 3rd position:
2. The next step is also similar: the engine finds a match for the dot '.' :
3. And now the search goes differently. Because we have a lazy mode for +? , the
engine doesn’t try to match a dot one more time, but stops and tries to match the
rest of the pattern '"' right now:
If there were a quote there, then the search would end, but there’s 'i' , so
there’s no match.
4. Then the regular expression engine increases the number of repetitions for the dot
and tries one more time:
Failure again. Then the number of repetitions is increased again and again…
6. The next search starts from the end of the current match and yield one more
result:
In this example we saw how the lazy mode works for +? . Quantifiers +? and ??
work the similar way – the regexp engine increases the number of repetitions only if
the rest of the pattern can’t match on the given position.
1. The pattern \d+ tries to match as many numbers as it can (greedy mode), so it
finds 123 and stops, because the next character is a space ' ' .
3. Then there’s \d+? . The quantifier is in lazy mode, so it finds one digit 4 and
tries to check if the rest of the pattern matches from there.
…But there’s nothing in the pattern after \d+? .
The lazy mode doesn’t repeat anything without a need. The pattern finished, so
we’re done. We have a match 123 4 .
Optimizations
Modern regular expression engines can optimize internal algorithms to work
faster. So they may work a bit different from the described algorithm.
Complex regular expressions are hard to optimize, so the search may work
exactly as described as well.
Alternative approach
With regexps, there’s often more than one way to do the same thing.
In our case we can find quoted strings without lazy mode using the regexp "
[^"]+" :
The regexp "[^"]+" gives correct results, because it looks for a quote '"'
followed by one or more non-quotes [^"] , and then the closing quote.
When the regexp engine looks for [^"]+ it stops the repetitions when it meets the
closing quote, and we’re done.
Please note, that this logic does not replace lazy quantifiers!
It is just different. There are times when we need one or another.
Let’s see an example where lazy quantifiers fail and this variant works right.
For instance, we want to find links of the form <a href="..." class="doc"> ,
with any href .
// Works!
alert( str.match(reg) ); // <a href="link" class="doc">
It worked. But let’s see what happens if there are many links in the text?
Now the result is wrong for the same reason as our “witches” example. The
quantifier .* took too many characters.
// Works!
alert( str.match(reg) ); // <a href="link1" class="doc">, <a href="link2" class="doc
// Wrong match!
alert( str.match(reg) ); // <a href="link1" class="wrong">... <p style="" class="doc
Now it fails. The match includes not just a link, but also a lot of text after it, including
<p...> .
Why?
But the problem is: that’s already beyond the link, in another tag <p> . Not what we
want.
Here’s the picture of the match aligned with the text:
The correct variant would be: href="[^"]*" . It will take all characters inside the
href attribute till the nearest quote, just what we need.
A working example:
// Works!
alert( str1.match(reg) ); // null, no matches, that's correct
alert( str2.match(reg) ); // <a href="link1" class="doc">, <a href="link2" class="do
Summary
Greedy
By default the regular expression engine tries to repeat the quantifier as many times
as possible. For instance, \d+ consumes all possible digits. When it becomes
impossible to consume more (no more digits or string end), then it continues to
match the rest of the pattern. If there’s no match then it decreases the number of
repetitions (backtracks) and tries again.
Lazy
Enabled by the question mark ? after the quantifier. The regexp engine tries to
match the rest of the pattern before each repetition of the quantifier.
As we’ve seen, the lazy mode is not a “panacea” from the greedy search. An
alternative is a “fine-tuned” greedy search, with exclusions. Soon we’ll see more
examples of it.
✔ Tasks
To solution
To solution
Find HTML tags
Create a regular expression to find all (opening and closing) HTML tags with their
attributes.
An example of use:
Here we assume that tag attributes may not contain < and > (inside squotes too),
that simplifies things a bit.
To solution
Capturing groups
A part of a pattern can be enclosed in parentheses (...) . This is called a
“capturing group”.
Example
In the example below the pattern (go)+ finds one or more 'go' :
1. The first part [-.\w]+ (before @ ) may include any alphanumeric word
characters, a dot and a dash, to match john.smith .
2. Then @ , and the domain. It may be a subdomain like host.site.com.uk , so
we match it as "a word followed by a dot ([\w-]+\.) (repeated), and then the
last part must be a word: com or uk (but not very long: 2-20 characters).
That regexp is not perfect, but good enough to fix errors or occasional mistypes.
In this example parentheses were used to make a group for repeating (...)+ . But
there are other uses too, let’s see them.
Contents of parentheses
Parentheses are numbered from left to right. The search engine remembers the
content matched by each of them and allows to reference it in the pattern or in the
replacement string.
For instance, we’d like to find HTML tags <.*?> , and process them.
Let’s wrap the inner content into parentheses, like this: <(.*?)> .
We’ll get both the tag as a whole and its content as an array:
The call to String#match returns groups only if the regexp only looks for the first
match, that is: has no /.../g flag.
If we need all matches with their groups then we can use .matchAll or
regexp.exec as described in Methods of RegExp and String:
let str = '<h1>Hello, world!</h1>';
Here we have two matches for <(.*?)> , each of them is an array with the full
match and groups.
Nested groups
Parentheses can be nested. In this case the numbering also goes from left to right.
Then groups, numbered from left to right. Whichever opens first gives the first group
result[1] . Here it encloses the whole tag content.
Then in result[2] goes the group from the second opening ( till the
corresponding ) – tag name, then we don’t group spaces, but group attributes for
result[3] .
For instance, let’s consider the regexp a(z)?(c)? . It looks for "a" optionally
followed by "z" optionally followed by "c" .
If we run it on the string with a single letter a , then the result is:
alert( match.length ); // 3
alert( match[0] ); // a (whole match)
alert( match[1] ); // undefined
alert( match[2] ); // undefined
The array has the length of 3 , but all groups are empty.
alert( match.length ); // 3
alert( match[0] ); // ac (whole match)
alert( match[1] ); // undefined, because there's nothing for (z)?
alert( match[2] ); // c
The array length is permanent: 3 . But there’s nothing for the group (z)? , so the
result is ["ac", undefined, "c"] .
Named groups
Remembering groups by their numbers is hard. For simple patterns it’s doable, but
for more complex ones we can give names to parentheses.
That’s done by putting ?<name> immediately after the opening paren, like this:
alert(groups.year); // 2019
alert(groups.month); // 04
alert(groups.day); // 30
As you can see, the groups reside in the .groups property of the match.
We can also use them in the replacement string, as $<name> (like $1..9 , but a
name instead of a digit).
alert(rearranged); // 30.04.2019
If we use a function for the replacement, then named groups object is always the
last argument:
alert(rearranged); // 30.04.2019
Usually, when we intend to use named groups, we don’t need positional arguments
of the function. For the majority of real-life cases we only need str and groups .
For instance, if we want to find (go)+ , but don’t want to remember the contents
( go ) in a separate array item, we can write: (?:go)+ .
In the example below we only get the name “John” as a separate member of the
results array:
alert( result.length ); // 2
alert( result[1] ); // John
Summary
Parentheses group together a part of the regular expression, so that the quantifier
applies to it as a whole.
Parentheses groups are numbered left-to-right, and can optionally be named with
(?<name>...) .
The content, matched by a group, can be referenced both in the replacement string
as $1 , $2 etc, or by the name $name if named.
So, parentheses groups are called “capturing groups”, as they “capture” a part of the
match. We get that part separately from the result as a member of the array or in
.groups if it’s named.
✔ Tasks
Write a RegExp that matches colors in the format #abc or #abcdef . That is: #
followed by 3 or 6 hexadecimal digits.
Usage example:
P.S. This should be exactly 3 or 6 hex digits: values like #abcd should not match.
To solution
Create a regexp that looks for positive numbers, including those without a decimal
point.
An example of use:
To solution
Write a regexp that looks for all decimal numbers including integer ones, with the
floating point and negative ones.
An example of use:
To solution
Parse an expression
An arithmetical expression consists of 2 numbers and an operator between them, for
instance:
● 1 + 2
● 1.2 * 3.4
● -3 / -6
● -2 - 2
There may be extra spaces at the beginning, at the end or between the parts.
For example:
alert(a); // 1.2
alert(op); // *
alert(b); // 3.4
To solution
Backreference by number: \n
A group can be referenced in the pattern using \n , where n is the group number.
As we can see, the pattern found an opening quote " , then the text is consumed
lazily till the other quote ' , that closes the match.
To make sure that the pattern looks for the closing quote exactly the same as the
opening one, we can wrap it into a capturing group and use the backreference.
Now it works! The regular expression engine finds the first quote (['"]) and
remembers the content of (...) , that’s the first capturing group.
Further in the pattern \1 means “find the same text as in the first group”, exactly the
same quote in our case.
Please note:
●
To reference a group inside a replacement string – we use $1 , while in the
pattern – a backslash \1 .
● If we use ?: in the group, then we can’t reference it. Groups that are excluded
from capturing (?:...) are not remembered by the engine.
Alternation (OR) |
Alternation is the term in regular expression that is actually a simple “OR”.
A usage example:
We already know a similar thing – square brackets. They allow to choose between
multiple character, for instance gr[ae]y matches gray or grey .
Square brackets allow only characters or character sets. Alternation allows any
expressions. A regexp A|B|C means one of expressions A , B or C .
For instance:
● gr(a|e)y means exactly the same as gr[ae]y .
●
gra|ey means gra or ey .
In previous chapters there was a task to build a regexp for searching time in the form
hh:mm , for instance 12:00 . But a simple \d\d:\d\d is too vague. It accepts
25:99 as the time (as 99 seconds match the pattern).
As a regexp: [01]\d|2[0-3] .
Next, the minutes must be from 0 to 59 . In the regexp language that means [0-
5]\d : the first digit 0-5 , and then any digit.
We’re almost done, but there’s a problem. The alternation | now happens to be
between [01]\d and 2[0-3]:[0-5]\d .
✔ Tasks
There are many programming languages, for instance Java, JavaScript, PHP, C,
C++.
Create a regexp that finds them in the string Java JavaScript PHP C++ C :
To solution
For instance:
[b]text[/b]
[url]http://google.com[/url]
BB-tags can be nested. But a tag can’t be nested into itself, for instance:
Normal:
[url] [b]http://google.com[/b] [/url]
[quote] [b]text[/b] [/quote]
Impossible:
[b][b]text[/b][/b]
[quote]
[b]text[/b]
[/quote]
For instance:
If tags are nested, then we need the outer tag (if we want we can continue the
search in its content):
To solution
The strings should support escaping, the same way as JavaScript strings do. For
instance, quotes can be inserted as \" a newline as \n , and the slash itself as
\\ .
let str = "Just like \"here\".";
Please note, in particular, that an escaped quote \" does not end a string.
So we should search from one quote to the other ignoring escaped quotes on the
way.
.. "test me" ..
.. "Say \"Hello\"!" ... (escaped quotes inside)
.. "\\" .. (double slash inside)
.. "\\ \"" .. (double slash and an escaped quote inside)
In JavaScript we need to double the slashes to pass them right into the string, like
this:
let str = ' .. "test me" .. "Say \\"Hello\\"!" .. "\\\\ \\"" .. ';
To solution
Write a regexp to find the tag <style...> . It should match the full tag: it may have
no attributes <style> or have several of them <style type="..."
id="..."> .
For instance:
To solution
String start ^ and finish $
The caret '^' and dollar '$' characters have special meaning in a regexp. They
are called “anchors”.
The caret ^ matches at the beginning of the text, and the dollar $ – in the end.
let str1 = "Mary had a little lamb, it's fleece was white as snow";
let str2 = 'Everywhere Mary went, the lamp was sure to go';
The pattern ^Mary means: “the string start and then Mary”.
To test whether the string ends with the email, let’s add $ to the pattern:
We can use both anchors together to check whether the string exactly follows the
pattern. That’s often used for validation.
For instance we want to check that str is exactly a color in the form # plus 6 hex
digits. The pattern for the color is #[0-9a-f]{6} .
To check that the whole string exactly matches it, we add ^...$ :
The regexp engine looks for the text start, then the color, and then immediately the
text end. Just what we need.
Anchors have zero length
Anchors just like \b are tests. They have zero-width.
In other words, they do not match a character, but rather force the regexp engine
to check the condition (text start/end).
The behavior of anchors changes if there’s a flag m (multiline mode). We’ll explore it
in the next chapter.
✔ Tasks
Regexp ^$
To solution
Check MAC-address
Usage:
To solution
In the multiline mode they match not only at the beginning and end of the string, but
also at start/end of line.
Line start ^
In the example below the text has multiple lines. The pattern /^\d+/gm takes a
number from the beginning of each one:
alert( str.match(/^\d+/gm) ); // 1, 2, 33
The regexp engine moves along the text and looks for a line start ^ , when finds –
continues to match the rest of the pattern \d+ .
alert( str.match(/^\d+/g) ); // 1
That’s because by default a caret ^ only matches at the beginning of the text, and in
the multiline mode – at the start of any line.
Line end $
The regular expression \w+$ finds the last word in every line
Anchors ^$ versus \n
To find a newline, we can use not only ^ and $ , but also the newline character \n .
The first difference is that unlike anchors, the character \n “consumes” the newline
character and adds it to the result.
So, anchors are usually better, they are closer to what we want to get.
We need a number (let’s say a price has no decimal point) followed by € sign.
Lookahead
The syntax is: x(?=y) , it means "look for x , but match only if followed by y ".
Lookbehind
Lookbehind is similar, but it looks behind. That is, it allows to match a pattern only if
there’s something before.
The syntax is:
●
Positive lookbehind: (?<=y)x , matches x , but only if it follows after y .
● Negative lookbehind: (?<!y)x , matches x , but only if there’s no y before.
For example, let’s change the price to US dollars. The dollar sign is usually before
the number, so to look for $30 we’ll use (?<=\$)\d+ – an amount preceded by
$:
And, to find the quantity – a number, not preceded by $ , we can use a negative
lookbehind (?<!\$)\d+ :
Capture groups
Generally, what’s inside the lookaround (a common name for both lookahead and
lookbehind) parentheses does not become a part of the match.
E.g. in the pattern \d+(?=€) , the € sign doesn’t get captured as a part of the
match. That’s natural: we look for a number \d+ , while (?=€) is just a test that it
should be followed by € .
But in some situations we might want to capture the lookaround expression as well,
or a part of it. That’s possible. Just wrap that into additional parentheses.
For instance, here the currency (€|kr) is captured, along with the amount:
Please note that for lookbehind the order stays be same, even though lookahead
parentheses are before the main pattern.
Summary
The typical situation – a regular expression works fine sometimes, but for certain
strings it “hangs” consuming 100% of CPU.
Introduction
We want to find all tags, with or without attributes – like <a href="..."
class="doc" ...> . We need the regexp to work reliably, because HTML comes
from the internet and can be messy.
In particular, we need it to match tags like <a test="<>" href="#"> – with <
and > in attributes. That’s allowed by HTML standard .
A simple regexp like <[^>]+> doesn’t work, because it stops at the first > , and we
need to ignore <> if inside an attribute:
If we substitute these into the pattern above and throw in some optional spaces \s ,
the full regexp becomes: <\w+(\s*\w+="[^"]*"\s*)*> .
That regexp is not perfect! It doesn’t support all the details of HTML syntax, such as
unquoted values, and there are other ways to improve, but let’s not add complexity. It
will demonstrate the problem for us.
The regexp seems to work:
Great! It found both the long tag <a test="<>" href="#"> and the short one
<b> .
Now, that we’ve got a seemingly working solution, let’s get to the infinite backtracking
itself.
Infinite backtracking
If you run our regexp on the input below, it may hang the browser (or another
JavaScript host):
let str = `<tag a="b" a="b" a="b" a="b" a="b" a="b" a="b" a="b"
a="b" a="b" a="b" a="b" a="b" a="b" a="b" a="b" a="b" a="b" a="b" a="b"
Some regexp engines can handle that search, but most of them can’t.
What’s the matter? Why a simple regular expression “hangs” on such a small string?
Let’s simplify the regexp by stripping the tag name and the quotes. So that we look
only for key=value attributes: <(\s*\w+=\w+\s*)*> .
let str = `<a=b a=b a=b a=b a=b a=b a=b a=b
a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b`;
Here we end the demo of the problem and start looking into what’s going on, why it
hangs and how to fix it.
Detailed example
This regular expression also has the same problem. In most regexp engines that
search takes a very long time (careful – can hang):
alert( '12345678901234567890123456789123456789z'.match(/(\d+)*$/) );
Indeed, the regexp is artificial. But the reason why it is slow is the same as those we
saw above. So let’s understand it, and then the previous example will become
obvious.
What happens during the search of (\d+)*$ in the line 123456789z ?
1. First, the regexp engine tries to find a number \d+ . The plus + is greedy by
default, so it consumes all digits:
\d+.......
(123456789)z
2. Then it tries to apply the star quantifier, but there are no more digits, so it the star
doesn’t give anything.
3. Then the pattern expects to see the string end $ , and in the text we have z , so
there’s no match:
X
\d+........$
(123456789)z
\d+.......
(12345678)9z
5. Now the engine tries to continue the search from the new position ( 9 ).
\d+.......\d+
(12345678)(9)z
X
\d+.......\d+
(12345678)(9)z
X
\d+......\d+
(1234567)(89)z
The search engine backtracks again. Backtracking generally works like this: the
last greedy quantifier decreases the number of repetitions until it can. Then the
previous greedy quantifier decreases, and so on. In our case the last greedy
quantifier is the second \d+ , from 89 to 8 , and then the star takes 9 :
X
\d+......\d+\d+
(1234567)(8)(9)z
7. …Fail again. The second and third \d+ backtracked to the end, so the first
quantifier shortens the match to 123456 , and the star takes the rest:
X
\d+.......\d+
(123456)(789)z
Again no match. The process repeats: the last greedy quantifier releases one
character ( 9 ):
X
\d+.....\d+ \d+
(123456)(78)(9)z
8. …And so on.
The regular expression engine goes through all combinations of 123456789 and
their subsequences. There are a lot of them, that’s why it takes so long.
What to do?
Should we turn on the lazy mode?
// sloooooowwwwww
alert( '12345678901234567890123456789123456789z'.match(/(\d+?)*$/) );
Some regular expression engines have tricky built-in checks to detect infinite
backtracking or other means to work around them, but there’s no universal solution.
Back to tags
How to fix?
The backtracking checks many variants that are an obvious fail for a human.
For instance, in the pattern (\d+)*$ a human can easily see that (\d+)* does
not need to backtrack + . There’s no difference between one or two \d+ :
\d+........
(123456789)z
\d+...\d+....
(1234)(56789)z
(name=value) name=value
Modern regexp engines support so-called “possessive” quantifiers for that. They are
like greedy, but don’t backtrack at all. Pretty simple, they capture whatever they can,
and the search continues. There’s also another tool called “atomic groups” that forbid
backtracking inside parentheses.
In other words:
●
The lookahead ?= looks for the maximal count a+ from the current position.
● And then they are “consumed into the result” by the backreference \1 ( \1
corresponds to the content of the second parentheses, that is a+ ).
There will be no backtracking, because lookahead does not backtrack. If, for
example, it found 5 instances of a+ and the further match failed, it won’t go back to
the 4th instance.
Please note:
There’s more about the relation between possessive quantifiers and lookahead
in articles Regex: Emulate Atomic Grouping (and Possessive Quantifiers) with
LookAhead and Mimicking Atomic Groups .
let badInput = `<tag a=b a=b a=b a=b a=b a=b a=b a=b
a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b a=b`;
Great, it works! We found both a long tag <a test="<>" href="#"> and a
small one <b> , and (!) didn’t hang the engine on the bad input.
Let’s briefly review them here. In short, normally characters are encoded with 2
bytes. That gives us 65536 characters maximum. But there are more characters in
the world.
a 0x0061 2
≈ 0x2248 2
𝒳 0x1d4b3 4
𝒴 0x1d4b4 4
😄 0x1f604 4
So characters like a and ≈ occupy 2 bytes, and those rare ones take 4.
The unicode is made in such a way that the 4-byte characters only have a meaning
as a whole.
In the past JavaScript did not know about that, and many string methods still have
problems. For instance, length thinks that here are two characters:
alert('😄'.length); // 2
alert('𝒳'.length); // 2
…But we can see that there’s only one, right? The point is that length treats 4
bytes as two 2-byte characters. That’s incorrect, because they must be considered
only together (so-called “surrogate pair”).
Normally, regular expressions also treat “long characters” as two 2-byte ones.
That leads to odd results, for instance let’s try to find [𝒳𝒴] in the string 𝒳 :
alert( '𝒳'.match(/[𝒳𝒴]/u) ); // 𝒳
alert( '𝒴'.match(/[𝒳-𝒵]/u) ); // 𝒴
In regular expressions these can be set by \p{…} . And there must be flag 'u' .
For instance, \p{Letter} denotes a letter in any of language. We can also use
\p{L} , as L is an alias of Letter , there are shorter aliases for almost every
property.
Here’s the main tree of properties:
●
Letter L :
● lowercase Ll , modifier Lm , titlecase Lt , uppercase Lu , other Lo
● Number N :
● decimal digit Nd , letter number Nl , other No
●
Punctuation P :
● connector Pc , dash Pd , initial quote Pi , final quote Pf , open Ps , close
Pe , other Po
● Mark M (accents etc):
● spacing combining Mc , enclosing Me , non-spacing Mn
● Symbol S :
● currency Sc , modifier Sk , math Sm , other So
● Separator Z :
●
line Zl , paragraph Zp , space Zs
● Other C :
● control Cc , format Cf , not assigned Cn , private use Co , surrogate Cs
More information
Interested to see which characters belong to a property? There’s a tool at
http://cldr.unicode.org/unicode-utilities/list-unicodeset for that.
For the full Unicode Character Database in text format (along with all properties),
see https://www.unicode.org/Public/UCD/latest/ucd/ .
There are also properties with a value. For instance, Unicode “Script” (a writing
system) can be Cyrillic, Greek, Arabic, Han (Chinese) etc, the list is long.
To search for characters in certain scripts (“alphabets”), we should supply Script=
<value> , e.g. to search for cyrillic letters: \p{sc=Cyrillic} , for Chinese
glyphs: \p{sc=Han} , etc:
alert( str.match(regexp) ); // 你好
Building multi-language \w
The pattern \w means “wordly characters”, but doesn’t work for languages that use
non-Latin alphabets, such as Cyrillic and others. It’s just a shorthand for [a-zA-
Z0-9_] , so \w+ won’t find any Chinese words etc.
Let’s make a “universal” regexp, that looks for wordly characters in any language.
That’s easy to do using Unicode properties:
/[\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]
The match is found, because regexp.exec starts to search from the given
position and goes on by the text, successfully matching “function” later.
So we’ve came to the problem: how to search for a match exactly at the given
position.
That’s what y flag does. It makes the regexp search only at the lastIndex
position.
Here’s an example
As we can see, now the regexp is only matched at the given position.
So what y does is truly unique, and very important for writing parsers.
The y flag allows to test a regular expression exactly at the given position and when
we understand what’s there, we can move on – step by step examining the text.
Without the flag the regexp engine always searches till the end of the text, that takes
time, especially if the text is large. So our parser would be very slow. The y flag is
exactly the right thing here.
Solutions
ArrayBuffer, binary arrays
function concat(arrays) {
// sum of individual array lengths
let totalLength = arrays.reduce((acc, value) => acc + value.length, 0);
return result;
}
To formulation
Fetch
Fetch users from GitHub
1. fetch('https://api.github.com/users/USERNAME') .
2. If the response has status 200 , call .json() to read the JS object.
If a fetch fails, or the response has non-200 status, we just return null
in the resulting arrray.
return results;
}
To formulation
As we’ll see, fetch also has options that prevent sending the Referer
and even allow to change it (within the same site).
To formulation
LocalStorage, sessionStorage
To formulation
CSS-animations
Animate a plane (CSS)
/* original class */
#flyjet {
transition: all 3s;
}
/* JS adds .growing */
#flyjet.growing {
width: 400px;
height: 240px;
}
Please note that transitionend triggers two times – once for every
property. So if we don’t perform an additional check then the message would
show up 2 times.
To formulation
We need to choose the right Bezier curve for that animation. It should have
y>1 somewhere for the plane to “jump out”.
For instance, we can take both control points with y>1 , like: cubic-
bezier(0.25, 1.5, 0.75, 1.5) .
The graph:
Animated circle
To formulation
JavaScript animations
To to get the “bouncing” effect we can use the timing function bounce in
easeOut mode.
animate({
duration: 2000,
timing: makeEaseOut(bounce),
draw(progress) {
ball.style.top = to * progress + 'px'
}
});
To formulation
The horizontal coordinate changes by another law: it does not “bounce”, but
gradually increases shifting the ball to the right.
The code:
To formulation
Custom elements
Please note:
1. We clear setInterval timer when the element is removed from the
document. That’s important, otherwise it continues ticking even if not
needed any more. And the browser can’t clear the memory from this
element and referenced by it.
2. We can access current date as elem.date property. All class
methods and properties are naturally element methods and properties.
To formulation
Character classes
To formulation
Java[^script]
To formulation
Answer: \d\d[-:]\d\d .
Please note that the dash '-' has a special meaning in square brackets,
but only between other characters, not when it’s in the beginning or at the
end, so we don’t need to escape it.
To formulation
Solution:
Please note that the dot is a special character, so we have to escape it and
insert as \. .
To formulation
// color
alert( "#123456".match( /#[a-f0-9]{6}\b/gi ) ); // #123456
// not a color
alert( "#12345678".match( /#[a-f0-9]{6}\b/gi ) ); // null
To formulation
First the lazy \d+? tries to take as little digits as it can, but it has to reach
the space, so it takes 123 .
Then the second \d+? takes only one digit, because that’s enough.
To formulation
We need to find the beginning of the comment <!-- , then everything till the
end of --> .
The first idea could be <!--.*?--> – the lazy quantifier makes the dot
stop right before --> .
But a dot in JavaScript means “any symbol except the newline”. So multiline
comments won’t be found.
To formulation
To formulation
Capturing groups
We can add exactly 3 more optional hex digits. We don’t need more or less.
Either we have them or we don’t.
In action:
To formulation
An non-negative integer number is \d+ . A zero 0 can’t be the first digit, but
we should allow it in further digits.
Because the decimal part is optional, let’s put it in parentheses with the
quantifier ? .
A positive number with an optional decimal part is (per previous task): \d+
(\.\d+)? .
To formulation
Parse an expression
An operator is [-+*/] .
Please note:
● Here the dash - goes first in the brackets, because in the middle it would
mean a character range, while we just want a character - .
● A slash / should be escaped inside a JavaScript regexp /.../ , we’ll do
that later.
To get a result as an array let’s put parentheses around the data that we
need: numbers and the operator: (-?\d+(\.\d+)?)\s*([-+*/])\s*
(-?\d+(\.\d+)?) .
In action:
let reg = /(-?\d+(\.\d+)?)\s*([-+*\/])\s*(-?\d+(\.\d+)?)/;
We only want the numbers and the operator, without the full match or the
decimal parts.
The full match (the arrays first item) can be removed by shifting the array
result.shift() .
function parse(expr) {
let reg = /(-?\d+(?:\.\d+)?)\s*([-+*\/])\s*(-?\d+(?:\.\d+)?)/;
return result;
}
To formulation
Alternation (OR) |
The regular expression engine looks for alternations one-by-one. That is: first
it checks if we have Java , otherwise – looks for JavaScript and so on.
In action:
To formulation
In action:
let str = `
[b]hello![/b]
[quote]
[url]http://google.com[/url]
[/quote]
`;
Please note that we had to escape a slash for the closing tag [/\1] ,
because normally the slash closes the pattern.
To formulation
Step by step:
In action:
We need either a space after <style and then optionally something else or
the ending > .
In action:
To formulation
Regexp ^$
The empty string is the only match: it starts and immediately finishes.
The task once again demonstrates that anchors are not characters, but tests.
The string is empty "" . The engine first matches the ^ (input start), yes it’s
there, and then immediately the end $ , it’s here too. So there’s a match.
To formulation
Check MAC-address
We need that number NN , and then :NN repeated 5 times (more numbers);
The regexp is: [0-9a-f]{2}(:[0-9a-f]{2}){5}
Now let’s show that the match should capture all the text: start at the
beginning and end at the end. That’s done by wrapping the pattern in
^...$ .
Finally:
To formulation