Web APIs and Browser Integration
1. Fetch API and HTTP Request Handling
Fetch API Basics
| Method | Syntax | Returns |
|---|---|---|
| fetch() | fetch(url, options?) | Promise<Response> |
| response.json() | response.json() | Promise<any> |
| response.text() | response.text() | Promise<string> |
| response.blob() | response.blob() | Promise<Blob> |
| response.arrayBuffer() | response.arrayBuffer() | Promise<ArrayBuffer> |
| response.formData() | response.formData() | Promise<FormData> |
Response Properties
| Property | Type | Description |
|---|---|---|
| response.ok | boolean | True if status 200-299 |
| response.status | number | HTTP status code (200, 404, etc.) |
| response.statusText | string | Status message ("OK", "Not Found") |
| response.headers | Headers | Response headers |
| response.url | string | Final URL (after redirects) |
| response.redirected | boolean | True if response redirected |
| response.type | string | "basic", "cors", "opaque", etc. |
Request Options
| Option | Type | Description |
|---|---|---|
| method | string | "GET", "POST", "PUT", "DELETE", etc. |
| headers | object | Request headers |
| body | string|FormData|Blob | Request body (not for GET/HEAD) |
| mode | string | "cors", "no-cors", "same-origin" |
| credentials | string | "omit", "same-origin", "include" |
| cache | string | "default", "no-cache", "reload", etc. |
| signal | AbortSignal | For request cancellation |
HTTP Status Code Categories
| Range | Category | Common Codes |
|---|---|---|
| 100-199 | Informational | 100 Continue, 101 Switching Protocols |
| 200-299 | Success | 200 OK, 201 Created, 204 No Content |
| 300-399 | Redirection | 301 Moved, 302 Found, 304 Not Modified |
| 400-499 | Client Error | 400 Bad Request, 401 Unauthorized, 404 Not Found |
| 500-599 | Server Error | 500 Internal Error, 503 Service Unavailable |
Example: Basic fetch requests
// GET request
fetch('https://api.example.com/users')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Fetch error:', error);
});
// Async/await version
async function getUsers() {
try {
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching users:', error);
throw error;
}
}
// POST request with JSON
async function createUser(userData) {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify(userData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return response.json();
}
// Usage
await createUser({
name: 'John Doe',
email: 'john@example.com'
});
// PUT request
async function updateUser(id, updates) {
const response = await fetch(`https://api.example.com/users/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(updates)
});
return response.json();
}
// DELETE request
async function deleteUser(id) {
const response = await fetch(`https://api.example.com/users/${id}`, {
method: 'DELETE'
});
if (response.status === 204) {
return {success: true};
}
return response.json();
}
// Upload file with FormData
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'My file');
const response = await fetch('https://api.example.com/upload', {
method: 'POST',
body: formData // Don't set Content-Type header - browser sets it
});
return response.json();
}
// Request with timeout
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
Example: Advanced fetch patterns
// Retry logic
async function fetchWithRetry(url, options = {}, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return response;
}
// Don't retry client errors (4xx)
if (response.status >= 400 && response.status < 500) {
throw new Error(`Client error: ${response.status}`);
}
// Retry on server errors (5xx)
if (i === retries - 1) {
throw new Error(`Max retries reached. Status: ${response.status}`);
}
// Wait before retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
} catch (error) {
if (i === retries - 1) {
throw error;
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
}
}
}
// Parallel requests
async function fetchMultiple(urls) {
const promises = urls.map(url => fetch(url).then(r => r.json()));
return Promise.all(promises);
}
// Usage
const [users, posts, comments] = await fetchMultiple([
'https://api.example.com/users',
'https://api.example.com/posts',
'https://api.example.com/comments'
]);
// Sequential requests (when one depends on another)
async function fetchUserAndPosts(userId) {
const user = await fetch(`/api/users/${userId}`).then(r => r.json());
const posts = await fetch(`/api/users/${userId}/posts`).then(r => r.json());
return {user, posts};
}
// Progress tracking for upload
async function uploadWithProgress(file, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
onProgress(percentComplete);
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`Upload failed: ${xhr.status}`));
}
});
xhr.addEventListener('error', () => reject(new Error('Upload error')));
const formData = new FormData();
formData.append('file', file);
xhr.open('POST', '/api/upload');
xhr.send(formData);
});
}
// Usage
await uploadWithProgress(file, (percent) => {
console.log(`Upload progress: ${percent.toFixed(2)}%`);
});
// API client class
class ApiClient {
constructor(baseURL, defaultHeaders = {}) {
this.baseURL = baseURL;
this.defaultHeaders = defaultHeaders;
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
...options,
headers: {
...this.defaultHeaders,
...options.headers
}
};
const response = await fetch(url, config);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `HTTP error! status: ${response.status}`);
}
// Handle no-content responses
if (response.status === 204) {
return null;
}
return response.json();
}
get(endpoint, options) {
return this.request(endpoint, {...options, method: 'GET'});
}
post(endpoint, data, options) {
return this.request(endpoint, {
...options,
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
}
put(endpoint, data, options) {
return this.request(endpoint, {
...options,
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
}
delete(endpoint, options) {
return this.request(endpoint, {...options, method: 'DELETE'});
}
}
// Usage
const api = new ApiClient('https://api.example.com', {
'Authorization': 'Bearer token123'
});
const users = await api.get('/users');
const newUser = await api.post('/users', {name: 'John'});
await api.delete(`/users/${newUser.id}`);
Key Points:
fetch() returns Promise<Response>. Check
response.ok for success (status 200-299). Use response.json() to parse JSON.
AbortController for request cancellation. FormData for file uploads. Set headers for authentication
and content type. Handle errors with try-catch. Always check response.ok before
parsing body.
2. DOM Manipulation and Element Selection
Element Selection Methods
| Method | Returns | Description |
|---|---|---|
| document.getElementById() | Element | null | Select by ID |
| document.querySelector() | Element | null | First match of CSS selector |
| document.querySelectorAll() | NodeList | All matches of CSS selector |
| document.getElementsByClassName() | HTMLCollection (live) | Elements by class name |
| document.getElementsByTagName() | HTMLCollection (live) | Elements by tag name |
| element.closest() | Element | null | Nearest ancestor matching selector |
| element.matches() | boolean | Check if element matches selector |
Element Creation and Modification
| Method | Purpose | Usage |
|---|---|---|
| document.createElement() | Create element | createElement('div') |
| document.createTextNode() | Create text node | createTextNode('text') |
| element.cloneNode() | Clone element | cloneNode(true) for deep clone |
| element.appendChild() | Add child at end | parent.appendChild(child) |
| element.insertBefore() | Insert before reference | parent.insertBefore(new, ref) |
| element.removeChild() | Remove child | parent.removeChild(child) |
| element.remove() | Remove self | element.remove() |
| element.replaceChild() | Replace child | parent.replaceChild(new, old) |
Modern DOM Insertion Methods
| Method | Position | Accepts |
|---|---|---|
| element.append() | Inside, at end | Nodes or strings |
| element.prepend() | Inside, at start | Nodes or strings |
| element.before() | Before element (sibling) | Nodes or strings |
| element.after() | After element (sibling) | Nodes or strings |
| element.replaceWith() | Replace element | Nodes or strings |
Element Properties and Attributes
| Property/Method | Purpose | Example |
|---|---|---|
| element.innerHTML | Get/set HTML content | div.innerHTML = '<p>text</p>' |
| element.textContent | Get/set text (no HTML parsing) | div.textContent = 'text' |
| element.innerText | Get/set rendered text | div.innerText = 'text' |
| element.getAttribute() | Get attribute value | img.getAttribute('src') |
| element.setAttribute() | Set attribute | img.setAttribute('src', 'url') |
| element.removeAttribute() | Remove attribute | div.removeAttribute('class') |
| element.hasAttribute() | Check if attribute exists | div.hasAttribute('data-id') |
| element.dataset | Access data-* attributes | div.dataset.userId |
CSS Class Manipulation
| Method | Purpose | Example |
|---|---|---|
| element.classList.add() | Add class(es) | div.classList.add('active', 'highlighted') |
| element.classList.remove() | Remove class(es) | div.classList.remove('active') |
| element.classList.toggle() | Toggle class | div.classList.toggle('hidden') |
| element.classList.contains() | Check if has class | div.classList.contains('active') |
| element.classList.replace() | Replace class | div.classList.replace('old', 'new') |
| element.className | Get/set all classes (string) | div.className = 'class1 class2' |
Example: DOM selection and manipulation
// Element selection
const header = document.getElementById('header');
const firstButton = document.querySelector('.btn');
const allButtons = document.querySelectorAll('.btn');
const items = document.getElementsByClassName('item');
// Modern selection with optional chaining
const content = document.querySelector('.container')?.querySelector('.content');
// Creating elements
const div = document.createElement('div');
div.className = 'card';
div.id = 'user-card';
div.textContent = 'User Card';
// Setting attributes
div.setAttribute('data-user-id', '123');
div.setAttribute('role', 'article');
// Using dataset for data attributes
div.dataset.userId = '123'; // Creates data-user-id
div.dataset.userName = 'John'; // Creates data-user-name
console.log(div.dataset.userId); // '123'
// Creating complex structures
const card = document.createElement('div');
card.className = 'card';
const title = document.createElement('h3');
title.textContent = 'Card Title';
const content2 = document.createElement('p');
content2.textContent = 'Card content...';
const button = document.createElement('button');
button.textContent = 'Click Me';
button.className = 'btn btn-primary';
card.append(title, content2, button);
document.body.appendChild(card);
// Inserting elements
const container = document.querySelector('.container');
// Insert at end
container.append('Some text', div);
// Insert at start
container.prepend('First content');
// Insert before/after
const reference = document.querySelector('.reference');
reference.before(document.createElement('div'));
reference.after(document.createElement('div'));
// Replace element
const oldElement = document.querySelector('.old');
const newElement = document.createElement('div');
newElement.textContent = 'New element';
oldElement.replaceWith(newElement);
// Removing elements
const element = document.querySelector('.to-remove');
element.remove(); // Modern way
// Or
element.parentNode.removeChild(element); // Old way
// Clone elements
const original = document.querySelector('.original');
const clone = original.cloneNode(true); // Deep clone
document.body.appendChild(clone);
// Class manipulation
const box = document.querySelector('.box');
box.classList.add('active');
box.classList.remove('inactive');
box.classList.toggle('visible');
if (box.classList.contains('active')) {
console.log('Box is active');
}
// Toggle with force parameter
box.classList.toggle('active', true); // Always add
box.classList.toggle('active', false); // Always remove
// Replace class
box.classList.replace('old-class', 'new-class');
// Working with innerHTML vs textContent
const container2 = document.querySelector('.container');
// innerHTML - parses HTML
container2.innerHTML = '<strong>Bold text</strong>';
// textContent - treats as plain text (safer, faster)
container2.textContent = '<strong>Not parsed</strong>';
// Finding elements relative to another
const parent = document.querySelector('.parent');
const child = parent.querySelector('.child');
const ancestor = child.closest('.ancestor');
const sibling = child.nextElementSibling;
const prevSibling = child.previousElementSibling;
// Check if element matches selector
if (element.matches('.active')) {
console.log('Element is active');
}
Example: Efficient DOM manipulation
// BAD: Multiple reflows
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
document.body.appendChild(div); // Causes reflow each time
}
// GOOD: Use DocumentFragment
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
fragment.appendChild(div);
}
document.body.appendChild(fragment); // Single reflow
// GOOD: Build HTML string and insert once
const items = Array.from({length: 100}, (_, i) => `
<div class="item">Item ${i}</div>
`).join('');
document.querySelector('.container').innerHTML = items;
// Helper function to create elements with properties
function createElement(tag, props = {}, children = []) {
const element = document.createElement(tag);
// Set properties
Object.entries(props).forEach(([key, value]) => {
if (key === 'className') {
element.className = value;
} else if (key === 'dataset') {
Object.assign(element.dataset, value);
} else if (key === 'style') {
Object.assign(element.style, value);
} else if (key.startsWith('on')) {
const event = key.slice(2).toLowerCase();
element.addEventListener(event, value);
} else {
element.setAttribute(key, value);
}
});
// Add children
children.forEach(child => {
if (typeof child === 'string') {
element.appendChild(document.createTextNode(child));
} else {
element.appendChild(child);
}
});
return element;
}
// Usage
const button = createElement('button', {
className: 'btn btn-primary',
dataset: {userId: '123'},
onclick: () => console.log('Clicked'),
type: 'button'
}, ['Click Me']);
// Batch DOM reads and writes
// BAD: Interleaving reads and writes
elements.forEach(el => {
const height = el.offsetHeight; // Read
el.style.height = height + 10 + 'px'; // Write
});
// GOOD: Batch all reads, then all writes
const heights = elements.map(el => el.offsetHeight); // All reads
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px'; // All writes
});
// Template element for reusable DOM structures
const template = document.createElement('template');
template.innerHTML = `
<div class="card">
<h3 class="card-title"></h3>
<p class="card-body"></p>
<button class="card-btn">Action</button>
</div>
`;
function createCard(title, body) {
const clone = template.content.cloneNode(true);
clone.querySelector('.card-title').textContent = title;
clone.querySelector('.card-body').textContent = body;
return clone;
}
document.body.appendChild(createCard('Title', 'Body text'));
// Observing DOM changes
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
console.log('Type:', mutation.type);
console.log('Added:', mutation.addedNodes);
console.log('Removed:', mutation.removedNodes);
});
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeOldValue: true
});
// Stop observing
observer.disconnect();
Key Points: Use
querySelector() for flexible selection with CSS selectors.
querySelectorAll() returns static NodeList. classList for class
manipulation. textContent is safer than innerHTML. Use DocumentFragment for batch
inserts. Modern methods: append(), prepend(), before(),
after(), remove(). Access data attributes via element.dataset.
3. Event System and Event Delegation
Event Registration Methods
| Method | Syntax | Notes |
|---|---|---|
| addEventListener() | element.addEventListener(type, handler, options) | Recommended, multiple handlers allowed |
| removeEventListener() | element.removeEventListener(type, handler, options) | Remove specific handler |
| element.onclick | element.onclick = handler | Only one handler, overwrites previous |
| HTML attribute | <button onclick="handler()"> | Not recommended |
Event Listener Options
| Option | Type | Description |
|---|---|---|
| capture | boolean | Use capture phase (default: false) |
| once | boolean | Remove after first invocation |
| passive | boolean | Won't call preventDefault() (improves scroll performance) |
| signal | AbortSignal | Remove listener when signal aborted |
Common Event Types
| Category | Events | When Fired |
|---|---|---|
| Mouse | click, dblclick, mousedown, mouseup, mousemove, mouseenter, mouseleave | Mouse interactions |
| Keyboard | keydown, keyup, keypress (deprecated) | Keyboard input |
| Form | submit, change, input, focus, blur, invalid | Form interactions |
| Focus | focus, blur, focusin, focusout | Element focus changes |
| Touch | touchstart, touchmove, touchend, touchcancel | Touch device interactions |
| Drag | dragstart, drag, dragend, dragover, drop | Drag and drop |
| Document | DOMContentLoaded, load, beforeunload, unload | Document lifecycle |
| Clipboard | copy, cut, paste | Clipboard operations |
Event Object Properties
| Property | Type | Description |
|---|---|---|
| event.type | string | Event type ("click", "keydown", etc.) |
| event.target | Element | Element that triggered event |
| event.currentTarget | Element | Element with listener attached |
| event.bubbles | boolean | Whether event bubbles |
| event.cancelable | boolean | Whether can be cancelled |
| event.defaultPrevented | boolean | Whether preventDefault called |
| event.timeStamp | number | Time event created (ms) |
| event.isTrusted | boolean | True if user-generated |
Event Control Methods
| Method | Purpose | Effect |
|---|---|---|
| event.preventDefault() | Cancel default action | Prevents default browser behavior |
| event.stopPropagation() | Stop bubbling | Prevents event from propagating |
| event.stopImmediatePropagation() | Stop all propagation | Stops propagation and remaining handlers |
Example: Event handling basics
// Basic event listener
const button = document.querySelector('.btn');
button.addEventListener('click', (event) => {
console.log('Button clicked!');
console.log('Target:', event.target);
console.log('Current target:', event.currentTarget);
});
// Event listener with options
button.addEventListener('click', handler, {
once: true, // Remove after first call
capture: false, // Bubbling phase
passive: true // Won't call preventDefault
});
// Remove event listener
function handleClick(event) {
console.log('Clicked');
}
button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick);
// Using AbortController to remove listeners
const controller = new AbortController();
button.addEventListener('click', () => {
console.log('Clicked');
}, {signal: controller.signal});
// Remove listener
controller.abort();
// Multiple elements
const buttons = document.querySelectorAll('.btn');
buttons.forEach(button => {
button.addEventListener('click', (e) => {
console.log('Button clicked:', e.target.textContent);
});
});
// Prevent default behavior
const link = document.querySelector('a');
link.addEventListener('click', (event) => {
event.preventDefault(); // Don't navigate
console.log('Link clicked but not followed');
});
// Form submission
const form = document.querySelector('form');
form.addEventListener('submit', (event) => {
event.preventDefault(); // Don't submit to server
const formData = new FormData(form);
const data = Object.fromEntries(formData);
console.log('Form data:', data);
// Handle form submission with fetch
fetch('/api/submit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
});
// Keyboard events
const input = document.querySelector('input');
input.addEventListener('keydown', (event) => {
console.log('Key:', event.key);
console.log('Code:', event.code);
console.log('Ctrl:', event.ctrlKey);
console.log('Shift:', event.shiftKey);
console.log('Alt:', event.altKey);
// Check for specific key
if (event.key === 'Enter') {
console.log('Enter pressed');
}
// Check for key combination
if (event.ctrlKey && event.key === 's') {
event.preventDefault(); // Prevent browser save
console.log('Ctrl+S pressed');
}
});
// Input event (fires on every change)
input.addEventListener('input', (event) => {
console.log('Current value:', event.target.value);
});
// Change event (fires when input loses focus)
input.addEventListener('change', (event) => {
console.log('Final value:', event.target.value);
});
// Mouse events
const box = document.querySelector('.box');
box.addEventListener('mouseenter', () => {
console.log('Mouse entered');
});
box.addEventListener('mouseleave', () => {
console.log('Mouse left');
});
box.addEventListener('mousemove', (event) => {
console.log('Mouse position:', event.clientX, event.clientY);
});
// Touch events
box.addEventListener('touchstart', (event) => {
console.log('Touch started');
console.log('Touches:', event.touches.length);
});
box.addEventListener('touchmove', (event) => {
event.preventDefault(); // Prevent scrolling
const touch = event.touches[0];
console.log('Touch position:', touch.clientX, touch.clientY);
}, {passive: false}); // Must be false to call preventDefault
// Document ready
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM fully loaded');
});
// Page load (including images, styles)
window.addEventListener('load', () => {
console.log('Page fully loaded');
});
// Before page unload
window.addEventListener('beforeunload', (event) => {
event.preventDefault();
event.returnValue = ''; // Show confirmation dialog
});
Example: Event delegation
// Event delegation - handle events on parent
const list = document.querySelector('.list');
// BAD: Add listener to each item (inefficient for many items)
document.querySelectorAll('.list-item').forEach(item => {
item.addEventListener('click', handleClick);
});
// GOOD: Single listener on parent (event delegation)
list.addEventListener('click', (event) => {
// Find closest list item
const item = event.target.closest('.list-item');
if (item) {
console.log('Item clicked:', item.textContent);
item.classList.toggle('selected');
}
});
// More complex delegation
list.addEventListener('click', (event) => {
const target = event.target;
// Handle delete button
if (target.matches('.delete-btn')) {
const item = target.closest('.list-item');
item.remove();
}
// Handle edit button
if (target.matches('.edit-btn')) {
const item = target.closest('.list-item');
editItem(item);
}
// Handle item click (but not buttons)
if (target.matches('.list-item') && !target.closest('button')) {
toggleItem(target);
}
});
// Delegation helper function
function delegate(parent, selector, eventType, handler) {
parent.addEventListener(eventType, (event) => {
const target = event.target.closest(selector);
if (target && parent.contains(target)) {
handler.call(target, event);
}
});
}
// Usage
delegate(list, '.list-item', 'click', function(event) {
console.log('Item clicked:', this.textContent);
});
// Dynamic content with delegation
function addItem(text) {
const item = document.createElement('div');
item.className = 'list-item';
item.innerHTML = `
<span>${text}</span>
<button class="delete-btn">Delete</button>
`;
list.appendChild(item);
// No need to add new event listeners!
}
// Event bubbling example
document.querySelector('.grandparent').addEventListener('click', () => {
console.log('Grandparent clicked');
});
document.querySelector('.parent').addEventListener('click', () => {
console.log('Parent clicked');
});
document.querySelector('.child').addEventListener('click', (event) => {
console.log('Child clicked');
// Stop propagation to parent
// event.stopPropagation();
});
// Clicking child logs: "Child clicked", "Parent clicked", "Grandparent clicked"
// Event capture phase
document.querySelector('.grandparent').addEventListener('click', () => {
console.log('Grandparent (capture)');
}, true); // Capture phase
document.querySelector('.parent').addEventListener('click', () => {
console.log('Parent (capture)');
}, true);
document.querySelector('.child').addEventListener('click', () => {
console.log('Child (bubble)');
});
// Order: "Grandparent (capture)", "Parent (capture)", "Child (bubble)"
// Custom event dispatcher
class EventBus {
constructor() {
this.events = {};
}
on(event, handler) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(handler);
}
off(event, handler) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(h => h !== handler);
}
emit(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(handler => {
handler(data);
});
}
}
// Usage
const bus = new EventBus();
bus.on('user:login', (user) => {
console.log('User logged in:', user);
});
bus.emit('user:login', {name: 'John', id: 123});
// Custom DOM events
const customEvent = new CustomEvent('myevent', {
detail: {message: 'Hello'},
bubbles: true,
cancelable: true
});
element.addEventListener('myevent', (event) => {
console.log('Custom event:', event.detail);
});
element.dispatchEvent(customEvent);
Key Points: Use
addEventListener() for flexibility. Event
delegation: attach listener to parent, check event.target. Use event.preventDefault() to
cancel default action. event.stopPropagation() stops bubbling. once: true for
one-time listeners. AbortController for cleanup. passive: true for scroll performance.
Event phases: capture → target → bubble.
4. Storage APIs (localStorage, sessionStorage, IndexedDB)
Web Storage API (localStorage & sessionStorage)
| Method | Purpose | Returns |
|---|---|---|
| setItem(key, value) | Store key-value pair | undefined |
| getItem(key) | Retrieve value | string | null |
| removeItem(key) | Remove item | undefined |
| clear() | Remove all items | undefined |
| key(index) | Get key at index | string | null |
| length | Number of items | number |
localStorage vs sessionStorage
| Feature | localStorage | sessionStorage |
|---|---|---|
| Persistence | Until manually cleared | Until tab/window closed |
| Scope | Shared across tabs | Per tab/window |
| Storage limit | ~5-10 MB | ~5-10 MB |
| Data type | Strings only | Strings only |
| Use case | Long-term data, preferences | Session data, temporary state |
IndexedDB Key Concepts
| Concept | Description | Example |
|---|---|---|
| Database | Container for object stores | indexedDB.open('myDB', 1) |
| Object Store | Like a table in SQL | db.createObjectStore('users') |
| Transaction | Atomic operation wrapper | db.transaction(['users'], 'readwrite') |
| Index | Query by non-key property | store.createIndex('email', 'email') |
| Key | Unique identifier | autoIncrement or keyPath |
| Cursor | Iterate over records | store.openCursor() |
IndexedDB Transaction Modes
| Mode | Access | When to Use |
|---|---|---|
| readonly | Read only | Queries, retrieving data |
| readwrite | Read and write | Creating, updating, deleting |
| versionchange | Schema changes | onupgradeneeded event |
Example: localStorage and sessionStorage
// localStorage - persists across sessions
// Store data
localStorage.setItem('username', 'john_doe');
localStorage.setItem('theme', 'dark');
// Retrieve data
const username = localStorage.getItem('username'); // 'john_doe'
const theme = localStorage.getItem('theme'); // 'dark'
// Store objects (must stringify)
const user = {
id: 123,
name: 'John Doe',
email: 'john@example.com'
};
localStorage.setItem('user', JSON.stringify(user));
// Retrieve and parse
const savedUser = JSON.parse(localStorage.getItem('user'));
console.log(savedUser.name); // 'John Doe'
// Remove item
localStorage.removeItem('theme');
// Clear all
localStorage.clear();
// Check if key exists
if (localStorage.getItem('username')) {
console.log('Username exists');
}
// Iterate over all items
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
console.log(key, value);
}
// sessionStorage - cleared when tab closes
sessionStorage.setItem('tempData', 'temporary');
// Storage helper class
class Storage {
constructor(storage = localStorage) {
this.storage = storage;
}
set(key, value) {
try {
this.storage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
console.error('Storage error:', error);
return false;
}
}
get(key, defaultValue = null) {
try {
const item = this.storage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
console.error('Parse error:', error);
return defaultValue;
}
}
remove(key) {
this.storage.removeItem(key);
}
clear() {
this.storage.clear();
}
has(key) {
return this.storage.getItem(key) !== null;
}
keys() {
return Object.keys(this.storage);
}
}
// Usage
const store = new Storage();
store.set('user', {name: 'John', age: 30});
const user2 = store.get('user');
console.log(user2.name); // 'John'
// Listen for storage changes (in other tabs)
window.addEventListener('storage', (event) => {
console.log('Storage changed:');
console.log('Key:', event.key);
console.log('Old value:', event.oldValue);
console.log('New value:', event.newValue);
console.log('URL:', event.url);
});
// Storage with expiration
class StorageWithExpiry {
set(key, value, ttl) {
const item = {
value: value,
expiry: Date.now() + ttl
};
localStorage.setItem(key, JSON.stringify(item));
}
get(key) {
const itemStr = localStorage.getItem(key);
if (!itemStr) {
return null;
}
const item = JSON.parse(itemStr);
if (Date.now() > item.expiry) {
localStorage.removeItem(key);
return null;
}
return item.value;
}
}
// Usage
const storage = new StorageWithExpiry();
// Store for 1 hour
storage.set('session', {token: 'abc123'}, 60 * 60 * 1000);
// Later...
const session = storage.get('session'); // null if expired
// Handle quota exceeded errors
try {
localStorage.setItem('large-data', 'x'.repeat(10 * 1024 * 1024));
} catch (error) {
if (error.name === 'QuotaExceededError') {
console.error('Storage quota exceeded');
// Clear old data or notify user
}
}
Example: IndexedDB basic operations
// Open database
function openDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('myDatabase', 1);
// Create/upgrade schema
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create object store
if (!db.objectStoreNames.contains('users')) {
const store = db.createObjectStore('users', {
keyPath: 'id',
autoIncrement: true
});
// Create indexes
store.createIndex('email', 'email', {unique: true});
store.createIndex('name', 'name', {unique: false});
}
};
request.onsuccess = (event) => {
resolve(event.target.result);
};
request.onerror = (event) => {
reject(event.target.error);
};
});
}
// Add data
async function addUser(user) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.add(user);
request.onsuccess = () => {
resolve(request.result); // Returns generated key
};
request.onerror = () => {
reject(request.error);
};
});
}
// Get data by key
async function getUser(id) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.get(id);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
// Get data by index
async function getUserByEmail(email) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const index = store.index('email');
const request = index.get(email);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
// Get all data
async function getAllUsers() {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.getAll();
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
// Update data
async function updateUser(user) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.put(user); // Updates or inserts
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
// Delete data
async function deleteUser(id) {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.delete(id);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(request.error);
};
});
}
// Usage
await addUser({name: 'John Doe', email: 'john@example.com'});
const user3 = await getUser(1);
const userByEmail = await getUserByEmail('john@example.com');
await updateUser({id: 1, name: 'John Smith', email: 'john@example.com'});
await deleteUser(1);
// Cursor iteration
async function iterateUsers() {
const db = await openDatabase();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.openCursor();
const results = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
results.push(cursor.value);
cursor.continue(); // Move to next
} else {
resolve(results); // Done
}
};
request.onerror = () => {
reject(request.error);
};
});
}
// IndexedDB wrapper class
class IDBWrapper {
constructor(dbName, version = 1) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
async open(upgrade) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onupgradeneeded = (event) => {
if (upgrade) {
upgrade(event.target.result);
}
};
request.onsuccess = (event) => {
this.db = event.target.result;
resolve(this.db);
};
request.onerror = () => reject(request.error);
});
}
async add(storeName, data) {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.add(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async get(storeName, key) {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getAll(storeName) {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async delete(storeName, key) {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.delete(key);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
}
// Usage
const db2 = new IDBWrapper('myApp', 1);
await db2.open((db) => {
db.createObjectStore('users', {keyPath: 'id', autoIncrement: true});
});
await db2.add('users', {name: 'John'});
const users = await db2.getAll('users');
Key Points:
localStorage persists, sessionStorage clears on tab
close.
Both store strings only - use JSON.stringify/parse for objects. IndexedDB for
large/complex data (NoSQL database). Use transactions for IndexedDB operations. Create indexes for efficient
queries. Handle QuotaExceededError. storage event for cross-tab communication.
5. Clipboard API and File API
Clipboard API Methods
| Method | Purpose | Returns |
|---|---|---|
| navigator.clipboard.writeText() | Copy text to clipboard | Promise<void> |
| navigator.clipboard.readText() | Read text from clipboard | Promise<string> |
| navigator.clipboard.write() | Copy data (images, HTML) to clipboard | Promise<void> |
| navigator.clipboard.read() | Read data from clipboard | Promise<ClipboardItem[]> |
File Input Properties
| Property | Type | Description |
|---|---|---|
| input.files | FileList | Selected files |
| input.accept | string | File type filter (e.g., "image/*") |
| input.multiple | boolean | Allow multiple file selection |
File Object Properties
| Property | Type | Description |
|---|---|---|
| file.name | string | File name |
| file.size | number | Size in bytes |
| file.type | string | MIME type (e.g., "image/png") |
| file.lastModified | number | Last modified timestamp |
FileReader Methods
| Method | Result Format | Use Case |
|---|---|---|
| readAsText() | String | Text files |
| readAsDataURL() | Data URL (base64) | Display images, download links |
| readAsArrayBuffer() | ArrayBuffer | Binary data, manipulation |
| readAsBinaryString() | Binary string (deprecated) | Legacy support |
Drag and Drop Events
| Event | Fires On | Usage |
|---|---|---|
| dragstart | Start dragging | Set drag data |
| drag | During drag | Track drag position |
| dragenter | Enter drop zone | Visual feedback |
| dragover | Over drop zone | Allow drop (preventDefault) |
| dragleave | Leave drop zone | Remove feedback |
| drop | Dropped | Handle dropped data |
| dragend | Drag finished | Cleanup |
Example: Clipboard API
// Copy text to clipboard
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
console.log('Copied to clipboard');
} catch (error) {
console.error('Failed to copy:', error);
}
}
// Usage
await copyToClipboard('Hello, World!');
// Read text from clipboard
async function readFromClipboard() {
try {
const text = await navigator.clipboard.readText();
console.log('Clipboard text:', text);
return text;
} catch (error) {
console.error('Failed to read:', error);
}
}
// Copy button
document.querySelector('.copy-btn').addEventListener('click', async () => {
const text = document.querySelector('.code').textContent;
await copyToClipboard(text);
});
// Copy image to clipboard
async function copyImageToClipboard(blob) {
try {
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob
})
]);
console.log('Image copied');
} catch (error) {
console.error('Failed to copy image:', error);
}
}
// Fetch and copy image
async function copyImageFromUrl(url) {
const response = await fetch(url);
const blob = await response.blob();
await copyImageToClipboard(blob);
}
// Fallback for older browsers
function fallbackCopy(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
const successful = document.execCommand('copy');
console.log('Copy:', successful ? 'success' : 'failed');
} catch (error) {
console.error('Fallback copy failed:', error);
}
document.body.removeChild(textarea);
}
// Copy with fallback
async function copyText(text) {
if (navigator.clipboard) {
await navigator.clipboard.writeText(text);
} else {
fallbackCopy(text);
}
}
// Listen for copy event
document.addEventListener('copy', (event) => {
// Customize copied content
const selection = document.getSelection().toString();
const customText = `${selection}\n\nSource: ${window.location.href}`;
event.clipboardData.setData('text/plain', customText);
event.preventDefault();
});
// Listen for paste event
document.addEventListener('paste', (event) => {
event.preventDefault();
const text = event.clipboardData.getData('text/plain');
console.log('Pasted:', text);
// Insert at cursor or handle paste
document.execCommand('insertText', false, text);
});
Example: File API - reading files
// HTML: <input type="file" id="fileInput" multiple accept="image/*">
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', (event) => {
const files = event.target.files;
for (const file of files) {
console.log('Name:', file.name);
console.log('Size:', file.size, 'bytes');
console.log('Type:', file.type);
console.log('Last modified:', new Date(file.lastModified));
}
});
// Read text file
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const text = e.target.result;
console.log('File content:', text);
};
reader.onerror = (e) => {
console.error('Error reading file:', e);
};
reader.readAsText(file);
}
});
// Read image and display
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const img = document.createElement('img');
img.src = e.target.result; // Data URL
document.body.appendChild(img);
};
reader.readAsDataURL(file);
}
});
// Read as ArrayBuffer for binary processing
fileInput.addEventListener('change', async (event) => {
const file = event.target.files[0];
if (file) {
const arrayBuffer = await file.arrayBuffer();
const bytes = new Uint8Array(arrayBuffer);
console.log('First 10 bytes:', bytes.slice(0, 10));
}
});
// Progress tracking
function readFileWithProgress(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onprogress = (event) => {
if (event.lengthComputable) {
const percent = (event.loaded / event.total) * 100;
console.log(`Progress: ${percent.toFixed(2)}%`);
}
};
reader.onload = (event) => {
resolve(event.target.result);
};
reader.onerror = (event) => {
reject(reader.error);
};
reader.readAsArrayBuffer(file);
});
}
// Multiple files
fileInput.addEventListener('change', async (event) => {
const files = Array.from(event.target.files);
const promises = files.map(file => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve({file, data: e.target.result});
reader.onerror = reject;
reader.readAsDataURL(file);
});
});
const results = await Promise.all(promises);
results.forEach(({file, data}) => {
console.log(`${file.name}: ${data.length} characters`);
});
});
// File validation
function validateFile(file) {
const maxSize = 5 * 1024 * 1024; // 5 MB
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (file.size > maxSize) {
throw new Error('File too large (max 5MB)');
}
if (!allowedTypes.includes(file.type)) {
throw new Error('Invalid file type');
}
return true;
}
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
try {
validateFile(file);
// Process file
} catch (error) {
alert(error.message);
fileInput.value = ''; // Clear input
}
});
Example: Drag and drop file upload
// HTML: <div id="dropZone">Drop files here</div>
const dropZone = document.getElementById('dropZone');
// Prevent default drag behaviors
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// Visual feedback
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, () => {
dropZone.classList.add('highlight');
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, () => {
dropZone.classList.remove('highlight');
}, false);
});
// Handle drop
dropZone.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
handleFiles(files);
}
function handleFiles(files) {
[...files].forEach(uploadFile);
}
function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
fetch('/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
console.log('Upload success:', data);
})
.catch(error => {
console.error('Upload error:', error);
});
}
// Preview dropped images
function handleDrop2(e) {
const dt = e.dataTransfer;
const files = dt.files;
[...files].forEach(file => {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const img = document.createElement('img');
img.src = e.target.result;
img.style.maxWidth = '200px';
dropZone.appendChild(img);
};
reader.readAsDataURL(file);
}
});
}
// Drag from element to another
const draggableItem = document.querySelector('.draggable');
draggableItem.addEventListener('dragstart', (e) => {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', e.target.innerHTML);
e.dataTransfer.setData('text/plain', e.target.textContent);
});
const dropTarget = document.querySelector('.drop-target');
dropTarget.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
dropTarget.addEventListener('drop', (e) => {
e.preventDefault();
const html = e.dataTransfer.getData('text/html');
dropTarget.innerHTML = html;
});
// File System Access API (modern browsers)
async function openFilePicker() {
try {
const [fileHandle] = await window.showOpenFilePicker({
types: [{
description: 'Text Files',
accept: {'text/plain': ['.txt']}
}],
multiple: false
});
const file = await fileHandle.getFile();
const text = await file.text();
console.log('File content:', text);
} catch (error) {
console.error('Error:', error);
}
}
// Save file
async function saveFile(content) {
try {
const fileHandle = await window.showSaveFilePicker({
suggestedName: 'document.txt',
types: [{
description: 'Text Files',
accept: {'text/plain': ['.txt']}
}]
});
const writable = await fileHandle.createWritable();
await writable.write(content);
await writable.close();
console.log('File saved');
} catch (error) {
console.error('Error:', error);
}
}
Key Points:
navigator.clipboard.writeText() for copying text (requires HTTPS).
Use FileReader to read file contents (text, dataURL, arrayBuffer). File
validation
(size, type) before upload. Drag and drop: preventDefault on dragover and drop. FormData for file uploads.
file.arrayBuffer() for modern async file reading. File System Access API for advanced file
operations.
6. Geolocation and Device APIs
Geolocation API Methods
| Method | Purpose | Callback Parameters |
|---|---|---|
| getCurrentPosition() | Get current location once | success, error, options |
| watchPosition() | Track location changes | success, error, options |
| clearWatch() | Stop watching position | watchId |
Position Object Properties
| Property | Type | Description |
|---|---|---|
| coords.latitude | number | Latitude in decimal degrees |
| coords.longitude | number | Longitude in decimal degrees |
| coords.accuracy | number | Accuracy in meters |
| coords.altitude | number | null | Altitude in meters |
| coords.altitudeAccuracy | number | null | Altitude accuracy in meters |
| coords.heading | number | null | Direction in degrees (0=North) |
| coords.speed | number | null | Speed in meters per second |
| timestamp | number | Timestamp of position |
Geolocation Options
| Option | Type | Description |
|---|---|---|
| enableHighAccuracy | boolean | Request best possible results (uses GPS) |
| timeout | number | Maximum time to wait (ms) |
| maximumAge | number | Max age of cached position (ms) |
Device Orientation Properties
| Property | Range | Description |
|---|---|---|
| alpha | 0-360 | Z-axis rotation (compass heading) |
| beta | -180 to 180 | X-axis rotation (front-to-back tilt) |
| gamma | -90 to 90 | Y-axis rotation (left-to-right tilt) |
| absolute | boolean | True if relative to Earth's coordinate frame |
Other Device APIs
| API | Purpose | Access Via |
|---|---|---|
| Battery Status | Battery level and charging status | navigator.getBattery() |
| Network Information | Connection type and speed | navigator.connection |
| Vibration | Trigger device vibration | navigator.vibrate() |
| Screen Orientation | Lock/unlock screen orientation | screen.orientation |
| Media Devices | Access camera/microphone | navigator.mediaDevices |
Example: Geolocation API
// Get current position
if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition(
(position) => {
console.log('Latitude:', position.coords.latitude);
console.log('Longitude:', position.coords.longitude);
console.log('Accuracy:', position.coords.accuracy, 'meters');
console.log('Timestamp:', new Date(position.timestamp));
},
(error) => {
console.error('Error:', error.code, error.message);
switch(error.code) {
case error.PERMISSION_DENIED:
console.log('User denied geolocation');
break;
case error.POSITION_UNAVAILABLE:
console.log('Location unavailable');
break;
case error.TIMEOUT:
console.log('Request timeout');
break;
}
},
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
}
);
} else {
console.log('Geolocation not supported');
}
// Async/await wrapper
function getPosition(options) {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, options);
});
}
// Usage
try {
const position = await getPosition({
enableHighAccuracy: true,
timeout: 10000
});
const {latitude, longitude} = position.coords;
console.log(`Location: ${latitude}, ${longitude}`);
} catch (error) {
console.error('Geolocation error:', error);
}
// Watch position (continuous tracking)
const watchId = navigator.geolocation.watchPosition(
(position) => {
const {latitude, longitude, speed, heading} = position.coords;
console.log(`Lat: ${latitude}, Lng: ${longitude}`);
if (speed !== null) {
console.log(`Speed: ${speed} m/s`);
}
if (heading !== null) {
console.log(`Heading: ${heading}°`);
}
// Update map or UI
updateMap(latitude, longitude);
},
(error) => {
console.error('Watch error:', error);
},
{
enableHighAccuracy: true,
maximumAge: 30000,
timeout: 27000
}
);
// Stop watching
navigator.geolocation.clearWatch(watchId);
// Calculate distance between two points (Haversine formula)
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371e3; // Earth's radius in meters
const φ1 = lat1 * Math.PI / 180;
const φ2 = lat2 * Math.PI / 180;
const Δφ = (lat2 - lat1) * Math.PI / 180;
const Δλ = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // Distance in meters
}
// Usage
const distance = calculateDistance(51.5074, -0.1278, 48.8566, 2.3522);
console.log(`Distance: ${(distance / 1000).toFixed(2)} km`);
// Geolocation with React hook
function useGeolocation(options) {
const [position, setPosition] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const watchId = navigator.geolocation.watchPosition(
setPosition,
setError,
options
);
return () => navigator.geolocation.clearWatch(watchId);
}, []);
return {position, error};
}
// Usage in component
function LocationTracker() {
const {position, error} = useGeolocation({enableHighAccuracy: true});
if (error) return <div>Error: {error.message}</div>;
if (!position) return <div>Loading...</div>;
return (
<div>
<p>Lat: {position.coords.latitude}</p>
<p>Lng: {position.coords.longitude}</p>
</div>
);
}
Example: Device orientation and other APIs
// Device orientation (gyroscope)
window.addEventListener('deviceorientation', (event) => {
const alpha = event.alpha; // Z-axis (0-360)
const beta = event.beta; // X-axis (-180 to 180)
const gamma = event.gamma; // Y-axis (-90 to 90)
console.log(`α: ${alpha}°, β: ${beta}°, γ: ${gamma}°`);
// Use for compass, level, or 3D effects
updateCompass(alpha);
});
// Device motion (accelerometer)
window.addEventListener('devicemotion', (event) => {
const acceleration = event.acceleration;
const rotationRate = event.rotationRate;
console.log('Acceleration:');
console.log(' x:', acceleration.x);
console.log(' y:', acceleration.y);
console.log(' z:', acceleration.z);
console.log('Rotation rate:');
console.log(' α:', rotationRate.alpha);
console.log(' β:', rotationRate.beta);
console.log(' γ:', rotationRate.gamma);
// Detect shake
const totalAcceleration = Math.abs(acceleration.x) +
Math.abs(acceleration.y) +
Math.abs(acceleration.z);
if (totalAcceleration > 30) {
console.log('Device shaken!');
}
});
// Request permission for iOS 13+
async function requestMotionPermission() {
if (typeof DeviceMotionEvent.requestPermission === 'function') {
const permission = await DeviceMotionEvent.requestPermission();
if (permission === 'granted') {
window.addEventListener('devicemotion', handleMotion);
}
}
}
// Battery API
navigator.getBattery().then((battery) => {
console.log('Battery level:', battery.level * 100, '%');
console.log('Charging:', battery.charging);
console.log('Charging time:', battery.chargingTime, 'seconds');
console.log('Discharging time:', battery.dischargingTime, 'seconds');
// Listen for changes
battery.addEventListener('levelchange', () => {
console.log('Battery level:', battery.level * 100, '%');
});
battery.addEventListener('chargingchange', () => {
console.log('Charging:', battery.charging);
});
});
// Network Information API
if ('connection' in navigator) {
const connection = navigator.connection;
console.log('Connection type:', connection.effectiveType);
console.log('Downlink speed:', connection.downlink, 'Mb/s');
console.log('RTT:', connection.rtt, 'ms');
console.log('Save data:', connection.saveData);
// Adjust quality based on connection
if (connection.effectiveType === '4g') {
loadHighQualityImages();
} else {
loadLowQualityImages();
}
// Listen for connection changes
connection.addEventListener('change', () => {
console.log('Connection changed to:', connection.effectiveType);
});
}
// Vibration API
if ('vibrate' in navigator) {
// Vibrate for 200ms
navigator.vibrate(200);
// Pattern: vibrate, pause, vibrate
navigator.vibrate([200, 100, 200]);
// Stop vibration
navigator.vibrate(0);
// Feedback on button click
button.addEventListener('click', () => {
navigator.vibrate(50);
});
}
// Screen Orientation API
if ('orientation' in screen) {
console.log('Current orientation:', screen.orientation.type);
console.log('Angle:', screen.orientation.angle);
// Lock orientation
screen.orientation.lock('landscape').then(() => {
console.log('Orientation locked');
}).catch((error) => {
console.error('Lock failed:', error);
});
// Unlock orientation
screen.orientation.unlock();
// Listen for orientation changes
screen.orientation.addEventListener('change', () => {
console.log('New orientation:', screen.orientation.type);
});
}
// Media Devices (Camera/Microphone)
async function getUserMedia() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: {ideal: 1920},
height: {ideal: 1080}
},
audio: true
});
// Display video
const video = document.querySelector('video');
video.srcObject = stream;
return stream;
} catch (error) {
console.error('Media access denied:', error);
}
}
// List available devices
async function listDevices() {
const devices = await navigator.mediaDevices.enumerateDevices();
devices.forEach(device => {
console.log(device.kind, ':', device.label);
});
}
// Online/offline detection
window.addEventListener('online', () => {
console.log('Back online');
syncOfflineData();
});
window.addEventListener('offline', () => {
console.log('Gone offline');
showOfflineMessage();
});
// Check current status
if (navigator.onLine) {
console.log('Currently online');
} else {
console.log('Currently offline');
}
// Page Visibility API
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('Page hidden');
pauseVideo();
} else {
console.log('Page visible');
resumeVideo();
}
});
Key Points: Geolocation API requires user permission. Use
enableHighAccuracy for GPS. watchPosition() for continuous tracking. Device
orientation
for gyroscope data. Battery API for power management. Network Information to adapt to connection quality.
Vibration API for haptic feedback. Always check for API support before use.
7. Web Workers and Service Workers
Web Worker Types
| Type | Scope | Use Case |
|---|---|---|
| Dedicated Worker | Single script | Heavy computation, background tasks |
| Shared Worker | Multiple scripts/tabs | Cross-tab communication |
| Service Worker | Network proxy | Offline support, caching, push notifications |
Worker Methods and Events
| Method/Event | Purpose | Context |
|---|---|---|
| postMessage() | Send message | Main thread ↔ Worker |
| onmessage | Receive message | Main thread ↔ Worker |
| terminate() | Stop worker | Main thread |
| close() | Stop self | Worker |
| onerror | Handle errors | Main thread |
| importScripts() | Load external scripts | Worker |
Service Worker Lifecycle
| State | Description | Event |
|---|---|---|
| Installing | First time registration | install |
| Installed (Waiting) | Installed but not active | - |
| Activating | Taking control | activate |
| Activated | Controlling pages | - |
| Redundant | Replaced by newer version | - |
Service Worker Events
| Event | When Fired | Use Case |
|---|---|---|
| install | First installation | Cache resources |
| activate | Becomes active | Clean old caches |
| fetch | Network request | Cache strategy, offline fallback |
| message | Receive message from page | Communication |
| push | Push notification received | Show notification |
| sync | Background sync triggered | Sync data when online |
Cache Strategies
| Strategy | Description | Best For |
|---|---|---|
| Cache First | Cache, fallback to network | Static assets (CSS, JS, images) |
| Network First | Network, fallback to cache | API calls, dynamic content |
| Cache Only | Always from cache | Offline-first apps |
| Network Only | Always from network | Real-time data |
| Stale While Revalidate | Cache first, update in background | Balance freshness and speed |
Example: Web Workers
// Main thread (main.js)
const worker = new Worker('worker.js');
// Send message to worker
worker.postMessage({
type: 'calculate',
data: [1, 2, 3, 4, 5]
});
// Receive message from worker
worker.onmessage = (event) => {
console.log('Result from worker:', event.data);
};
// Handle errors
worker.onerror = (error) => {
console.error('Worker error:', error.message);
};
// Terminate worker
worker.terminate();
// Worker file (worker.js)
self.onmessage = (event) => {
const {type, data} = event.data;
if (type === 'calculate') {
// Heavy computation
const result = data.reduce((sum, num) => sum + num, 0);
// Send result back
self.postMessage({
type: 'result',
result: result
});
}
};
// Import external scripts in worker
importScripts('math-utils.js', 'helpers.js');
// Example: Image processing in worker
// Main thread
const imageWorker = new Worker('image-processor.js');
canvas.toBlob((blob) => {
imageWorker.postMessage({
type: 'process',
image: blob
});
});
imageWorker.onmessage = (event) => {
const processedImage = event.data;
displayImage(processedImage);
};
// Worker (image-processor.js)
self.onmessage = async (event) => {
const {type, image} = event.data;
if (type === 'process') {
// Process image
const bitmap = await createImageBitmap(image);
// Apply filters, transformations
const processed = applyFilters(bitmap);
self.postMessage(processed);
}
};
// Transferable objects (zero-copy transfer)
const buffer = new ArrayBuffer(1024);
worker.postMessage(buffer, [buffer]); // Transfer ownership
// buffer is now unusable in main thread
// Worker pool for parallel processing
class WorkerPool {
constructor(workerScript, poolSize = 4) {
this.workers = [];
this.queue = [];
for (let i = 0; i < poolSize; i++) {
const worker = new Worker(workerScript);
worker.busy = false;
worker.onmessage = (event) => {
worker.busy = false;
worker.currentTask.resolve(event.data);
this.processQueue();
};
worker.onerror = (error) => {
worker.busy = false;
worker.currentTask.reject(error);
this.processQueue();
};
this.workers.push(worker);
}
}
run(data) {
return new Promise((resolve, reject) => {
this.queue.push({data, resolve, reject});
this.processQueue();
});
}
processQueue() {
if (this.queue.length === 0) return;
const availableWorker = this.workers.find(w => !w.busy);
if (availableWorker) {
const task = this.queue.shift();
availableWorker.busy = true;
availableWorker.currentTask = task;
availableWorker.postMessage(task.data);
}
}
terminate() {
this.workers.forEach(w => w.terminate());
}
}
// Usage
const pool = new WorkerPool('worker.js', 4);
const results = await Promise.all([
pool.run({task: 'process', data: data1}),
pool.run({task: 'process', data: data2}),
pool.run({task: 'process', data: data3})
]);
pool.terminate();
Example: Service Workers
// Register service worker (main.js)
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('Service Worker registered:', registration.scope);
// Check for updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
console.log('New version available!');
// Prompt user to refresh
}
});
});
} catch (error) {
console.error('Service Worker registration failed:', error);
}
});
}
// Service Worker file (sw.js)
const CACHE_NAME = 'my-app-v1';
const urlsToCache = [
'/',
'/styles.css',
'/script.js',
'/images/logo.png'
];
// Install event - cache resources
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('Caching files');
return cache.addAll(urlsToCache);
})
);
// Skip waiting to activate immediately
self.skipWaiting();
});
// Activate event - clean old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
// Take control of all pages immediately
self.clients.claim();
});
// Fetch event - serve from cache or network
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// Cache hit - return response
if (response) {
return response;
}
// Clone request
const fetchRequest = event.request.clone();
return fetch(fetchRequest).then((response) => {
// Check valid response
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Clone response
const responseToCache = response.clone();
// Cache new response
caches.open(CACHE_NAME)
.then((cache) => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
// Network First strategy
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
// Cache successful response
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then((cache) => cache.put(event.request, responseToCache));
return response;
})
.catch(() => {
// Network failed, try cache
return caches.match(event.request);
})
);
});
// Stale While Revalidate
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((cachedResponse) => {
const fetchPromise = fetch(event.request)
.then((networkResponse) => {
// Update cache
caches.open(CACHE_NAME)
.then((cache) => cache.put(event.request, networkResponse.clone()));
return networkResponse;
});
// Return cached response immediately, update in background
return cachedResponse || fetchPromise;
})
);
});
// Message handling
self.addEventListener('message', (event) => {
if (event.data.action === 'skipWaiting') {
self.skipWaiting();
}
});
// Push notifications
self.addEventListener('push', (event) => {
const data = event.data.json();
const options = {
body: data.body,
icon: '/images/icon.png',
badge: '/images/badge.png',
data: {url: data.url}
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
// Notification click
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
});
// Background sync
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-messages') {
event.waitUntil(syncMessages());
}
});
async function syncMessages() {
// Get pending messages from IndexedDB
const messages = await getPendingMessages();
// Send to server
for (const message of messages) {
try {
await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify(message)
});
// Remove from pending
await removePendingMessage(message.id);
} catch (error) {
console.error('Sync failed:', error);
}
}
}
// Unregister service worker
navigator.serviceWorker.getRegistrations().then((registrations) => {
registrations.forEach((registration) => {
registration.unregister();
});
});
Key Points: Web Workers run on separate thread for heavy
computation.
Use
postMessage() for communication. Service Workers act as network
proxy for offline support. Cache strategies: cache-first, network-first, stale-while-revalidate. Service worker
lifecycle: install → activate. Handle fetch events to intercept requests. Push notifications and background
sync.
Transferable objects for efficient data transfer.
Section 21 Summary: Web APIs and Browser Integration
- Fetch API: Promise-based HTTP requests, response.json(), request options (method, headers, body)
- Response Handling: Check response.ok, status codes, AbortController for cancellation
- DOM Selection: querySelector/All, getElementById, closest, matches for flexible selection
- DOM Manipulation: createElement, append/prepend, classList, innerHTML vs textContent
- Events: addEventListener with options (once, capture, passive), event delegation pattern
- Storage: localStorage (persistent), sessionStorage (tab-scoped), IndexedDB (NoSQL database)
- Clipboard API: navigator.clipboard.writeText/readText, requires HTTPS and permissions
- File API: FileReader (readAsText, readAsDataURL), drag and drop, file validation
- Geolocation: getCurrentPosition, watchPosition, coordinates (lat/lng/accuracy)
- Device APIs: Orientation, motion, battery, network info, vibration, media devices
- Web Workers: Separate thread for computation, postMessage communication, worker pools
- Service Workers: Network proxy, offline support, caching strategies, push notifications