├── .gitignore
├── 01 - async
├── 1-1-working-with-promises
│ └── 1-01-using-promises.js
├── 1-2-image-with-fallback
│ ├── 1-02-image-with-fallback.js
│ └── 1-03-using-loadImage.js
├── 1-3-chaining-promises
│ └── 1-04-promise-chain.js
├── 1-4-async-await
│ └── 1-05-using-await.js
├── 1-5-promises-parallel
│ ├── 1-06-promise-all.js
│ └── 1-07-promise-allsettled.js
├── 1-6-requestAnimationFrame
│ └── 1-08-requestAnimationFrame.js
└── 1-7-promisification
│ ├── 1-09-promisify-xmlhttprequest.js
│ └── 1-10-using-loadJSON.js
├── 02 - web-storage-api
├── 2-1-check-if-available
│ └── 2-01-check-if-available.js
├── 2-2-persisting-string-data
│ └── 2-02-color-picker-persist.js
├── 2-3-simple-objects
│ ├── 2-03-json-parse-and-stringify.js
│ └── 2-04-persist-object-incorrect.js
├── 2-4-complex-objects
│ ├── 2-05-serialize-date-initial.js
│ ├── 2-06-json-replacer.js
│ ├── 2-07-serialize-with-replacer.js
│ ├── 2-08-json-reviver.js
│ ├── 2-09-parse-with-reviver.js
│ └── 2-10-to-json.js
├── 2-5-listening-for-changes
│ └── 2-11-storage-event.js
├── 2-6-find-known-keys
│ ├── 2-12-get-all-keys.js
│ └── 2-13-get-records-by-key.js
└── 2-7-removing-data
│ ├── 2-14-remove-item.js
│ └── 2-15-removing-all-items.js
├── 03 - urls-and-routing
├── 3-1-resolve-relative-url
│ ├── 3-01-relative-urls.js
│ └── 3-02-same-origin.js
├── 3-2-remove-query-params
│ ├── 3-03-remove-query-params.js
│ ├── 3-04-incorrect-remove-params.js
│ └── 3-05-remove-single-param.js
├── 3-3-add-query-params
│ ├── 3-06-add-query-params.js
│ ├── 3-07-add-without-value.js
│ ├── 3-08-append-non-string.js
│ ├── 3-09-param-encoding.js
│ └── 3-10-set-query-params.js
├── 3-4-read-query-params
│ ├── 3-11-get-query-params.js
│ └── 3-12-using-get-query-params.js
├── 3-5-basic-router
│ ├── 3-13-client-router.js
│ └── 3-14-route-links.js
└── 3-6-url-patterns
│ ├── 3-15-create-pattern.js
│ ├── 3-16-url-patterns.js
│ ├── 3-17-wildcards.js
│ └── 3-18-extract-group.js
├── 04 - network-requests
├── 4-1-xmlhttprequest
│ └── 4-01-xhr.js
├── 4-2-fetch-get
│ ├── 4-02-fetch-get.js
│ └── 4-03-fetch-await.js
├── 4-3-fetch-post
│ ├── 4-04-fetch-post.js
│ └── 4-05-fetch-form-data.js
├── 4-4-fetch-upload
│ └── 4-06-fetch-upload.js
├── 4-5-beacon
│ └── 4-07-send-beacon.js
├── 4-6-server-sent-events
│ └── 4-08-server-sent-events.js
└── 4-7-websockets
│ ├── 4-09-websockets.js
│ ├── 4-10-websocket-send.js
│ └── 4-11-websocket-respond.js
├── 05 - indexeddb
├── 5-1-basics
│ ├── 5-01-open-db.js
│ ├── 5-02-read.js
│ ├── 5-03-add.js
│ ├── 5-04-delete.js
│ ├── 5-05-using-openDatabase.js
│ ├── 5-06-load-render-objects.js
│ ├── 5-07-add-re-render.js
│ └── 5-08-encapsulation.js
├── 5-2-upgrade
│ └── 5-09-upgrade-db.js
├── 5-3-indexes
│ ├── 5-10-create-index.js
│ └── 5-11-query-index.js
├── 5-4-cursors
│ └── 5-12-using-cursor.js
├── 5-5-pagination
│ └── 5-13-pagination.js
└── 5-6-promises
│ ├── 5-14-promise-open.js
│ ├── 5-15-promise-read.js
│ └── 5-16-usage.js
├── 06 - observers
├── 6-1-lazy-load-image
│ └── 6-01-lazy-load-image.js
├── 6-2-promisify-intersection-observer
│ ├── 6-02-promisify-intersection-observer.js
│ └── 6-03-usage.js
├── 6-3-pause-resume-video
│ └── 6-04-pause-resume-video.js
├── 6-4-animate-height
│ └── 6-05-animate-height.js
├── 6-5-resize-observer
│ └── 6-06-resize-observer.js
├── 6-6-animate-scroll
│ ├── 6-07-fade-images.js
│ └── 6-08-image-style.css
└── 6-7-infinite-scroll
│ └── 6-09-infinite-scroll.js
├── 07 - forms
├── 7-01-form-localstorage
│ └── 7-01-remember-username.js
├── 7-02-form-data
│ └── 7-02-using-form-data.js
├── 7-03-form-json
│ ├── 7-03-form-json.js
│ └── 7-04-form-array.js
├── 7-04-field-required
│ └── 7-05-required-field.html
├── 7-05-number-input
│ └── 7-06-number-range.html
├── 7-06-validate-pattern
│ └── 7-07-field-pattern.html
├── 7-07-validating-forms
│ ├── 7-08-disable-validation.html
│ ├── 7-09-error-placeholders.html
│ ├── 7-10-setup-validation.js
│ └── 7-11-trigger-validation.js
├── 7-08-custom-validation
│ └── 7-12-custom-validation.js
├── 7-09-checkbox-group
│ ├── 7-13-validate-checkbox-group.js
│ ├── 7-14-setup-checkbox-validation.js
│ └── 7-15-validating-checkbox-form.js
└── 7-10-async-validation
│ ├── 7-16-async.js
│ ├── 7-17-async-form-submit.js
│ └── 7-18-revalidate-blur.js
├── 08 - web-animations-api
├── 8-0-intro
│ ├── 8-01-keyframe-animation.css
│ └── 8-02-javascript-animation.js
├── 8-1-ripple
│ ├── 8-03-ripple-styles.css
│ └── 8-04-ripple-animation.js
├── 8-2-start-stop
│ └── 8-05-toggle.js
├── 8-3-insert-remove
│ ├── 8-06-insert.js
│ └── 8-07-remove.js
├── 8-4-reverse-cancel
│ ├── 8-08-hover-effect.js
│ ├── 8-09-remove-hover.js
│ └── 8-10-single-animation-function.js
├── 8-5-scroll-progress
│ ├── 8-11-progress-bar-styles.css
│ └── 8-12-scroll-timeline.js
├── 8-6-bounce
│ └── 8-13-bounce-animations.js
├── 8-7-multiple-animations
│ └── 8-14-combine.js
├── 8-8-loading-animation
│ ├── 8-15-loader-element.html
│ ├── 8-16-loader-animation.js
│ └── 8-17-using-loader.js
└── 8-9-media-query
│ └── 8-18-check-reduced-motion.js
├── 09 - web-speech-api
├── 9-1-add-dictation
│ └── 9-01-add-dictation.js
├── 9-2-promise-helper
│ ├── 9-02-promise-helper.js
│ └── 9-03-using-helper.js
├── 9-3-voices
│ └── 9-04-get-voices.js
├── 9-4-synthesis
│ ├── 9-05-synthesis.js
│ └── 9-06-use-another-voice.js
├── 9-5-customizing-parameters
│ └── 9-07-customizing-output.js
└── 9-6-auto-pause
│ └── 9-08-auto-pause.js
├── 10 - files
├── 10-1-load-text
│ ├── 10-01-file-input.html
│ ├── 10-02-load-text.js
│ └── 10-03-promise.js
├── 10-2-load-image
│ ├── 10-04-input.html
│ └── 10-05-load-image.js
├── 10-3-load-video
│ ├── 10-06-video-player.html
│ └── 10-07-play-video.js
├── 10-4-drag-drop
│ ├── 10-08-drop-target.html
│ ├── 10-09-read-dropped-file.js
│ ├── 10-10-drag-drop.js
│ └── 10-11-file-input.js
├── 10-5-permissions
│ └── 10-12-check-permissions.js
├── 10-6-export-api
│ ├── 10-13-select-output-file.js
│ └── 10-14-export-to-file.js
├── 10-7-export-download
│ ├── 10-15-export-link.html
│ └── 10-16-export-link.js
└── 10-8-upload-drag-drop
│ └── 10-17-upload-drag-drop.js
├── 11 - internationalization
├── 11-1-format-date
│ ├── 11-01-format-date.js
│ └── 11-02-format-date-time.js
├── 11-10-format-lists
│ └── 11-16-format-list.js
├── 11-11-sort-names
│ ├── 11-17-sort-collator.js
│ └── 11-18-sort-directly.js
├── 11-2-format-date-parts
│ ├── 11-03-format-parts.js
│ └── 11-04-date-parts.js
├── 11-3-relative-date
│ ├── 11-05-find-offset.js
│ └── 11-06-format-relative-date.js
├── 11-4-format-numbers
│ ├── 11-07-format-number.js
│ ├── 11-08-format-number-de.js
│ └── 11-09-format-range.js
├── 11-5-round-decimal-places
│ └── 11-10-round-number.js
├── 11-6-format-price-range
│ └── 11-11-format-price-range.js
├── 11-7-measurement-units
│ └── 11-12-format-gigabytes.js
├── 11-8-pluralization
│ ├── 11-13-plural-form.js
│ └── 11-14-full-solution.js
└── 11-9-word-count
│ └── 11-15-getting-counts.js
├── 12 - web-components
├── 12-0-intro
│ ├── 12-01-barebones-web-component.js
│ ├── 12-02-register-component.js
│ ├── 12-03-using-custom-element.html
│ ├── 12-04-template-with-slots.html
│ └── 12-05-slot-content.html
├── 12-1-todays-date
│ ├── 12-06-todays-date.js
│ └── 12-07-show-todays-date.html
├── 12-2-custom-date
│ ├── 12-08-custom-date-component.js
│ └── 12-09-using-date.html
├── 12-3-feedback-component
│ ├── 12-10-template.js
│ ├── 12-11-implementation.js
│ ├── 12-12-usage.html
│ └── 12-13-listen-for-event.js
├── 12-4-profile-card
│ ├── 12-14-template.js
│ ├── 12-15-implementation.js
│ └── 12-16-usage.html
├── 12-5-lazy-image
│ ├── 12-17-lazy-image-component.js
│ └── 12-18-usage.html
├── 12-6-disclosure
│ ├── 12-19-template.js
│ ├── 12-20-disclosure.js
│ ├── 12-21-fix-flicker.css
│ ├── 12-22-usage.html
│ └── 12-23-show-default.html
└── 12-7-button
│ ├── 12-24-template.js
│ ├── 12-25-button-component.js
│ ├── 12-26-usage.html
│ └── 12-27-click-listener.html
├── 13 - ui-elements
├── 13-1-alert-dialog
│ ├── 13-01-dialog-markup.html
│ ├── 13-02-dialog-js.js
│ └── 13-03-style-backdrop.css
├── 13-2-confirm-dialog
│ ├── 13-04-confirm-markup.html
│ └── 13-05-event-listeners.js
├── 13-3-confirm-web-component
│ ├── 13-06-template.js
│ ├── 13-07-dialog-implementation.js
│ ├── 13-08-component-markup.html
│ ├── 13-09-using.js
│ └── 13-10-fix-flicker.css
├── 13-4-disclosure
│ ├── 13-11-details.html
│ ├── 13-12-open-attribute.html
│ └── 13-13-toggle.js
├── 13-5-popover
│ └── 13-14-automatic.html
├── 13-6-manual-popover
│ ├── 13-15-trigger-markup.html
│ └── 13-16-toggle.js
├── 13-7-position-popover
│ ├── 13-17-popover-styles.css
│ ├── 13-18-popover-markup.html
│ └── 13-19-popover-position.js
├── 13-8-tooltip
│ ├── 13-20-tooltip-styles.css
│ ├── 13-21-tooltip-markup.html
│ └── 13-22-show-hide.js
└── 13-9-notification
│ ├── 13-23-permissions.js
│ └── 13-24-show-notification.js
├── 14 - device-integration
├── 14-1-battery-status
│ ├── 14-01-battery-markup.html
│ ├── 14-02-battery-status-api.js
│ ├── 14-03-battery-events.js
│ └── 14-04-checking-support.js
├── 14-2-network-status
│ └── 14-05-network-capabilities.js
├── 14-3-device-location
│ └── 14-06-request-location.js
├── 14-4-map
│ └── 14-07-map-iframe.js
├── 14-5-copy-paste
│ ├── 14-08-copy-selection.js
│ ├── 14-09-paste-selection.js
│ └── 14-10-clipboard-permission.js
├── 14-6-web-share
│ └── 14-11-sharing-link.js
└── 14-7-vibrate
│ ├── 14-12-single-vibration.js
│ └── 14-13-multiple-vibrations.js
├── 15 - performance
├── 15-1-page-load
│ └── 15-01-navigation-timing.js
├── 15-2-resource-performance
│ └── 15-02-resource-performance-entries.js
├── 15-3-slowest-resources
│ ├── 15-03-find-slowest-resources.js
│ └── 15-04-fastest-resources.js
├── 15-4-specific-timings
│ └── 15-05-specific-url.js
├── 15-5-rendering-performance
│ ├── 15-06-measure-rendering.js
│ ├── 15-07-lookup-by-name.js
│ └── 15-08-measure-with-data.js
├── 15-6-multi-step
│ ├── 15-09-profiling-multi-step.js
│ └── 15-10-generating-measures.js
└── 15-7-listening-for-performance-entries
│ ├── 15-11-performance-observer.js
│ └── 15-12-batch.js
├── 16 - console
├── 16-01-styling-output
│ └── 16-01-styling-console-output.js
├── 16-02-log-levels
│ └── 16-02-using-log-levels.js
├── 16-03-named-loggers
│ ├── 16-03-create-named-logger.js
│ └── 16-04-using-named-logger.js
├── 16-04-table
│ ├── 16-05-logging-table.js
│ ├── 16-06-object.js
│ └── 16-07-limiting-columns.js
├── 16-05-timers
│ └── 16-08-using-timers.js
├── 16-06-groups
│ └── 16-09-using-groups.js
├── 16-07-counters
│ ├── 16-10-using-counters.js
│ └── 16-11-counter-output.txt
├── 16-08-log-variable
│ ├── 16-12-logging-variable-and-value.js
│ └── 16-13-without-object-shorthand.js
├── 16-09-stack-trace
│ └── 16-14-using-console-trace.js
├── 16-10-assert
│ └── 16-15-using-assert.js
└── 16-11-dir
│ └── 16-16-using-console-dir.js
├── 17 - css
├── 17-01-highlight
│ ├── 17-01-create-range.js
│ ├── 17-02-markup.html
│ ├── 17-03-use-getRange.js
│ ├── 17-04-register-highlight.js
│ ├── 17-05-style-highlight.css
│ ├── 17-06-mark-element.html
│ ├── 17-07-markup-highlight.html
│ └── 17-08-invalid.html
├── 17-02-font-loading
│ ├── 17-09-roboto.js
│ ├── 17-10-add-global-font.js
│ └── 17-11-wait-font-load.js
├── 17-03-view-transitions
│ ├── 17-12-simple-view-transition.js
│ └── 17-13-slow-transition.css
├── 17-04-modify-css
│ └── 17-14-add-css-rule.js
├── 17-05-conditional-css
│ └── 17-15-toggle-class.js
├── 17-06-match-media
│ ├── 17-16-dark-color-scheme.js
│ └── 17-17-listen-changes.js
└── 17-07-computed-style
│ ├── 17-18-html-style.html
│ ├── 17-19-get-computed-style.js
│ ├── 17-20-inline-styles.html
│ ├── 17-21-check-inline-styles.js
│ └── 17-22-check-computed-styles.js
├── 18 - media
├── 18-01-record-screen
│ ├── 18-01-capture.screen.js
│ ├── 18-02-upload-recording.js
│ └── 18-03-trigger-download.js
├── 18-02-capture-camera
│ ├── 18-04-markup.html
│ ├── 18-05-video-stream.js
│ └── 18-06-capture-image.js
├── 18-03-capture-video
│ ├── 18-07-video-element.html
│ ├── 18-08-open-stream.js
│ ├── 18-09-set-up-mediarecorder.js
│ └── 18-10-event-handlers.js
├── 18-04-media-capabilities
│ ├── 18-11-checking-capabilities.js
│ └── 18-12-check-video-format.js
├── 18-05-applying-filters
│ ├── 18-13-markup.html
│ └── 18-14-video-stream.js
└── 18-15-css-filter.css
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
--------------------------------------------------------------------------------
/01 - async/1-1-working-with-promises/1-01-using-promises.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 1-1. Using a Promise based API
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | // Assuming a function called getUsers that returns a Promise
7 | getUsers()
8 | .then(
9 | // This function is called when the user list has been loaded.
10 | userList => {
11 | console.log('User List:');
12 | userList.forEach(user => {
13 | console.log(user.name);
14 | });
15 | }
16 | ).catch(error => {
17 | console.error('Failed to load the user list:', error);
18 | });
19 |
--------------------------------------------------------------------------------
/01 - async/1-2-image-with-fallback/1-02-image-with-fallback.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 1-2. Loading an image with a fallback
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Loads an image. If there's an error loading the image, uses a fallback
8 | * image URL instead.
9 | *
10 | * @param url The image URL to load
11 | * @param fallbackUrl The fallback image to load if there's an error
12 | * @returns a Promise that resolves to an Image element to insert into the DOM
13 | */
14 | function loadImage(url, fallbackUrl) {
15 | return new Promise((resolve, reject) => {
16 | const image = new Image();
17 |
18 | // Attempt to load the image from the given URL
19 | image.src = url;
20 |
21 | // The image triggers the `load` event when it is successfully loaded.
22 | image.addEventListener('load', () => {
23 | // The now-loaded image is used to resolve the `Promise`
24 | resolve(image);
25 | });
26 |
27 | // If an image failed to load, it triggers the `error` event.
28 | image.addEventListener('error', error => {
29 | // Reject the `Promise` in one of two scenarios:
30 | // (1) There is no fallback URL
31 | // (2) The fallback URL is the one that failed
32 | if (!fallbackUrl || image.src === fallbackUrl) {
33 | reject(error);
34 | } else {
35 | // If this is executed, it means the original image failed to load.
36 | // Try to load the fallback.
37 | image.src = fallbackUrl;
38 | }
39 | });
40 | });
41 | }
42 |
43 | loadImage('https://example.com/profile.jpg', 'https://example.com/fallback.jpg')
44 | .then(image => {
45 | // `container` is an element in the DOM where the image will go
46 | container.appendChild(image);
47 | }).catch(error => {
48 | console.error('Image load failed');
49 | });
50 |
--------------------------------------------------------------------------------
/01 - async/1-2-image-with-fallback/1-03-using-loadImage.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 1-3. Using the loadImage function
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | *
5 | * Demonstrates usage of the loadImage function from Example 1-2.
6 | */
7 |
8 | loadImage('https://example.com/profile.jpg', 'https://example.com/fallback.jpg')
9 | .then(image => {
10 | // `container` is an element in the DOM where the image will go
11 | container.appendChild(image);
12 | }).catch(error => {
13 | console.error('Image load failed');
14 | });
--------------------------------------------------------------------------------
/01 - async/1-3-chaining-promises/1-04-promise-chain.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 1-4. Using a Promise chain
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | *
5 | * Assumes there are getUser and getPosts functions that
6 | * return Promises.
7 | */
8 |
9 | /**
10 | * Loads the post titles for a given user ID.
11 | * @param userId the ID of the user whose posts you want to load
12 | * @returns a Promise that resolves to an array of post titles
13 | */
14 | function getPostTitles(userId) {
15 | return getUser(userId)
16 | // Callback is called with the loaded user object
17 | .then(user => {
18 | console.log(`Getting posts for ${user.name}`);
19 | // This `Promise` is also returned from `.then`
20 | return getPosts(user);
21 | })
22 | // Calling `then` on the `getPosts` `Promise`
23 | .then(posts => {
24 | // Returns another Promise that will resolve to an array of post titles
25 | return posts.map(post => post.title);
26 | })
27 | // Called if either getUser or getPosts are rejected
28 | .catch(error => {
29 | console.error('Error loading data:', error);
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/01 - async/1-4-async-await/1-05-using-await.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 1-5. Using the await keyword
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | *
5 | * Assumes there is a getUsers function that returns a Promise.
6 | */
7 |
8 | // A function must be declared with the `async` keyword
9 | // in order to use `await` in its body.
10 | async function listUsers() {
11 | try {
12 | // Equivalent to getUsers().then(...)
13 | const userList = await getUsers();
14 | console.log('User List:');
15 | userList.forEach(user => {
16 | console.log(user.name);
17 | });
18 | } catch (error) { // Equivalent to .catch(...)
19 | console.error('Failed to load the user list:', error);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/01 - async/1-5-promises-parallel/1-06-promise-all.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 1-6. Loading multiple users with Promise.all
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | *
5 | * Assumes there is a getUser function that returns a Promise.
6 | */
7 |
8 | // Loading three users at once
9 | Promise.all([
10 | getUser(1),
11 | getUser(2),
12 | getUser(3)
13 | ]).then(users => {
14 | // users is an array of user objects - the values returned from
15 | // the parallel getUser calls
16 | }).catch(error => {
17 | // If any of the above Promises are rejected
18 | console.error('One of the users failed to load:', error);
19 | });
20 |
--------------------------------------------------------------------------------
/01 - async/1-5-promises-parallel/1-07-promise-allsettled.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 1-7. Using Promise.allSettled
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | *
5 | * Assumes there is a getUser function that returns a Promise.
6 | */
7 |
8 | Promise.allSettled([
9 | getUser(1),
10 | getUser(2),
11 | getUser(3)
12 | ]).then(results => {
13 | results.forEach(result => {
14 | if (result.status === 'fulfilled') {
15 | console.log('- User:', result.value.name);
16 | } else {
17 | console.log('- Error:', result.reason);
18 | }
19 | });
20 | });
21 | // No catch necessary here because allSettled is always fulfilled
22 |
--------------------------------------------------------------------------------
/01 - async/1-6-requestAnimationFrame/1-08-requestAnimationFrame.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 1-8. Fade-out animation using requestAnimationFrame
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | *
5 | * Animates an element's opacity with the requestAnimationFrame
6 | * function.
7 | */
8 |
9 | const animationSeconds = 2; // Animate over 2 seconds
10 | const fps = 60; // A nice smooth animation
11 |
12 | // The time interval between each frame.
13 | const frameInterval = 1000 / fps;
14 |
15 | // The total number of frames for the animation.
16 | const frameCount = animationSeconds * fps;
17 |
18 | // The amount to adjust the opacity by in each frame.
19 | const opacityIncrement = 1 / frameCount;
20 |
21 | // The timestamp of the last frame.
22 | let lastTimestamp;
23 |
24 | // The starting opacity value
25 | let opacity = 1;
26 |
27 | function fade(timestamp) {
28 | // Set the last timestamp to now if there isn't an existing one.
29 | if (!lastTimestamp) {
30 | lastTimestamp = timestamp;
31 | }
32 |
33 | // Calculate how much time has elapsed since the last frame.
34 | // If not enough time has passed yet, schedule another call of this
35 | // function and return.
36 | const elapsed = timestamp - lastTimestamp;
37 | if (elapsed < frameInterval) {
38 | requestAnimationFrame(animate);
39 | return;
40 | }
41 |
42 | // Time for a new animation frame. Remember this timestamp.
43 | lastTimestamp = timestamp;
44 |
45 | // Adjust the opacity value and make sure it doesn't go below 0.
46 | opacity = Math.max(0, opacity - opacityIncrement)
47 | box.style.opacity = opacity;
48 |
49 | // If the opacity hasn't reached the target value of 0, schedule another
50 | // call to this function.
51 | if (opacity > 0) {
52 | requestAnimationFrame(animate);
53 | }
54 | }
55 |
56 | // Schedule the first call to the animation function
57 | requestAnimationFrame(fade);
58 |
--------------------------------------------------------------------------------
/01 - async/1-7-promisification/1-09-promisify-xmlhttprequest.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 1-9. Promisifying the XMLHttpRequest API
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | *
5 | * Wraps the event-based XMLHttpRequest API in a Promise.
6 | */
7 |
8 | /**
9 | * Sends a GET request to the specified URL. Returns a Promise that will resolve to the
10 | * JSON body parsed as an object, or will reject if there is an error or the response is not
11 | * valid JSON.
12 | */
13 | function loadJSON(url) {
14 | // Create a new Promise object, performing the async work inside the
15 | // constructor function.
16 | return new Promise((resolve, reject) => {
17 | const request = new XMLHttpRequest();
18 |
19 | // If the request is successful, parse the JSON response and
20 | // resolve the `Promise` with the resulting object
21 | request.addEventListener('load', event => {
22 | // Wrap the JSON.parse call in a try/catch block just in case
23 | // the response body is not valid JSON.
24 | try {
25 | resolve(JSON.parse(event.target.responseText));
26 | } catch (error) {
27 | // There was an error parsing the response body.
28 | // Reject the `Promise` with this error.
29 | reject(error);
30 | }
31 | });
32 |
33 | // If the request fails, reject the `Promise` with the
34 | // error that was emitted
35 | request.addEventListener('error', error => {
36 | reject(error);
37 | });
38 |
39 | // Set the target URL and send the request
40 | request.open('GET', url);
41 | request.send();
42 | });
43 | }
44 |
--------------------------------------------------------------------------------
/01 - async/1-7-promisification/1-10-using-loadJSON.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 1-10. Using the loadJSON helper
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | *
5 | * Demonstrates the usage of the loadJSON helper function
6 | * from Example 1-8.
7 | */
8 |
9 | // Using .then
10 | loadJSON('/api/users/1').then(user => {
11 | console.log('Got user:', user);
12 | })
13 |
14 | // Using await
15 | const user = await loadJSON('/api/users/1');
16 | console.log('Got user:', user);
17 |
--------------------------------------------------------------------------------
/02 - web-storage-api/2-1-check-if-available/2-01-check-if-available.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 2-1. Checking if local storage is available
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Determines if local storage is available.
8 | * @returns true if the browser can use local storage, false if not
9 | */
10 | function isLocalStorageAvailable() {
11 | try {
12 | // Local storage is available if the property exists
13 | return typeof window.localStorage !== 'undefined';
14 | } catch (error) {
15 | // If window.localStorage exists but the user is blocking local
16 | // storage, the attempting to read the property throws an exception.
17 | // If this happens, consider local storage not available.
18 | return false;
19 | }
20 | }
--------------------------------------------------------------------------------
/02 - web-storage-api/2-2-persisting-string-data/2-02-color-picker-persist.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 2-2. Persisting data to local storage
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | // A reference to the color picker input element.
7 | const colorPicker = document.querySelector('#colorPicker');
8 |
9 | // Load the saved color, if any, and set it on the color picker.
10 | const storedValue = localStorage.getItem('savedColor');
11 | if (storedValue) {
12 | console.log('Found saved color:', storedValue);
13 | colorPicker.value = storedValue;
14 | }
15 |
16 | // Update the saved color whenever the value changes.
17 | colorPicker.addEventListener('change', event => {
18 | localStorage.setItem('savedColor', event.target.value);
19 | console.log('Saving new color:', colorPicker.value);
20 | });
--------------------------------------------------------------------------------
/02 - web-storage-api/2-3-simple-objects/2-03-json-parse-and-stringify.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 2-3. Using JSON.parse and JSON.stringify
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Given a user profile object, serialize it to JSON and store it in local storage.
8 | * @param userProfile the profile object to save
9 | */
10 | function saveProfile(userProfile) {
11 | localStorage.setItem('userProfile', JSON.stringify(userProfile));
12 | }
13 |
14 | /**
15 | * Loads the user profile from local storage, and deserializes the JSON back to
16 | * an object. If there is no stored profile, an empty object is returned.
17 | * @returns the stored user profile, or an empty object
18 | */
19 | function loadProfile() {
20 | // If there is no stored `userProfile` value, this will return `null`. In this case,
21 | // use the default value of an empty object.
22 | return JSON.parse(localStorage.getItem('userProfile')) || {};
23 | }
24 |
--------------------------------------------------------------------------------
/02 - web-storage-api/2-3-simple-objects/2-04-persist-object-incorrect.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 2-4. Attempting to persist an array
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | *
5 | * Demonstrates that you can't use `localStorage.setItem` to persist an
6 | * object directly.
7 | */
8 |
9 | const userProfile = {
10 | firstName: 'Ava',
11 | lastName: 'Johnson'
12 | };
13 |
14 | localStorage.setItem('userProfile', userProfile);
15 |
16 | // Prints '[object Object]'
17 | console.log(localStorage.getItem('userProfile'));
18 |
--------------------------------------------------------------------------------
/02 - web-storage-api/2-4-complex-objects/2-05-serialize-date-initial.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 2-5. Attempting to serialize an object with a Date property
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const userProfile = {
7 | firstName: 'Ava',
8 | lastName: 'Johnson',
9 |
10 | // This date represents June 2, 2025.
11 | // Months start with zero but days start with 1.
12 | lastUpdated: new Date(2025, 5, 2);
13 | }
14 |
15 | const json = JSON.stringify(userProfile);
16 |
17 |
--------------------------------------------------------------------------------
/02 - web-storage-api/2-4-complex-objects/2-06-json-replacer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 2-6. The replacer function
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | function replacer(key, value) {
7 | if (key === '') {
8 | // first replacer call, `value` is the object itself.
9 | // Return all properties of the object, but transform `due`.
10 | // This uses object spread syntax to make a copy of `value` before
11 | // adding the `due` property.
12 | return {
13 | ...value, // make a copy to avoid altering the original object
14 | due: value.due.getTime()
15 | };
16 | }
17 |
18 | // After the initial transformation, the replacer is called once for each key-value pair.
19 | // No more replacements are necessary, so return these as is.
20 | return value;
21 | }
22 |
23 | const json = JSON.stringify(todo, replacer);
24 |
--------------------------------------------------------------------------------
/02 - web-storage-api/2-4-complex-objects/2-07-serialize-with-replacer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 2-7. Stringifying with the replacer
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | *
5 | * Demonstrates using the replacer function from Example 2-6.
6 | */
7 |
8 | const json = JSON.stringify(userProfile, replacer);
--------------------------------------------------------------------------------
/02 - web-storage-api/2-4-complex-objects/2-08-json-reviver.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 2-8. The reviver function
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | function reviver(key, value) {
7 | // JSON.parse calls the reviver once for each key/value pair. Watch for the `due` key.
8 | // Only proceed if there's actually a value for `due`.
9 | if (key === 'due' && value) {
10 | // Here, the value is the timestamp. You can pass this to the `Date` constructor
11 | // to create a `Date` object referring to the proper time.
12 | return new Date(value);
13 | }
14 |
15 | // Restore all other values as is.
16 | return value;
17 | }
18 |
19 | const object = JSON.parse(todo, reviver);
20 |
--------------------------------------------------------------------------------
/02 - web-storage-api/2-4-complex-objects/2-09-parse-with-reviver.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 2-9. Parsing with the reviver
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | *
5 | * Demonstrates parsing an object using the reviver function
6 | * from Example 2-8.
7 | */
8 |
9 | const object = JSON.parse(userProfile, reviver);
10 |
--------------------------------------------------------------------------------
/02 - web-storage-api/2-4-complex-objects/2-10-to-json.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 2-10. Using a factory that adds a toJSON function
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * A factory function to create a user profile object,
8 | * with the lastUpdated property set to today and a toJSON method.
9 | *
10 | * @param firstName The user's first name
11 | * @param lastName The user's last name
12 | */
13 | function createUser(firstName, lastName) {
14 | return {
15 | firstName,
16 | lastName,
17 | lastUpdated: new Date(),
18 | toJSON() {
19 | return {
20 | firstName: this.firstName,
21 | lastName: this.lastName,
22 | lastUPdated: this.lastUpdated.getTime();
23 | }
24 | }
25 | }
26 | }
27 |
28 | const userProfile = createUser('Ava', 'Johnson');
29 |
--------------------------------------------------------------------------------
/02 - web-storage-api/2-5-listening-for-changes/2-11-storage-event.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 2-11. Listening for storage changes from another tab
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | *
5 | * Listens for storage changes with the persistent color picker
6 | * from Example 2-2.
7 | */
8 |
9 | // Listen for the `storage` event. If another tab changes the
10 | // `savedColor` item, update this page's color picker with the new value.
11 | window.addEventListener('storage', event => {
12 | if (event.key === 'savedColor') {
13 | console.log('New color was chosen in another tab:', event.newValue);
14 | colorPicker.value = event.newValue;
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/02 - web-storage-api/2-6-find-known-keys/2-12-get-all-keys.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 2-12. Building a list of keys
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Generates an array of all keys found in the local storage area.
8 | * @returns an array of keys
9 | */
10 | function getAllKeys() {
11 | const keys = [];
12 |
13 | for (let i = 0; i < localStorage.length; i++) {
14 | keys.push(localStorage.key(i));
15 | }
16 |
17 | return keys;
18 | }
--------------------------------------------------------------------------------
/02 - web-storage-api/2-6-find-known-keys/2-13-get-records-by-key.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 2-13. Querying for a subset of key-value pairs
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | function getAll(keys) {
7 | const results = {};
8 |
9 | // Check each key in local storage.
10 | for (let i = 0; i < localStorage.length; i++) {
11 |
12 | // Get the ith key. If the keys array includes this key, add it and its value
13 | // to the results object.
14 | const key = localStorage.key(i);
15 | if (keys.includes(key)) {
16 | results[key] = localStorage.getItem(key);
17 | }
18 | }
19 |
20 | // `results` now has all key-value pairs that exist in local storage.
21 | return results;
22 | }
23 |
--------------------------------------------------------------------------------
/02 - web-storage-api/2-7-removing-data/2-14-remove-item.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 2-14. Removing an item from local storage
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | // This is a safe operation. If the key doesn't exist,
7 | // no exception is thrown.
8 | localStorage.removeItem('my-key');
9 |
--------------------------------------------------------------------------------
/02 - web-storage-api/2-7-removing-data/2-15-removing-all-items.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 2-15. Removing all items from local storage
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | localStorage.clear();
7 |
--------------------------------------------------------------------------------
/03 - urls-and-routing/3-1-resolve-relative-url/3-01-relative-urls.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 3-1. Creating relative URLs
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Given a relative path and a base URL, resolves a full absolute URL.
8 | * @param relativePath The relative path for the URL
9 | * @param baseUrl A valid URL to use as the base.
10 | */
11 | function resolveUrl(relativePath, baseUrl) {
12 | return new URL(relativePath, baseUrl).href;
13 | }
14 |
15 | // https://example.com/api/users
16 | console.log(resolveUrl('/api/users', 'https://example.com'));
17 |
18 | // https://example.com/api/v1/users
19 | console.log(resolveUrl('/api/v1/users', 'https://example.com'));
20 |
21 | // https://example.com/api/v1/users
22 | // Note that /api/v2 is discarded due to the leading slash in /api/v1/users
23 | console.log(resolveUrl('/api/v1/users', 'https://example.com/api/v2'));
24 |
25 | // https://example.com/api/v1/users
26 | console.log(resolveUrl('../v1/users/', 'https://example.com/api/v2'));
27 |
28 | // https://example.com/api/v1/users
29 | console.log(resolveUrl('users', 'https://example.com/api/v1/groups'));
30 |
--------------------------------------------------------------------------------
/03 - urls-and-routing/3-1-resolve-relative-url/3-02-same-origin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 3-2. Creating a relative URL on the same origin
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const usersApiUrl = new URL('/api/users', window.location);
7 |
--------------------------------------------------------------------------------
/03 - urls-and-routing/3-2-remove-query-params/3-03-remove-query-params.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 3-3. Removing a URL's query parameters
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Removes all parameters from an input URL.
8 | *
9 | * @param inputUrl a URL string containing query parameters
10 | * @returns a new URL string with all query parameters removed
11 | */
12 | function removeAllQueryParameters(inputUrl) {
13 | const url = new URL(inputUrl);
14 | url.search = '';
15 | return url.toString();
16 | }
17 |
18 | // Results in 'https://example.com/api/users'
19 | removeAllQueryParams('https://example.com/api/users?user=sysadmin&q=user');
20 |
--------------------------------------------------------------------------------
/03 - urls-and-routing/3-2-remove-query-params/3-04-incorrect-remove-params.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 3-4. Incorrectly trying to remove all query parameters
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const url = new URL('https://example.com/api/users?user=sysadmin&q=user');
7 |
8 | url.search = null;
9 | console.log(url.toString()); // https://example.com/api/users?null
10 |
--------------------------------------------------------------------------------
/03 - urls-and-routing/3-2-remove-query-params/3-05-remove-single-param.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 3-5. Removing a single query parameter
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Removes a single parameter from an input URL.
8 | *
9 | * @param inputUrl a URL string containing query parameters
10 | * @param paramName the name of the parameter to remove
11 | * @returns a new URL string with the given query parameter removed
12 | */
13 | function removeQueryParameter(inputUrl, paramName) {
14 | console.log({ inputUrl, paramName });
15 | const url = new URL(inputUrl);
16 | url.searchParams.delete(paramName);
17 | return url.toString();
18 | }
19 |
20 | console.log(
21 | removeQueryParameter(
22 | 'https://example.com/api/users?user=sysadmin&q=user',
23 | 'q'
24 | )
25 | ); // https://example.com/api/users?user=sysadmin
26 |
--------------------------------------------------------------------------------
/03 - urls-and-routing/3-3-add-query-params/3-06-add-query-params.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 3-6. Adding additional query parameters
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const url = new URL('https://example.com/api/search?objectType=user');
7 |
8 | url.searchParams.append('userRole', 'admin');
9 | url.searchParams.append('userRole', 'user');
10 | url.searchParams.append('name', 'luke');
11 |
12 | // Prints "https://example.com/api/search?objectType=user&userRole=admin&userRole=user&name=luke"
13 | console.log(url.toString());
14 |
--------------------------------------------------------------------------------
/03 - urls-and-routing/3-3-add-query-params/3-07-add-without-value.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 3-7. Attempting to call append without a value
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const url = new URL('https://example.com/api/search?objectType=user');
7 |
8 | // TypeError: Failed to execute 'append' on 'URLSearchParams':
9 | // 2 arguments required, but only 1 present.
10 | url.searchParams.append('name');
11 |
--------------------------------------------------------------------------------
/03 - urls-and-routing/3-3-add-query-params/3-08-append-non-string.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 3-8. Appending non-string parameters
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const url = new URL('https://example.com/api/search?objectType=user');
7 |
8 | // The resulting URL has the query string:
9 | // ?objectType=user&name=null&role=undefined
10 | url.searchParams.append('name', null);
11 | url.searchParams.append('role', undefined);
12 |
--------------------------------------------------------------------------------
/03 - urls-and-routing/3-3-add-query-params/3-09-param-encoding.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 3-9. Encoding reserved characters in a query parameter
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const url = new URL('https://example.com/api/search');
7 |
8 | // Contrived example string demonstrating several reserved characters
9 | url.searchParams.append('q', 'admin&user?luke');
10 |
--------------------------------------------------------------------------------
/03 - urls-and-routing/3-3-add-query-params/3-10-set-query-params.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 3-10. Adding query parameters with set
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const url = new URL('https://example.com/api/search?objectType=user');
7 |
8 | url.searchParams.set('userRole', 'admin');
9 | url.searchParams.set('userRole', 'user');
10 | url.searchParams.set('name', 'luke');
11 |
--------------------------------------------------------------------------------
/03 - urls-and-routing/3-4-read-query-params/3-11-get-query-params.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 3-11. Reading query parameters
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Takes a URL and returns an array of its query parameters.
8 | *
9 | * @param inputUrl A URL string
10 | * @returns An array of objects with key and value properties
11 | */
12 | function getQueryParameters(inputUrl) {
13 | // Can't use an object here because there may be multiple
14 | // parameters with the same key, and we want to return all parameers.
15 | const result = [];
16 |
17 | const url = new URL(inputUrl);
18 |
19 | // Add each key/value pair to the result array
20 | url.searchParams.forEach((value, key) => {
21 | result.push({ key, value });
22 | });
23 |
24 | // Results are ready!
25 | return result;
26 | }
27 |
--------------------------------------------------------------------------------
/03 - urls-and-routing/3-4-read-query-params/3-12-using-get-query-params.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 3-12. Using the getQueryParameters function
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | // The name parameter contains a percent-encoded ampersand character (%26)
7 | getQueryParameters('https://example.com/api/search?name=luke%26ben');
8 |
--------------------------------------------------------------------------------
/03 - urls-and-routing/3-5-basic-router/3-13-client-router.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 3-13. A simple client-side router
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | // Route definitions. Each route has a path and some content to render.
7 | const routes = [
8 | { path: '/', content: '
Home
' },
9 | { path: '/about', content: '
About
' }
10 | ];
11 |
12 | function navigate(path, pushState = true) {
13 | // Find the matching route and render its content
14 | const route = this.routes.find(route => route.path === path);
15 |
16 | // Be careful using innerHTML in a real app!
17 | document.querySelector('#main').innerHTML = route.content;
18 |
19 | if (pushState) {
20 | // Change the URL to match the new route
21 | history.pushState({}, '', path);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/03 - urls-and-routing/3-5-basic-router/3-14-route-links.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 3-14. Adding click handlers to route links
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | document.querySelectorAll('a').forEach(link => {
7 | link.addEventListener('click', event => {
8 | // Prevent the browser from trying to load the new URL from the server!
9 | event.preventDefault();
10 | navigate(link.getAttribute('href'));
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/03 - urls-and-routing/3-6-url-patterns/3-15-create-pattern.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 3-15. Creating a URLPattern
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const profilePattern = new URLPattern({ pathname: '/api/users/:userId/profile' });
7 |
--------------------------------------------------------------------------------
/03 - urls-and-routing/3-6-url-patterns/3-16-url-patterns.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 3-16. Testing URLs against a pattern
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const profilePattern = new URLPattern({ pathname: '/api/users/:userId/profile' });
7 |
8 | // The pattern won't match a pathname alone; it must be a valid URL
9 | console.log(profilePattern.test('/api/users/123/profile')); // false
10 |
11 | // This URL matches because the pathname matches the pattern
12 | console.log(profilePattern.test('https://example.com/api/users/123/profile')); // true
13 |
14 | // It also matches URL objects
15 | console.log(profilePattern.test(new URL('https://example.com/api/users/123/profile'))); // true
16 |
17 | // The pathname must match exactly, so this won't match.
18 | console.log(profilePattern.test('https://example.com/v1/api/users/123/profile')); // false
19 |
--------------------------------------------------------------------------------
/03 - urls-and-routing/3-6-url-patterns/3-17-wildcards.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 3-17. Using a wildcard in the pattern
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const wildcardProfilePattern = new URLPattern({ pathname: '/*/api/users/:userId/profile' });
7 |
8 | // This matches now because the /v1 portion of the URL matches the wildcard
9 | console.log(wildcardProfilePattern.test('https://example.com/v1/api/users/123/profile')); // true
10 |
--------------------------------------------------------------------------------
/03 - urls-and-routing/3-6-url-patterns/3-18-extract-group.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 3-18. Extracting the user ID
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const profilePattern = new URLPattern({ pathname: '/api/users/:userId/profile' });
7 |
8 | const match = profilePattern.exec('https://example.com/api/users/123/profile');
9 | console.log(match.pathname.input); // '/api/users/123/profile'
10 | console.log(match.pathname.groups.userId); // '123'
11 |
--------------------------------------------------------------------------------
/04 - network-requests/4-1-xmlhttprequest/4-01-xhr.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 4-1. Making a GET request with XMLHttpRequest
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Loads user data from the URL /api/users, then prints them
8 | * to the console.
9 | */
10 | function getUsers() {
11 | const request = new XMLHttpRequest();
12 |
13 | request.addEventListener('load', event => {
14 | // The event target is the XHR itself; it contains a
15 | // responseText property that we can use to create a JavaScript object from
16 | // the JSON text.
17 | const users = JSON.parse(event.target.responseText);
18 | console.log('Got users:', users);
19 | });
20 |
21 | // Handle any potential errors with the request.
22 | // This only handles network errors. If the request
23 | // returns an error status like 404, the `load` event still fires
24 | // where you can inspect the status code.
25 | request.addEventListener('error', err => {
26 | console.log('Error!', err);
27 | });
28 |
29 | request.open('GET', '/api/users');
30 | request.send();
31 | }
32 |
--------------------------------------------------------------------------------
/04 - network-requests/4-2-fetch-get/4-02-fetch-get.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 4-2. Sending a GET request with the Fetch API
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Loads users by calling the /api/users API, and parses the
8 | * response JSON.
9 | * @returns a Promise that resolves to an array of users returned by the API
10 | */
11 | function loadUsers() {
12 | // Make the request
13 | return fetch('/api/users')
14 | // Parse the response body to an object
15 | .then(response => response.json())
16 | // Handle errors, including network and JSON parsing errors
17 | .catch(error => console.error('Couldn\'t fetch:', error.message));
18 | }
19 |
20 | loadUsers().then(users => {
21 | console.log('Got users:', users);
22 | });
23 |
--------------------------------------------------------------------------------
/04 - network-requests/4-2-fetch-get/4-03-fetch-await.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 4-3. Using Fetch with async/await
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | async function loadUsers() {
7 | try {
8 | const response = await fetch('/api/users');
9 | return response.json();
10 | } catch (error) {
11 | console.error('Error loading users:', error);
12 | }
13 | }
14 |
15 | async function printUsers() {
16 | const users = await loadUsers();
17 | console.log('Got users:', users);
18 | }
19 |
--------------------------------------------------------------------------------
/04 - network-requests/4-3-fetch-post/4-04-fetch-post.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 4-4. Sending JSON payload via POST with the Fetch API
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Creates a new user by sending a POST request to /api/users.
8 | * @param firstName The user's first name
9 | * @param lastName The user's last name
10 | * @param department The user's department
11 | * @returns a Promise that resolves to the API response body
12 | */
13 | function createUser(firstName, lastName, department) {
14 | return fetch('/api/users', {
15 | method: 'POST',
16 | body: JSON.stringify({ firstName, lastName, department }),
17 | headers: {
18 | 'Content-Type': 'application/json'
19 | }
20 | })
21 | .then(response => response.json())
22 | .catch(error => console.error('Error:', error.message));
23 | }
24 |
25 | createUser('John', 'Doe', 'Engineering')
26 | .then(() => console.log('Created user!'))
27 | .catch(error => console.error('Error creating user:', error));
28 |
--------------------------------------------------------------------------------
/04 - network-requests/4-3-fetch-post/4-05-fetch-form-data.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 4-5. Sending form data in a POST request
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | fetch('/login', {
7 | method: 'POST',
8 | body: 'username=sysadmin&password=password',
9 | headers: {
10 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
11 | }
12 | })
13 | .then(response => response.json())
14 | .then(data => console.log('Logged in!', data))
15 | .catch(error => console.error('Request failed:', error));
16 |
--------------------------------------------------------------------------------
/04 - network-requests/4-4-fetch-upload/4-06-fetch-upload.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 4-6. Sending file data with the Fetch API
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Given a form with a `file` input, sends a POST request containing
8 | * the file data in its body.
9 | * @param form the form object (should have a file input with the name `file`)
10 | * @returns a Promise that resolves when the response JSON is received
11 | */
12 | function uploadFile(form) {
13 | const formData = new FormData(form);
14 | const fileData = formData.get('file');
15 | return fetch('https://httpbin.org/post', {
16 | method: 'POST',
17 | body: fileData
18 | })
19 | .then(response => response.json());
20 | }
21 |
--------------------------------------------------------------------------------
/04 - network-requests/4-5-beacon/4-07-send-beacon.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 4-7. Sending a beacon
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const currentUser = {
7 | username: 'sysadmin'
8 | };
9 |
10 | // Some analytics data we want to capture
11 | const data = {
12 | user: currentUser.username,
13 | lastVisited: new Date()
14 | };
15 |
16 | // Send the data before unload
17 | document.addEventListener('visibilitychange', () => {
18 | // If the visibility state is 'hidden', that means the page just became hidden
19 | if (document.visibilityState === 'hidden') {
20 | navigator.sendBeacon('/api/analytics', data);
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/04 - network-requests/4-6-server-sent-events/4-08-server-sent-events.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 4-8. Opening an SSE connection
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const events = new EventSource('https://example.com/events');
7 |
8 | // Fired once connected
9 | events.addEventListener('open', () => {
10 | console.log('Connection is open');
11 | });
12 |
13 | // Fired if a connection error occurs
14 | events.addEventListener('error', event => {
15 | console.log('An error occurred:', event);
16 | });
17 |
18 | // Fired when receiving an event with a type of "heartbeat"
19 | events.addEventListener('heartbeat', event => {
20 | console.log('got heartbeat:', event.data);
21 | });
22 |
23 | // Fired when receiving an event with a type of "notice"
24 | events.addEventListener('notice', event => {
25 | console.log('got notice:', event.data);
26 | })
27 |
28 | // The EventSource leaves the connection open. If we want to close the connection,
29 | // we need to call `close` on the EventSource object.
30 | function cleanup() {
31 | events.close();
32 | }
33 |
--------------------------------------------------------------------------------
/04 - network-requests/4-7-websockets/4-09-websockets.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 4-9. Creating a WebSocket connection
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | // Open the WebSocket connection (the URL scheme should be ws: or wss:)
7 | const socket = new WebSocket(url);
8 |
9 | socket.addEventListener('open', onSocketOpened);
10 | socket.addEventListener('message', handleMessage);
11 | socket.addEventListener('error', handleError);
12 | socket.addEventListener('close', onSocketClosed);
13 |
14 | function onSocketOpened() {
15 | console.log('Socket ready for messages');
16 | }
17 |
18 | function handleMessage(event) {
19 | console.log('Received message:', event.data);
20 | }
21 |
22 | function handleError(event) {
23 | console.log('Socket error:', event);
24 | }
25 |
26 | function onSocketClosed() {
27 | console.log('Connection was closed');
28 | }
29 |
--------------------------------------------------------------------------------
/04 - network-requests/4-7-websockets/4-10-websocket-send.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 4-10. Sending WebSocket messages
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | // Messages are simple strings
7 | socket.send('Hello');
8 |
9 | // The socket needs the data as a string, so you can use
10 | // JSON.stringify to serialize objects to be sent
11 | socket.send(JSON.stringify({
12 | username: 'sysadmin',
13 | password: 'password'
14 | }));
15 |
--------------------------------------------------------------------------------
/04 - network-requests/4-7-websockets/4-11-websocket-respond.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 4-11. Responding to a WebSocket message
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | socket.addEventListener('message', event => {
7 | socket.send('ACKNOWLEDGED');
8 | });
9 |
--------------------------------------------------------------------------------
/05 - indexeddb/5-1-basics/5-01-open-db.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 5-1. Opening the database
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Opens the database, creating the object store if needed.
8 | * Because this is asynchronous, it takes a callback function `onSuccess`. Once the
9 | * database is ready, `onSuccess` will be called with the database object.
10 | *
11 | * @param onSuccess A callback function that is executed when the database is ready
12 | */
13 | function openDatabase(onSuccess) {
14 | const request = indexedDB.open('contacts');
15 |
16 | // Create the object store if needed
17 | request.addEventListener('upgradeneeded', () => {
18 | const db = request.result;
19 |
20 | // The contact objects will have an `id` property which will
21 | // be used as the key. When you add a new contact object, you don't need to
22 | // set an `id` property; the `autoIncrement` flag means that the database will
23 | // automatically set an `id` for you.
24 | db.createObjectStore('contacts', {
25 | keyPath: 'id',
26 | autoIncrement: true
27 | });
28 | });
29 |
30 | // When the database is ready for use, it triggers a `success` event.
31 | request.addEventListener('success', () => {
32 | const db = request.result;
33 |
34 | // Call the given callback with the database.
35 | onSuccess(db);
36 | });
37 |
38 | // Always handle errors!
39 | request.addEventListener('error', () => {
40 | console.error('Error opening database:', request.error);
41 | });
42 | }
43 |
--------------------------------------------------------------------------------
/05 - indexeddb/5-1-basics/5-02-read.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 5-2. Reading the contacts
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Reads the contacts from the database, and renders them in the table.
8 | * @param contactsDb The IndexedDB database.
9 | * @param onSuccess A callback function that is executed when the contacts are loaded
10 | */
11 | function getContacts(contactsDb, onSuccess) {
12 | const request = contactsDb
13 | .transaction(['contacts'], 'readonly')
14 | .objectStore('contacts')
15 | .getAll();
16 |
17 |
18 | // When the data has been loaded, the database triggers a `success` event on the
19 | // request object.
20 | request.addEventListener('success', () => {
21 | console.log('Got contacts:', request.result);
22 | onSuccess(request.result);
23 | });
24 |
25 | request.addEventListener('error', () => {
26 | console.error('Error loading contacts:', request.error);
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/05 - indexeddb/5-1-basics/5-03-add.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 5-3. Adding a contact
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Adds a new contact to the database, then re-renders the table.
8 | * @param contactsDb The IndexedDB database.
9 | * @param contact The new contact object to add
10 | * @param onSuccess A callback function that is executed when the contact is added
11 | */
12 | function addContact(contactsDb, contact, onSuccess) {
13 | const request = contactsDb
14 | .transaction(['contacts'], 'readwrite')
15 | .objectStore('contacts')
16 | .add(contact);
17 |
18 | request.addEventListener('success', () => {
19 | console.log('Added new contact:', contact);
20 | onSuccess();
21 | });
22 |
23 | request.addEventListener('error', () => {
24 | console.error('Error adding contact:', request.error);
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/05 - indexeddb/5-1-basics/5-04-delete.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 5-4. Deleting a contact
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Deletes a contact from the database, then re-renders the table.
8 | * @param contactsDb The IndexedDB database.
9 | * @param contact The contact object to delete
10 | * @param onSuccess A callback function that is executed when the contact is deleted
11 | */
12 | function deleteContact(contactsDb, contact, onSuccess) {
13 | const request = contactsDb
14 | .transaction(['contacts'], 'readwrite')
15 | .objectStore('contacts')
16 | .delete(contact.id);
17 |
18 | request.addEventListener('success', () => {
19 | console.log('Deleted contact:', contact);
20 | onSuccess();
21 | });
22 |
23 | request.addEventListener('error', () => {
24 | console.error('Error deleting contact:', request.error);
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/05 - indexeddb/5-1-basics/5-05-using-openDatabase.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 5-5. Using the openDatabase function
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | let contactsDb;
7 |
8 | // Open the database and do the initial contact list render.
9 | // The success handler sets `contactsDb` to the new database object for later use,
10 | // then loads and renders the contacts.
11 | openDatabase(db => {
12 | contactsDb = db;
13 | renderContacts(contactsDb);
14 | });
15 |
--------------------------------------------------------------------------------
/05 - indexeddb/5-1-basics/5-06-load-render-objects.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 5-6. Loading and rendering contacts
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | getContacts(contactsDb, contacts => {
7 | // Contacts have been loaded, now render them
8 | renderContacts(contacts);
9 | });
10 |
--------------------------------------------------------------------------------
/05 - indexeddb/5-1-basics/5-07-add-re-render.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 5-7. Adding and re-rendering contacts
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const newContact = { name: 'Connie Myers', email: 'cmyers@example.com' };
7 | addContact(contactsDb, newContact, () => {
8 | // Contact has been added, now load the updated list and render it
9 | getContacts(contactsDb, contacts => {
10 | renderContacts(contacts);
11 | })
12 | });
13 |
--------------------------------------------------------------------------------
/05 - indexeddb/5-1-basics/5-08-encapsulation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 5-4. An encapsulated database
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const contactsDb = {
7 | open(onSuccess) {
8 | const request = indexedDB.open('contacts');
9 |
10 | request.addEventListener('upgradeneeded', () => {
11 | const db = request.result;
12 | db.createObjectStore('contacts', {
13 | keyPath: 'id',
14 | autoIncrement: true
15 | });
16 | });
17 |
18 | request.addEventListener('success', () => {
19 | this.db = request.result;
20 | onSuccess();
21 | });
22 | },
23 |
24 | getContacts(onSuccess) {
25 | const request = this.db
26 | .transaction(['contacts'], 'readonly')
27 | .objectStore('contacts')
28 | .getAll();
29 |
30 | request.addEventListener('success', () => {
31 | console.log('Got contacts:', request.result);
32 | onSuccess(request.result);
33 | });
34 | },
35 |
36 | // other operations follow similarly
37 | };
38 |
--------------------------------------------------------------------------------
/05 - indexeddb/5-2-upgrade/5-09-upgrade-db.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 5-9. Upgrading a database
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | // todoList database is now at version 2
7 | const request = indexedDB.open('todoList', 2);
8 |
9 | // If the user's database is still at version 1, an `upgradeneeded` event
10 | // is triggered so that the new object store can be added.
11 | request.addEventListener('upgradeneeded', event => {
12 | const db = event.target.result;
13 |
14 | // This event is also triggered when no database exists yet, so you still need
15 | // to handle this case and create the `todos` object store.
16 | // The `oldVersion` property specifies the user's current version of the database. If the
17 | // database is just being created, the version is `0`.
18 | if (event.oldVersion < 1) {
19 | db.createObjectStore('todos', {
20 | keyPath: 'id'
21 | });
22 | }
23 |
24 | // If this database has not yet been upgraded to version 2, create the new object store.
25 | if (event.oldVersion < 2) {
26 | db.createObjectStore('people', {
27 | keyPath: 'id'
28 | });
29 | }
30 | });
31 |
32 | request.addEventListener('success', () => {
33 | // Database is ready to go
34 | });
35 |
36 | // Log any error that might have occurred. The error object is
37 | // stored in the request's `error` property.
38 | request.addEventListener('error', () => {
39 | console.error('Error opening database:', request.error);
40 | });
41 |
--------------------------------------------------------------------------------
/05 - indexeddb/5-3-indexes/5-10-create-index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 5-4. Defining an index when the object store is created
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Opens the database, creating the object store and index if needed.
8 | * Once the database is ready, `onSuccess` will be called with the database object.
9 | *
10 | * @param onSuccess A callback function that is executed when the database is ready
11 | */
12 | function openDatabase(onSuccess) {
13 | const request = indexedDB.open('employees');
14 |
15 | request.addEventListener('upgradeneeded', () => {
16 | const db = request.result;
17 |
18 | // New employee objects will be given an auto-generated
19 | // `id` property which serves as its key.
20 | const employeesStore = db.createObjectStore('employees', {
21 | keyPath: 'id',
22 | autoIncrement: true,
23 | });
24 |
25 | // Create an index on the `department` property called `department`.
26 | employeesStore.createIndex('department', 'department');
27 | });
28 |
29 | request.addEventListener('success', () => {
30 | onSuccess(request.result);
31 | });
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/05 - indexeddb/5-3-indexes/5-11-query-index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 5-11. Querying the employees by the department index
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Gets the employees for a given department, or all employees
8 | * if no department is given.
9 | *
10 | * @param department The department to filter by
11 | * @param onSuccess A callback function that is executed when the employees are loaded
12 | */
13 | function getEmployees(department, onSuccess) {
14 | const request = employeeDb
15 | .transaction(['employees'], 'readonly')
16 | .objectStore('employees')
17 | .index('department')
18 | .getAll(department);
19 |
20 | request.addEventListener('success', () => {
21 | console.log('Got employees:', request.result);
22 | onSuccess(request.result);
23 | });
24 |
25 | request.addEventListener('error', () => {
26 | console.log('Error loading employees:', request.error);
27 | });
28 | }
29 |
--------------------------------------------------------------------------------
/05 - indexeddb/5-4-cursors/5-12-using-cursor.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 5-12. Searching string values with a cursor
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Searches for employees by name.
8 | *
9 | * @param name A query string to match employee names
10 | * @param onSuccess Success callback that will receive the matching employees.
11 | */
12 | function searchEmployees(name, onSuccess) {
13 | // An array to hold all contacts with a name containing the query text.
14 | const results = [];
15 |
16 | const query = name.toLowerCase();
17 |
18 | const request = employeeDb
19 | .transaction(['employees'], 'readonly')
20 | .objectStore('employees')
21 | .openCursor();
22 |
23 |
24 | // The cursor request will emit a `success` event for each object it finds
25 | request.addEventListener('success', () => {
26 | const cursor = request.result;
27 | if (cursor) {
28 | const name = `${cursor.value.firstName} ${cursor.value.lastName}`.toLowerCase();
29 | // Add the contact to the result array if it matches the query.
30 | if (name.includes(query)) {
31 | results.push(cursor.value);
32 | }
33 |
34 | // Continue to the next record.
35 | cursor.continue();
36 | } else {
37 | onSuccess(results);
38 | }
39 | });
40 |
41 | request.addEventListener('error', () => {
42 | console.error('Error searching employees:', request.error);
43 | });
44 | }
45 |
--------------------------------------------------------------------------------
/05 - indexeddb/5-5-pagination/5-13-pagination.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 5-13. Using a cursor to get a page of records
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Uses a cursor to fetch a single "page" of data from an IndexedDB object store.
8 | *
9 | * @param db The IndexedDB database object
10 | * @param storeName The name of the object store
11 | * @param offset The starting offset (0 being the first item)
12 | * @param length The number of items after the offset to return
13 | */
14 | function getPaginatedRecords(db, storeName, offset, length) {
15 | const cursor = db
16 | .transaction([storeName], 'readonly')
17 | .objectStore(storeName)
18 | .openCursor();
19 |
20 | const results = [];
21 |
22 | // This flag indicates whether or not the cursor has skipped ahead to the offset yet.
23 | let skipped = false;
24 |
25 | request.addEventListener('success', event => {
26 | const cursor = event.target.result;
27 |
28 | if (!skipped) {
29 | // Set the flag and skip ahead by the given offset. Next time around,
30 | // the cursor will be in the starting position and can start collecting records.
31 | skipped = true;
32 | cursor.advance(offset);
33 | } else if (cursor && result.length < length) {
34 | // Collect the record the cursor is currently pointing to.
35 | results.push(cursor.value);
36 |
37 | // Continue on to the next record.
38 | cursor.continue();
39 | } else {
40 | // There are either no records left, or the length has been reached.
41 | console.log('Got records:', request.result);
42 | }
43 | });
44 |
45 | request.addEventListener('error', () => {
46 | console.error('Error getting records:', request.error);
47 | });
48 | }
49 |
--------------------------------------------------------------------------------
/05 - indexeddb/5-6-promises/5-14-promise-open.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 5-14. Creating a database with a Promise
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Opens the database, creating the object store if needed.
8 | * @returns a Promise that is resolved with the database, or rejected with an error
9 | */
10 | function openDatabase() {
11 | return new Promise((resolve, reject) => {
12 | const request = indexedDB.open('contacts-promise');
13 |
14 | // Create the object store if needed
15 | request.addEventListener('upgradeneeded', () => {
16 | const db = request.result;
17 | db.createObjectStore('contacts', {
18 | keyPath: 'id',
19 | autoIncrement: true
20 | });
21 | });
22 |
23 | request.addEventListener('success', () => resolve(request.result));
24 | request.addEventListener('error', () => reject(request.error));
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/05 - indexeddb/5-6-promises/5-15-promise-read.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 5-15. Getting objects from a store with a Promise
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Reads the contacts from the database.
8 | * @returns a Promise that is resolved with the contacts, or rejected with an error
9 | */
10 | function getContacts() {
11 | return new Promise((resolve, reject) => {
12 | const request = contactsDb
13 | .transaction(['contacts'], 'readonly')
14 | .objectStore('contacts')
15 | .getAll();
16 |
17 | request.addEventListener('success', () => {
18 | console.log('Got contacts:', request.result);
19 | resolve(request.result);
20 | });
21 |
22 | request.addEventListener('error', () => {
23 | console.error('Error loading contacts:', request.error);
24 | reject(request.error);
25 | });
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/05 - indexeddb/5-6-promises/5-16-usage.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 5-16. Using the promisified database
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | async function loadAndPrintContacts() {
7 | try {
8 | const db = await openDatabase();
9 | const contacts = await getContacts();
10 | console.log('Got contacts:', contacts);
11 | } catch (error) {
12 | console.error('Error:', error);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/06 - observers/6-1-lazy-load-image/6-01-lazy-load-image.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 6-1. Lazily loading an image with IntersectionObserver
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Observes an image element for lazy loading.
8 | *
9 | * @param img a reference to the image DOM node
10 | * @param url the URL of the image to load
11 | */
12 | function lazyLoad(img, url) {
13 | const observer = new IntersectionObserver(entries => {
14 | // isIntersecting becomes true once the image enters the viewport.
15 | // At that point set the src URL, and stop listening.
16 | if (entries[0].isIntersecting) {
17 | img.src = url;
18 | observer.disconnect();
19 | }
20 | });
21 |
22 | // Start observing the image element
23 | observer.observe(img);
24 | }
25 |
--------------------------------------------------------------------------------
/06 - observers/6-2-promisify-intersection-observer/6-02-promisify-intersection-observer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 6-2. Wrapping an IntersectionObserver with a Promise
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Returns a Promise that is resolved once the given element enters the viewport.
8 | */
9 | function waitForElement(element) {
10 | return new Promise(resolve => {
11 | const observer = new IntersectionObserver(entries => {
12 | if (entries[0].isIntersecting) {
13 | observer.disconnect();
14 | resolve();
15 | }
16 | });
17 |
18 | observer.observe(element);
19 | });
20 | }
21 |
--------------------------------------------------------------------------------
/06 - observers/6-2-promisify-intersection-observer/6-03-usage.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 6-3. Using the waitForElement helper to lazily load an image
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | function lazyLoad(img, url) {
7 | waitForElement(img)
8 | .then(() => img.src = url)
9 | }
10 |
--------------------------------------------------------------------------------
/06 - observers/6-3-pause-resume-video/6-04-pause-resume-video.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 6-4. Automatically pausing and resuming a video
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const observer = new IntersectionObserver(entries => {
7 | if (!entries[0].isIntersecting) {
8 | video.pause();
9 | } else {
10 | video.play()
11 | .catch(error => {
12 | // In case there is a permission error auto-playing the video.
13 | // This avoids an unhandled rejection error which could crash your app.
14 | });
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/06 - observers/6-4-animate-height/6-05-animate-height.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 6-5. Animating an element's height due to child element changes
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Watches an element for changes to its children. When the height changes
8 | * due to child changes, animate the change.
9 | * @param element The element to watch for changes.
10 | */
11 | function animateHeightChanges(element) {
12 | // You can't animate an element with `height: auto`, so an explicit
13 | // height is needed here.
14 | element.style.height = `${details.offsetHeight}px`;
15 |
16 | // Set a few CSS properties needed for the animated transition.
17 | element.style.transition = 'height 200ms';
18 | element.style.overflow = 'hidden';
19 |
20 | /**
21 | * This observer will fire when the element's child elements
22 | * change. It measures the new height, then uses `requestAnimationFrame`
23 | * to update the height. The height change will be animated.
24 | */
25 | const observer = new MutationObserver(entries => {
26 | // `entries` is always an array. There may be times where this array has multiple
27 | // elements, but in this case the first and only element is what you need.
28 | const element = entries[0].target;
29 |
30 | // The content has changed, and so the height has as well.
31 | // There are a few steps to measure the new explicit height.
32 |
33 | // (1) Remember the current height to use for the animation's starting point.
34 | const currentHeightValue = element.style.height;
35 |
36 | // (2) Set the height to 'auto' and read the `offsetHeight` property. This
37 | // is the new height to set.
38 | element.style.height = 'auto';
39 | const newHeight = element.offsetHeight;
40 |
41 | // (3) Set the current height back before animating.
42 | element.style.height = currentHeightValue;
43 |
44 | // On the next animation frame, change the height. This will
45 | // trigger the animated transition.
46 | requestAnimationFrame(() => {
47 | element.style.height = `${newHeight}px`;
48 | });
49 | });
50 |
51 | // Begin watching the element for changes.
52 | observer.observe(element, { childList: true });
53 | }
54 |
--------------------------------------------------------------------------------
/06 - observers/6-5-resize-observer/6-06-resize-observer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 6-6. Updating an element's content when it is resized
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | // Look up the element you want to observe.
7 | const container = document.querySelector('#resize-container');
8 |
9 | // Create a ResizeObserver that will watch the element for size changes.
10 | const observer = new ResizeObserver(entries => {
11 | // The observer fires immediately, so you can set the initial text.
12 | // There's typically only going to be one entry in the array - the first element is the element
13 | // you're interested in.
14 | container.textContent = `My width is ${entries[0].contentRect.width}px`;
15 | });
16 |
17 | // Start watching the element.
18 | observer.observe(container);
19 |
--------------------------------------------------------------------------------
/06 - observers/6-6-animate-scroll/6-07-fade-images.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 6-7. Fading in all images on the page when they scroll into view
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const observer = new IntersectionObserver(entries => {
7 | // There are multiple images per row, so there are multiple
8 | // entries.
9 | entries.forEach(entry => {
10 | // Once the element becomes partially visible, apply the animated transition
11 | if (entry.isIntersecting) {
12 | // The image is 25% visible, begin the fade-in transition.
13 | entry.target.style.opacity = 1;
14 |
15 | // No need to observe this element any further
16 | observer.unobserve(entry.target);
17 | }
18 | });
19 | }, { threshold: 0.25 }); // Fires when images become 25% visible
20 |
21 | // Observe all images on the page. Only images with the `animate`
22 | // class name will be observed, since you might not want to do this to
23 | // all images on the page.
24 | document.querySelectorAll('img.animate').forEach(image => {
25 | observer.observe(image);
26 | });
27 |
--------------------------------------------------------------------------------
/06 - observers/6-6-animate-scroll/6-08-image-style.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Example 6-8. Styling images to fade in
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 | img.animate {
6 | opacity: 0;
7 | transition: opacity 500ms;
8 | }
--------------------------------------------------------------------------------
/06 - observers/6-7-infinite-scroll/6-09-infinite-scroll.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 6-9. Using IntersectionObserver for infinite scrolling
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Observes a placeholder element with an IntersectionObserver.
8 | * When the placeholder becomes visible, more data is loaded.
9 | *
10 | * @param placeholder The "Load More" placeholder element
11 | * @param loadMore A function that loads more data
12 | */
13 | function observeForInfiniteScroll(placeholder, loadMore) {
14 | const observer = new IntersectionObserver(entries => {
15 | // If the placeholder becomes visible, it means the user
16 | // has scrolled to the bottom of the list. In this case, time to
17 | // load more data.
18 | if (entries[0].isIntersecting) {
19 | loadMore();
20 | }
21 | });
22 |
23 | observer.observe(placeholder);
24 | }
25 |
--------------------------------------------------------------------------------
/07 - forms/7-01-form-localstorage/7-01-remember-username.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 7-4. Remembering the username field
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 | const form = document.querySelector('#login-form');
6 |
7 | const username = localStorage.getItem('username');
8 | if (username) {
9 | form.elements.username.value = username;
10 | }
11 |
12 | form.addEventListener('submit', event => {
13 | const data = new FormData(form);
14 | localStorage.setItem('username', data.get('username'));
15 | });
16 |
--------------------------------------------------------------------------------
/07 - forms/7-02-form-data/7-02-using-form-data.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 7-2. Adding data with the FormData API
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | // In a real world application, the API token would be stored somewhere and
7 | // not hard coded like this.
8 | const apiToken = 'aBcD1234EfGh5678IjKlM';
9 |
10 | form.addEventListener('submit', event => {
11 | // Important: Stop the browser from automatically submitting the form
12 | event.preventDefault();
13 |
14 | // Set up a FormData object and add the API token to it.
15 | const data = new FormData(event.target);
16 | data.set('apiToken', apiToken);
17 |
18 | // Use the Fetch API to send this FormData object to the endpoint.
19 | fetch('/api/form', {
20 | method: 'POST',
21 | body: data
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/07 - forms/7-03-form-json/7-03-form-json.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 7-3. Submitting a form as JSON using Fetch
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | form.addEventListener('submit', event => {
7 | // Important: Stop the browser from automatically submitting the form
8 | event.preventDefault();
9 |
10 | // Create a new FormData containing this form's data, then add each
11 | // key-value pair to the response body.
12 | const data = new FormData(event.target);
13 | const body = {};
14 | for (const [key, value] of data.entries()) {
15 | body[key] = value;
16 | }
17 |
18 | // Send the JSON body to the form endpoint
19 | fetch('/api/form', {
20 | method: 'POST',
21 |
22 | // The object must be converted to a JSON string
23 | body: JSON.stringify(body),
24 |
25 | // Tell the server you're sending JSON
26 | headers: {
27 | 'content-type': 'application/json'
28 | }
29 | })
30 | .then(response => response.json())
31 | .then(body => console.log('Got response:', body));
32 | });
33 |
--------------------------------------------------------------------------------
/07 - forms/7-03-form-json/7-04-form-array.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 7-4. Handling array form values
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Converts a form's data into an object that can be sent as JSON.
8 | * @param form The form element
9 | * @returns An object containing all the mapped keys and values
10 | */
11 | function toObject(form) {
12 | const data = new FormData(form);
13 | const body = {};
14 |
15 | for (const key of data.keys()) {
16 | // Returns an array of all values bound to a given key
17 | const values = data.getAll(key);
18 |
19 | // If there's only one element in the array, set that element directly
20 | if (values.length === 1) {
21 | body[key] = values[0];
22 | } else {
23 | // Otherwise, set the array
24 | body[key] = values;
25 | }
26 | }
27 |
28 | return body;
29 | }
30 |
--------------------------------------------------------------------------------
/07 - forms/7-04-field-required/7-05-required-field.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/07 - forms/7-05-number-input/7-06-number-range.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/07 - forms/7-06-validate-pattern/7-07-field-pattern.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/07 - forms/7-07-validating-forms/7-08-disable-validation.html:
--------------------------------------------------------------------------------
1 |
5 |
8 |
--------------------------------------------------------------------------------
/07 - forms/7-07-validating-forms/7-09-error-placeholders.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/07 - forms/7-07-validating-forms/7-10-setup-validation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 7-10. Setting up validation for a form field
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Adds the necessary event listeners to an element to participate in form validation.
8 | * It handles setting and clearing error messages depending on the validation state.
9 | * @param element The input element to validate
10 | */
11 | function addValidation(element) {
12 | const errorElement = document.getElementById(`${element.id}-error`);
13 |
14 | /**
15 | * Fired when the form is validated and the field is not valid.
16 | * Sets the error message and style, and also sets the shouldValidate flag.
17 | */
18 | element.addEventListener('invalid', () => {
19 | errorElement.textContent = element.validationMessage;
20 | element.dataset.shouldValidate = true;
21 | });
22 |
23 | /**
24 | * Fired when user input occurs in the field. If the shouldValidate flag is set,
25 | * it will re-check the field's validity and clear the error message if it becomes valid.
26 | */
27 | element.addEventListener('input', () => {
28 | if (element.dataset.shouldValidate) {
29 | if (element.checkValidity()) {
30 | errorElement.textContent = '';
31 | }
32 | }
33 | });
34 |
35 | /**
36 | * Fired when the field loses focus, applying the shouldValidate flag.
37 | */
38 | element.addEventListener('blur', () => {
39 | // This field has been touched, it will now be validated on subsequent
40 | // `input` events.
41 | // This sets the input's `data-should-validate` attribute to `true` in the DOM.
42 | element.dataset.shouldValidate = true;
43 | });
44 | }
45 |
--------------------------------------------------------------------------------
/07 - forms/7-07-validating-forms/7-11-trigger-validation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 7-11. Triggering form validation
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | // Assuming the form has two inputs with name `name` and `email`
7 | addValidation(form.elements.name);
8 | addValidation(form.elements.email);
9 |
10 | form.addEventListener('submit', event => {
11 | event.preventDefault();
12 | if (form.checkValidity()) {
13 | // Validation passed, submit the form
14 | }
15 | });
16 |
--------------------------------------------------------------------------------
/07 - forms/7-08-custom-validation/7-12-custom-validation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 7-12. Using custom validation
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Custom validation function that ensures the password and confirmPassword fields have the same value.
8 | * @param form the form containing the two fields
9 | */
10 | function validatePasswordsMatch(form) {
11 | const { password, confirmPassword } = form.elements;
12 |
13 | if (password.value !== confirmPassword.value) {
14 | confirmPassword.setCustomValidity('Passwords do not match.');
15 | } else {
16 | confirmPassword.setCustomValidity('');
17 | }
18 | }
19 |
20 | form.addEventListener('submit', event => {
21 | event.preventDefault();
22 |
23 | validatePasswordsMatch(form);
24 | if (form.checkValidity()) {
25 | // Validation passed, submit the form
26 | }
27 | });
28 |
--------------------------------------------------------------------------------
/07 - forms/7-09-checkbox-group/7-13-validate-checkbox-group.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 7-13. Validating a checkbox group
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | function validateCheckboxes(form) {
7 | const data = new FormData(form);
8 |
9 | // To avoid setting the validation error on multiple elements,
10 | // choose the first checkbox and use that to hold the group's validation
11 | // message.
12 | const element = form.elements.option1;
13 |
14 | if (!data.has('options')) {
15 | element.setCustomValidity('Please select at least one option.');
16 | } else {
17 | element.setCustomValidity('');
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/07 - forms/7-09-checkbox-group/7-14-setup-checkbox-validation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 7-14. Setting up checkbox validation
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Adds the necessary event listeners to an element to participate in form validation.
8 | * It handles setting and clearing error messages depending on the validation state.
9 | * @param element The input element to validate
10 | * @param errorId The ID of a placeholder element that will show the error message
11 | */
12 | function addValidation(element, errorId) {
13 | const errorElement = document.getElementById(errorId);
14 |
15 | /**
16 | * Fired when the form is validated and the field is not valid.
17 | * Sets the error message and style.
18 | */
19 | element.addEventListener('invalid', () => {
20 | errorElement.textContent = element.validationMessage;
21 | });
22 |
23 | /**
24 | * Fired when user input occurs in the field.
25 | * It will re-check the field's validity and clear the error message if it becomes valid.
26 | */
27 | element.addEventListener('change', () => {
28 | validateCheckboxes(form);
29 | if (form.elements.option1.checkValidity()) {
30 | errorElement.textContent = '';
31 | }
32 | });
33 | }
34 |
--------------------------------------------------------------------------------
/07 - forms/7-09-checkbox-group/7-15-validating-checkbox-form.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 7-15. Validating the checkbox form
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | addValidation(form.elements.option1, 'checkbox-error');
7 | addValidation(form.elements.option2, 'checkbox-error');
8 | addValidation(form.elements.option3, 'checkbox-error');
9 |
10 | form.addEventListener('submit', event => {
11 | event.preventDefault();
12 | validateCheckboxes(form);
13 | console.log(form.checkValidity());
14 | });
15 |
--------------------------------------------------------------------------------
/07 - forms/7-10-async-validation/7-16-async.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 7-16. Performing asynchronous password strength validation
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Calls an API to validate that a password meets strength requirements.
8 | * @param form the form containing the password field
9 | */
10 | async function validatePasswordStrength(form) {
11 | const { password } = form.elements;
12 | const response = await fetch(`/api/password-strength?password=${password.value}`);
13 | const result = await response.json();
14 |
15 | if (result.status === 'error') {
16 | password.setCustomValidity(result.error);
17 | } else {
18 | password.setCustomValidity('');
19 | }
20 | }
--------------------------------------------------------------------------------
/07 - forms/7-10-async-validation/7-17-async-form-submit.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 7-17. The async form submit handler
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | form.addEventListener('submit', async event => {
7 | event.preventDefault();
8 | await validatePasswordStrength(form);
9 | console.log(form.checkValidity());
10 | });
11 |
--------------------------------------------------------------------------------
/07 - forms/7-10-async-validation/7-18-revalidate-blur.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 7-18. Revalidating on blur
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | form.elements.password.addEventListener('blur', async event => {
7 | const password = event.target;
8 | const errorElement = document.getElementById('password-error');
9 | if (password.dataset.shouldValidate) {
10 | await validatePasswordStrength(form);
11 | if (password.checkValidity()) {
12 | errorElement.textContent = '';
13 | password.classList.remove('border-danger');
14 | }
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/08 - web-animations-api/8-0-intro/8-01-keyframe-animation.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 8-1. Using a CSS keyframe animation
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | @keyframes fade {
7 | from {
8 | opacity: 0;
9 | }
10 |
11 | to {
12 | opacity: 1;
13 | }
14 | }
15 |
16 | .some-element {
17 | animation: fade 250ms;
18 | }
19 |
--------------------------------------------------------------------------------
/08 - web-animations-api/8-0-intro/8-02-javascript-animation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 8-2. Fading in with the Web Animations API
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const element = document.querySelector('.some-element');
7 | element.animate([
8 | { opacity: 0 },
9 | { opacity: 1 }
10 | ], { duration: 250 });
11 |
--------------------------------------------------------------------------------
/08 - web-animations-api/8-1-ripple/8-03-ripple-styles.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 8-3. Styles for the button and ripple elements
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | .ripple-button {
7 | position: relative;
8 | overflow: hidden;
9 | }
10 |
11 | .ripple {
12 | background: white;
13 | pointer-events: none;
14 | transform-origin: center;
15 | opacity: 0;
16 | position: absolute;
17 | border-radius: 50%;
18 | width: 150px;
19 | height: 150px;
20 | }
21 |
--------------------------------------------------------------------------------
/08 - web-animations-api/8-1-ripple/8-04-ripple-animation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 8-4. Performing the ripple animation
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | button.addEventListener('click', async event => {
7 | // Create the temporary element for the ripple, set its class, and
8 | // add it to the button.
9 | const ripple = document.createElement('div');
10 | ripple.className = 'ripple';
11 |
12 | // Find the largest dimension (width or height) of the button, and
13 | // use that as the ripple's size.
14 | const rippleSize = Math.max(button.offsetWidth, button.offsetHeight);
15 | ripple.style.width = `${rippleSize}px`;
16 | ripple.style.height = `${rippleSize}px`;
17 |
18 | // Center the ripple element on the click location.
19 | ripple.style.top = `${event.offsetY - (rippleSize / 2)}px`;
20 | ripple.style.left = `${event.offsetX - (rippleSize / 2)}px`;
21 |
22 | button.appendChild(ripple);
23 |
24 | // Perform the ripple animation, and wait for it to complete.
25 | await ripple.animate([
26 | { transform: 'scale(0)', opacity: 0.5 },
27 | { transform: 'scale(2.5)', opacity: 0 }
28 | ], {
29 | duration: 500,
30 | easing: 'ease-in'
31 | }).finished;
32 |
33 | // All done, remove the ripple element
34 | ripple.remove();
35 | });
36 |
--------------------------------------------------------------------------------
/08 - web-animations-api/8-2-start-stop/8-05-toggle.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 8-5. Toggling an animation's play state
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Given an animation, toggles the animation state.
8 | * If the animation is running, it will be paused.
9 | * If it is paused, it will be resumed.
10 | */
11 | function toggleAnimation(animation) {
12 | if (animation.playState === 'running') {
13 | animation.pause();
14 | } else {
15 | animation.play();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/08 - web-animations-api/8-3-insert-remove/8-06-insert.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 8-6. Showing an element with an animation
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Shows an element that was just added to the DOM with a fade in animation.
8 | * @param element The element to show
9 | */
10 | function showElement(element) {
11 | document.body.appendChild(element);
12 | element.animate([
13 | { opacity: 0 },
14 | { opacity: 1 }
15 | ], { duration: 250 });
16 | }
17 |
--------------------------------------------------------------------------------
/08 - web-animations-api/8-3-insert-remove/8-07-remove.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 8-7. Removing an element with an animation
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Removes an element from the DOM after performing a fade out animation.
8 | * @param element The element to remove
9 | */
10 | async function removeElement(element) {
11 | // First, perform the animation and make the element disappear from view.
12 | // The resulting animation's `finished` property is a Promise.
13 | await element.animate([
14 | { opacity: 1 },
15 | { opacity: 0 }
16 | ], { duration: 250 }).finished;
17 |
18 | // Animation is done, now remove the element from the DOM.
19 | element.remove();
20 | }
21 |
--------------------------------------------------------------------------------
/08 - web-animations-api/8-4-reverse-cancel/8-08-hover-effect.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 8-8. The hover effect
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | element.addEventListener('mouseover', async () => {
7 | if (animation) {
8 | // There was already an animation in progress. Instead of starting a new animation,
9 | // reverse the current one.
10 | animation.reverse();
11 | } else {
12 | // Nothing is in progress, so start a new animation.
13 | animation = element.animate([
14 | { transform: 'scale(1)' },
15 | { transform: 'scale(2)' }
16 | ], { duration: 1000, fill: 'both' });
17 |
18 | // Once the animation finishes, set the current animation to null.
19 | await animation.finished;
20 | animation = null;
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/08 - web-animations-api/8-4-reverse-cancel/8-09-remove-hover.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 8-9. Removing the hover effect
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | button.addEventListener('mouseout', async () => {
7 | if (animation) {
8 | // There was already an animation in progress. Instead of starting a new animation,
9 | // reverse the current one.
10 | animation.reverse();
11 | } else {
12 | // Nothing is in progress, so start a new animation.
13 | animation = button.animate([
14 | { transform: 'scale(2)' },
15 | { transform: 'scale(1)' }
16 | ], { duration: 1000, fill: 'both' });
17 |
18 | // Once the animation finishes, set the current animation to null.
19 | await animation.finished;
20 | animation = null;
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/08 - web-animations-api/8-4-reverse-cancel/8-10-single-animation-function.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 8-10. A single animation function
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | async function animate(element, direction) {
7 | if (animation) {
8 | animation.reverse();
9 | } else {
10 | animation = element.animate([
11 | { transform: 'scale(1)' },
12 | { transform: 'scale(2)' }
13 | ], { duration: 1000, fill: 'forwards', direction });
14 |
15 | await animation.finished;
16 | animation = null;
17 | }
18 | }
19 |
20 | element.addEventListener('mouseover', () => {
21 | animate(element, 'normal');
22 | });
23 |
24 | element.addEventListener('mouseout', () => {
25 | animate(element, 'reverse');
26 | });
27 |
--------------------------------------------------------------------------------
/08 - web-animations-api/8-5-scroll-progress/8-11-progress-bar-styles.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 8-11. Scroll progress bar styles
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | .scroll-progress {
7 | height: 8px;
8 | transform-origin: left;
9 | position: sticky;
10 | top: 0;
11 | transform: scaleX(0);
12 | background: blue;
13 | }
14 |
--------------------------------------------------------------------------------
/08 - web-animations-api/8-5-scroll-progress/8-12-scroll-timeline.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 8-12. Creating the scroll timeline
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const progress = document.querySelector('.scroll-progress');
7 |
8 | // Create a timeline that's linked to the document's
9 | // scroll position.
10 | const timeline = new ScrollTimeline({
11 | source: document.documentElement
12 | });
13 |
14 | // Start the animation, passing the timeline you just created.
15 | progress.animate(
16 | [
17 | { transform: 'scaleX(0)' },
18 | { transform: 'scaleX(1)' }
19 | ],
20 | { timeline });
21 |
--------------------------------------------------------------------------------
/08 - web-animations-api/8-6-bounce/8-13-bounce-animations.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 8-13. Applying the bounce animations in series
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | async function animateBounce(element) {
7 | const distances = [ '40px', '20px', '10px' ];
8 | for (let distance of distances) {
9 | // Wait for this animation to complete before continuing
10 | await element.animate([
11 | // Start at the bottom
12 | { transform: 'translateY(0)' },
13 |
14 | // Move up by the current distance
15 | { transform: `translateY(-${distance})`, offset: 0.5 },
16 |
17 | // Back to the bottom
18 | { transform: 'translateY(0)' }
19 | ], {
20 | duration: 250,
21 |
22 | // Use a more fluid easing function than linear
23 | // (the default).
24 | easing: 'ease-in-out'
25 | }).finished;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/08 - web-animations-api/8-7-multiple-animations/8-14-combine.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 8-14. Combining two transform animations
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 | // The first animation will move the element back and forth on the X axis
6 | element.animate([
7 | { transform: 'translateX(0)' },
8 | { transform: 'translateX(250px)' }
9 | ], {
10 | // Animate for 5 seconds
11 | duration: 5000,
12 | // Run the animation forwards, then run it in reverse
13 | direction: 'alternate',
14 | // Repeat the animation forever
15 | iterations: Infinity,
16 | // Slow to start, fast in the middle, slow at the end
17 | easing: 'ease-in-out'
18 | });
19 |
20 | // The second animation rotates the element
21 | element.animate([
22 | { transform: 'rotate(0deg)' },
23 | { transform: 'rotate(360deg)' }
24 | ], {
25 | // Animate for 3 seconds
26 | duration: 3000,
27 | // Repeat the animation forever
28 | iterations: Infinity,
29 | // Combine the effects with other running animations
30 | composite: 'add'
31 | });
32 |
--------------------------------------------------------------------------------
/08 - web-animations-api/8-8-loading-animation/8-15-loader-element.html:
--------------------------------------------------------------------------------
1 |
5 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/08 - web-animations-api/8-8-loading-animation/8-16-loader-animation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 8-16. The loader animations
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 | async function showLoader(promise) {
6 | const loader = document.querySelector('#loader');
7 |
8 | // Start the spin animation before fading in
9 | const spin = loader.animate([
10 | { transform: 'rotate(0deg)' },
11 | { transform: 'rotate(360deg)' }
12 | ], { duration: 1000, iterations: Infinity });
13 |
14 | // Since the opacity is 0, the loader isn't visible yet.
15 | // Show it with a fade in animation.
16 | // The loader will continue spinning as it fades in.
17 | loader.animate([
18 | { opacity: 0 },
19 | { opacity: 1 }
20 | ], { duration: 500, fill: 'both' });
21 |
22 | // Wait for the Promise to resolve
23 | await promise;
24 |
25 | // The Promise is done. Now, fade the loader out.
26 | // Don't stop the spin animation until the fade out is complete.
27 | // You can wait by awaiting the `finished` Promise.
28 | await loader.animate([
29 | { opacity: 1 },
30 | { opacity: 0 }
31 | ], { duration: 500, fill: 'both' }).finished;
32 |
33 | // Finally, stop the spin animation.
34 | spin.cancel();
35 |
36 | // Return the original Promise to allow chaining.
37 | return promise;
38 | }
39 |
--------------------------------------------------------------------------------
/08 - web-animations-api/8-8-loading-animation/8-17-using-loader.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 8-17. Using the loader
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 | showLoader(
6 | fetch('https://example.com/api/users')
7 | .then(response => response.json())
8 | );
9 |
--------------------------------------------------------------------------------
/08 - web-animations-api/8-9-media-query/8-18-check-reduced-motion.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 8-18. Using the prefers-reduced-motion media query
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
7 | // Reduced motion is not enabled, animate normally
8 | } else {
9 | // Skip this animation, or run a less intense one
10 | }
11 |
--------------------------------------------------------------------------------
/09 - web-speech-api/9-1-add-dictation/9-01-add-dictation.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 9-1. Adding basic dictation to a text field
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Starts listening for speech. When speech is recognized, it is appended
8 | * to the given text field's value.
9 | * Recognition continues until the returned recognition object is stopped.
10 | *
11 | * @param textField A text field to append to
12 | * @returns The recognition object
13 | */
14 | function startDictation(textField) {
15 | // Only proceed if this browser supports speech recognition
16 | if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
17 | const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
18 | const recognition = new SpeechRecognition();
19 | recognition.continuous = true;
20 |
21 | recognition.addEventListener('result', event => {
22 | const result = event.results[event.resultIndex];
23 | textField.value += result[0].transcript;
24 | });
25 |
26 | recognition.addEventListener('error', event => {
27 | console.log('error', event);
28 | });
29 |
30 | recognition.start();
31 |
32 | // Return the recognition object so recognition
33 | // can be stopped later (like when the user clicks a toggle button).
34 | return recognition;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/09 - web-speech-api/9-2-promise-helper/9-02-promise-helper.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 9-2. A Promise helper for speech recognition
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Listens for speech and performs speech recognition.
8 | * Assumes that speech recognition is available in the current browser.
9 | * @returns a Promise that is resolved with the recognized transcript when speech is recognized, and rejects on an error
10 | */
11 | function captureSpeech() {
12 | const speechPromise = new Promise((resolve, reject) => {
13 | const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
14 |
15 | // If this browser doesn't support speech recognition, reject the Promise
16 | if (!SpeechRecognition) {
17 | reject('Speech recognition is not supported on this browser.')
18 | }
19 |
20 | const recognition = new SpeechRecognition();
21 |
22 | // Resolve the promise on successful speech recognition
23 | recognition.addEventListener('result', event => {
24 | const result = event.results[event.resultIndex];
25 | resolve(result[0].transcript);
26 | });
27 |
28 | recognition.addEventListener('error', event => {
29 | // Reject the promise if there was a recognition error
30 | reject(event);
31 | });
32 |
33 | // Start listening for speech
34 | recognition.start();
35 | });
36 |
37 | // Whether there was successful speech recognition or an error, make sure
38 | // the recognition engine has stopped listening.
39 | return speechPromise.finally(() => {
40 | recognition.stop();
41 | });
42 | }
43 |
--------------------------------------------------------------------------------
/09 - web-speech-api/9-2-promise-helper/9-03-using-helper.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 9-3. Using the captureSpeech helper
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const spokenText = await captureSpeech();
7 |
--------------------------------------------------------------------------------
/09 - web-speech-api/9-3-voices/9-04-get-voices.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 9-4. Getting the list of available speech synthesis voices
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | function showVoices() {
7 | voices.innerHTML = '';
8 | speechSynthesis.getVoices().forEach(voice => {
9 | console.log('Voice:', voice.name);
10 | });
11 | }
12 |
13 | speechSynthesis.addEventListener('voiceschanged', () => showVoices());
14 | showVoices();
15 |
--------------------------------------------------------------------------------
/09 - web-speech-api/9-4-synthesis/9-05-synthesis.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 9-5. Speaking some text with the Web Speech API
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | function speakText(text) {
7 | const utterance = new SpeechSynthesisUtterance(text);
8 | speechSynthesis.speak(utterance);
9 | }
10 |
--------------------------------------------------------------------------------
/09 - web-speech-api/9-4-synthesis/9-06-use-another-voice.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 9-6. Using another voice
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | // assuming the voices are available now
7 | const aliceVoice = speechSynthesis
8 | .getVoices()
9 | .find(voice => voice.name === 'Alice');
10 |
11 | function speakText(text) {
12 | const utterance = new SpeechSynthesisUtterance(text);
13 |
14 | // Make sure the "Alice" voice was found
15 | if (aliceVoice) {
16 | utterance.voice = aliceVoice;
17 | }
18 |
19 | speechSynthesis.speak(utterance);
20 | }
21 |
--------------------------------------------------------------------------------
/09 - web-speech-api/9-5-customizing-parameters/9-07-customizing-output.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 9-7. Customizing speech output
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | const utteranceLow = new SpeechSynthesisUtterance('This will be spoken slowly in a low tone');
7 | utterance.pitch = 0.1;
8 | utterance.rate = 0.5;
9 | speechSynthesis.speak(utterance);
10 |
11 | const utteranceHigh = new SpeechSynthesisUtterance('This will be spoken quickly in a high tone');
12 | utterance.pitch = 2;
13 | utterance.rate = 2;
14 | speechSynthesis.speak(utterance);
15 |
--------------------------------------------------------------------------------
/09 - web-speech-api/9-6-auto-pause/9-08-auto-pause.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 9-8. Pausing speech when the page becomes hidden
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | document.addEventListener('visibilitychange', () => {
7 | // speechSynthesis.speaking will be true:
8 | // (1) when speech is currently being spoken
9 | // (2) when speech was being spoken, but is paused
10 | if (speechSynthesis.speaking) {
11 | if (document.visibilityState === 'hidden') {
12 | speechSynthesis.pause();
13 | } else if (document.visibilityState === 'visible') {
14 | speechSynthesis.resume();
15 | }
16 | }
17 | });
18 |
--------------------------------------------------------------------------------
/10 - files/10-1-load-text/10-01-file-input.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/10 - files/10-1-load-text/10-02-load-text.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 10-2. Loading plain text from a file
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Reads the text content of a file.
8 | * @param file the File object containing the data to be read
9 | * @param onSuccess a function to call when the data is available
10 | */
11 | function readFileContent(file, onSuccess) {
12 | const reader = new FileReader();
13 |
14 | // When the content is loaded, the reader will emit a
15 | // `load` event
16 | reader.addEventListener('load', event => {
17 | onSuccess(event.target.result);
18 | });
19 |
20 | // Always handle errors!
21 | reader.addEventListener('error', event => {
22 | console.error('Error reading file:', event);
23 | });
24 |
25 | // Start the file read operation.
26 | reader.readAsText(file);
27 | }
28 |
29 | const fileInput = document.querySelector('#select-file');
30 |
31 | // The input fires a `change` event when a file is selected
32 | fileInput.addEventListener('change', event => {
33 | // This is an array, because a file input can be used to select
34 | // multiple files. Here, there's only once file selected.
35 | // This is using array destructuring syntax to get the first file.
36 | const [file] = fileInput.files;
37 |
38 | readFileContent(file, content => {
39 | // The file's text content is now available.
40 | // Imagine you have a textarea element you want to set the text in.
41 | const textArea = document.querySelector('.file-content-textarea');
42 | textArea.textContent = content;
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/10 - files/10-1-load-text/10-03-promise.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 10-3. Promisified readFileContent function
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | function readFileContent(file) {
7 | const reader = new FileReader();
8 |
9 | return new Promise((resolve, reject) => {
10 | reader.addEventListener('load', event => {
11 | resolve(event.target.result);
12 | });
13 |
14 | reader.addEventListener('error', reject);
15 |
16 | reader.readAsText(file);
17 | });
18 | }
19 |
20 | try {
21 | const content = await readFileContent(inputFile);
22 | const textArea = document.querySelector('.file-content-textarea');
23 | textArea.textContent = content;
24 | } catch (error) {
25 | console.error('Error reading file content:', error);
26 | }
27 |
--------------------------------------------------------------------------------
/10 - files/10-2-load-image/10-04-input.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
11 |
12 |
--------------------------------------------------------------------------------
/10 - files/10-2-load-image/10-05-load-image.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example 10-5. Loading an image into the page
3 | * From "Web Browser API Cookbook" by Joe Attardi
4 | */
5 |
6 | /**
7 | * Loads and shows an image from a file.
8 | * @param file the File object containing the image data
9 | * @param imageElement A placeholder Image element that will
10 | * show the image data.
11 | */
12 | function showImageFile(file, imageElement) {
13 | const reader = new FileReader();
14 |
15 | reader.addEventListener('load', event => {
16 | // Set the data URL directly as the image's
17 | // `src` attribute to load the image.
18 | imageElement.src = event.target.result;
19 | });
20 |
21 | reader.addEventListener('error', event => {
22 | console.log('error', event);
23 | });
24 |
25 | reader.readAsDataURL(file);
26 | }
27 |
28 | const fileInput = document.querySelector('#select-file');
29 | fileInput.addEventListener('change', event => {
30 | showImageFile(
31 | fileInput.files[0],
32 | document.querySelector('#placeholder-image')
33 | );
34 | });
35 |
--------------------------------------------------------------------------------
/10 - files/10-3-load-video/10-06-video-player.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
11 |
12 |