├── .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 |
6 | 7 |
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 |