Local Storage and Session Storage

Module 4: Forms & Interactive HTML - Thursday: Lecture 1

Introduction to Web Storage

Web Storage APIs provide a way for websites to store data in a user's browser, allowing for persistent state across page reloads and browser sessions. Before the introduction of these APIs, web developers primarily relied on cookies for client-side storage, which had significant limitations in size, complexity, and security.

The Web Storage API introduces two key mechanisms for storing data on the client side:

Think of localStorage as a small persistent database in the browser, similar to a filing cabinet that remains in your office even when you leave for the day. In contrast, sessionStorage is like a temporary whiteboard that gets erased when you leave the room.

flowchart TD A[Web Storage] --> B[localStorage] A --> C[sessionStorage] B --> D[Persists indefinitely] B --> E[Shared across browser tabs/windows] B --> F[Limit ~5MB per domain] C --> G[Lasts for page session] C --> H[Isolated to specific tab] C --> I[Limit ~5MB per domain]

The Evolution of Client-Side Storage

Understanding the history of client-side storage helps appreciate the significance of Web Storage:

timeline title Evolution of Client-Side Storage section Early Web 1994 : Cookies introduced 1997 : Cookie specification formalized section Pre-HTML5 2002 : Flash Local Shared Objects 2006 : Google Gears (offline storage) section HTML5 Era 2009 : Web Storage specification 2010 : IndexedDB working draft 2013 : Web Storage widely implemented section Modern Web 2015 : Service Workers for offline storage 2017 : Progressive Web Apps gaining traction 2020+ : Advanced storage APIs (File System Access API)

The Web Storage API resolved many limitations of cookies:

Feature Cookies Web Storage
Storage Capacity Small (~4KB) Large (~5MB)
Sent with Requests Yes (increases bandwidth) No (stays in browser)
API Complexity Complex string manipulation Simple key-value storage
Data Types Strings only Strings only (but easily JSON-transformable)
Expiration Configurable None (localStorage) or session-based (sessionStorage)

localStorage Basics

localStorage provides persistent storage that survives browser restarts. Data stored in localStorage remains available until explicitly removed by code or by the user clearing their browser data.

Core Methods and Properties

// Storing data in localStorage
localStorage.setItem('username', 'john_doe');
localStorage.setItem('preferences', JSON.stringify({
  theme: 'dark',
  fontSize: 'medium',
  notifications: true
}));

// Retrieving data from localStorage
const username = localStorage.getItem('username');
console.log(username); // 'john_doe'

const preferences = JSON.parse(localStorage.getItem('preferences'));
console.log(preferences.theme); // 'dark'

// Removing a single item
localStorage.removeItem('username');

// Clearing all localStorage data
// localStorage.clear(); // Uncomment to execute

Object-like Syntax

localStorage can also be accessed using object-like notation:

// Object-like syntax for localStorage
localStorage.user = 'jane_doe';
console.log(localStorage.user); // 'jane_doe'

// Setting properties
localStorage['lastLogin'] = new Date().toISOString();

// Getting properties
const lastLogin = localStorage['lastLogin'];
console.log(lastLogin);

While this syntax works, using the standard methods (setItem, getItem, etc.) is generally recommended for clarity and to avoid potential conflicts with built-in properties.

Storage Event

When localStorage changes in one browser tab, other tabs with the same origin receive a storage event:

// Listen for changes to localStorage from other tabs/windows
window.addEventListener('storage', function(event) {
  console.log('Storage changed in another tab/window');
  console.log('Key modified:', event.key);
  console.log('Old value:', event.oldValue);
  console.log('New value:', event.newValue);
  console.log('Storage area:', event.storageArea === localStorage ? 'localStorage' : 'sessionStorage');
  console.log('Page URL that made the change:', event.url);
});

This feature enables real-time synchronization between multiple open tabs/windows of the same website. Note that the event doesn't fire in the tab that made the change.

sessionStorage Basics

sessionStorage works similarly to localStorage but with a critical difference: data persists only for the duration of the page session. Once the user closes the browser tab or window, the data is cleared.

Core Methods and Properties

sessionStorage has the same API as localStorage:

// Storing temporary data in sessionStorage
sessionStorage.setItem('currentPage', 'products');
sessionStorage.setItem('searchQuery', 'bluetooth headphones');
sessionStorage.setItem('filters', JSON.stringify({
  priceRange: [20, 100],
  brand: ['Sony', 'Bose'],
  rating: 4
}));

// Retrieving data
const currentQuery = sessionStorage.getItem('searchQuery');
console.log(currentQuery); // 'bluetooth headphones'

const filters = JSON.parse(sessionStorage.getItem('filters'));
console.log(filters.brand); // ['Sony', 'Bose']

// Removing a specific item
sessionStorage.removeItem('searchQuery');

// Checking if an item exists
if (sessionStorage.getItem('currentPage')) {
  console.log('User is on page:', sessionStorage.getItem('currentPage'));
}

Session Context

It's important to understand what constitutes a "session" for sessionStorage:

graph TD A[Browser Behavior] --> B[Same Tab Navigation] A --> C[New Tab/Window] A --> D[Tab Duplication] A --> E[Tab/Window Close] B --> B1[sessionStorage preserved] C --> C1[New empty sessionStorage] D --> D1[sessionStorage copied] E --> E1[sessionStorage deleted]

Common Use Cases

localStorage Use Cases

// Example: Storing user preferences
function saveUserPreferences(preferences) {
  localStorage.setItem('userPreferences', JSON.stringify(preferences));
}

function loadUserPreferences() {
  const savedPrefs = localStorage.getItem('userPreferences');
  return savedPrefs ? JSON.parse(savedPrefs) : getDefaultPreferences();
}

function applyTheme() {
  const preferences = loadUserPreferences();
  document.body.classList.add(`theme-${preferences.theme}`);
  document.body.style.fontSize = `${preferences.fontSize}px`;
}

// Example: Form autosave
const form = document.getElementById('contact-form');
const formFields = form.querySelectorAll('input, select, textarea');

// Save form data as user types
formFields.forEach(field => {
  field.addEventListener('input', function() {
    const formData = {};
    formFields.forEach(input => {
      formData[input.name] = input.value;
    });
    localStorage.setItem('contactFormData', JSON.stringify(formData));
  });
});

// Load saved form data on page load
document.addEventListener('DOMContentLoaded', function() {
  const savedData = localStorage.getItem('contactFormData');
  if (savedData) {
    const formData = JSON.parse(savedData);
    for (const key in formData) {
      const field = form.querySelector(`[name="${key}"]`);
      if (field) {
        field.value = formData[key];
      }
    }
  }
});

sessionStorage Use Cases

// Example: Multi-step wizard with sessionStorage
function saveWizardStep(stepNumber, stepData) {
  // Save the current step number
  sessionStorage.setItem('currentWizardStep', stepNumber);
  
  // Save the data for this step
  sessionStorage.setItem(`wizardStep${stepNumber}`, JSON.stringify(stepData));
}

function loadWizardStep() {
  // Get the current step (default to 1 if not found)
  const currentStep = sessionStorage.getItem('currentWizardStep') || 1;
  
  // Load the data for this step
  const stepData = sessionStorage.getItem(`wizardStep${currentStep}`);
  return {
    step: parseInt(currentStep),
    data: stepData ? JSON.parse(stepData) : {}
  };
}

// Example: Showing the right wizard step on page load
document.addEventListener('DOMContentLoaded', function() {
  const wizardState = loadWizardStep();
  
  // Show the correct step
  document.querySelectorAll('.wizard-step').forEach(step => {
    step.style.display = 'none';
  });
  document.querySelector(`#step-${wizardState.step}`).style.display = 'block';
  
  // Populate step data
  const stepData = wizardState.data;
  for (const key in stepData) {
    const field = document.querySelector(`#step-${wizardState.step} [name="${key}"]`);
    if (field) {
      field.value = stepData[key];
    }
  }
});

Working with Complex Data

Web Storage only supports string values, but you can store complex data using JSON:

Storing and Retrieving Arrays

// Storing arrays
const recentSearches = ['javascript tutorial', 'web storage api', 'html5 features'];
localStorage.setItem('recentSearches', JSON.stringify(recentSearches));

// Retrieving arrays
const searches = JSON.parse(localStorage.getItem('recentSearches')) || [];
console.log(searches);  // ['javascript tutorial', 'web storage api', 'html5 features']

// Updating arrays
searches.unshift('new search term'); // Add to beginning
if (searches.length > 10) {
  searches.pop(); // Remove from end to maintain max length
}
localStorage.setItem('recentSearches', JSON.stringify(searches));

Storing and Retrieving Objects

// Storing objects
const userProfile = {
  name: 'Jane Smith',
  email: 'jane@example.com',
  preferences: {
    theme: 'light',
    fontSize: 16,
    notifications: {
      email: true,
      push: false,
      sms: false
    }
  },
  lastLogin: new Date().toISOString()
};

localStorage.setItem('userProfile', JSON.stringify(userProfile));

// Retrieving objects
const profile = JSON.parse(localStorage.getItem('userProfile'));
console.log(profile.name); // 'Jane Smith'
console.log(profile.preferences.theme); // 'light'

// Updating objects (merging properties)
const updatedSettings = {
  fontSize: 18,
  notifications: {
    push: true
  }
};

// Partial update (merging nested objects requires a deep merge approach)
const currentProfile = JSON.parse(localStorage.getItem('userProfile')) || {};
currentProfile.preferences = {
  ...currentProfile.preferences,
  ...updatedSettings
};

// For nested properties like notifications, we need manual merging
currentProfile.preferences.notifications = {
  ...currentProfile.preferences.notifications,
  ...updatedSettings.notifications
};

localStorage.setItem('userProfile', JSON.stringify(currentProfile));

Handling Special Data Types

JSON doesn't natively support certain types like Date, functions, or circular references:

// Handling date objects
const event = {
  title: 'Conference',
  date: new Date(2025, 5, 15), // This will become a string in JSON
  reminderSet: true
};

// Custom JSON serialization
localStorage.setItem('event', JSON.stringify(event, function(key, value) {
  // Identify date objects and mark them for later conversion
  if (this[key] instanceof Date) {
    return { _isDate: true, _value: this[key].toISOString() };
  }
  return value;
}));

// Custom JSON deserialization
const savedEvent = JSON.parse(localStorage.getItem('event'), function(key, value) {
  // Convert marked date objects back to Date instances
  if (typeof value === 'object' && value !== null && value._isDate) {
    return new Date(value._value);
  }
  return value;
});

console.log(savedEvent.date instanceof Date); // true
console.log(savedEvent.date.getFullYear()); // 2025

Storage Management

Storage Limits

Web Storage has size limitations that vary by browser:

// Checking storage usage (approximate method)
function getStorageUsage() {
  let totalBytes = 0;
  
  for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    const value = localStorage.getItem(key);
    totalBytes += key.length + value.length;
  }
  
  // Convert to KB/MB for readability
  let usage = totalBytes + ' bytes';
  if (totalBytes > 1024) {
    usage = (totalBytes / 1024).toFixed(2) + ' KB';
  }
  if (totalBytes > 1024 * 1024) {
    usage = (totalBytes / (1024 * 1024)).toFixed(2) + ' MB';
  }
  
  return usage;
}

console.log('Current localStorage usage:', getStorageUsage());

Error Handling

Handle storage errors gracefully to provide a better user experience:

// Safely setting items with error handling
function safeSetItem(key, value) {
  try {
    localStorage.setItem(key, value);
    return true;
  } catch (e) {
    if (isQuotaExceeded(e)) {
      console.warn('Storage quota exceeded. Could not save data.');
      // Handle quota exceeded (show message, clear old data, etc.)
      handleStorageFull();
      return false;
    } else {
      console.error('Error saving to localStorage:', e);
      return false;
    }
  }
}

// Check if the error is a quota exceeded error
function isQuotaExceeded(e) {
  return (
    e instanceof DOMException &&
    // Everything except Firefox
    (e.code === 22 ||
     // Firefox
     e.code === 1014 ||
     // Test name field as well for older browsers
     e.name === 'QuotaExceededError' ||
     e.name === 'NS_ERROR_DOM_QUOTA_REACHED')
  );
}

// Example handler for storage full situation
function handleStorageFull() {
  // Option 1: Show user a message asking them to clear data
  alert('Storage is full. Please clear some data to continue.');
  
  // Option 2: Clear old items automatically
  clearOldData();
}

// Example function to clear old data
function clearOldData() {
  // Create a timestamp for items older than 30 days
  const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
  
  // Get items with timestamps
  const timestampedItems = [];
  
  for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    
    try {
      const value = JSON.parse(localStorage.getItem(key));
      if (value && value.timestamp && value.timestamp < thirtyDaysAgo) {
        timestampedItems.push(key);
      }
    } catch (e) {
      // Skip non-JSON items
      continue;
    }
  }
  
  // Remove old items
  timestampedItems.forEach(key => localStorage.removeItem(key));
  
  console.log(`Cleared ${timestampedItems.length} old items from storage`);
}

Storage Expiration

Unlike cookies, localStorage doesn't have a built-in expiration mechanism. You can implement your own:

// Setting an item with expiration
function setItemWithExpiry(key, value, ttl) {
  const item = {
    value: value,
    expiry: Date.now() + ttl,
    timestamp: Date.now()
  };
  localStorage.setItem(key, JSON.stringify(item));
}

// Getting an item and checking expiration
function getItemWithExpiry(key) {
  const itemStr = localStorage.getItem(key);
  
  // Return null if item doesn't exist
  if (!itemStr) {
    return null;
  }
  
  const item = JSON.parse(itemStr);
  
  // Return null if the item is expired
  if (Date.now() > item.expiry) {
    localStorage.removeItem(key);
    return null;
  }
  
  return item.value;
}

// Example usage
// Set an item that expires in 24 hours
setItemWithExpiry('authToken', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', 24 * 60 * 60 * 1000);

// Later, retrieve the item
const token = getItemWithExpiry('authToken');
if (token) {
  // Token is valid
  console.log('Valid token:', token);
} else {
  // Token has expired or doesn't exist
  console.log('Token has expired or does not exist');
}

Security Considerations

Web Storage is convenient but has important security implications:

Key Security Concerns

What Not to Store

Avoid storing the following in Web Storage:

Best Practices

// Sanitizing user-generated content before storage
function safeStoreUserContent(key, content) {
  // Basic sanitization to prevent XSS
  const sanitized = content
    .replace(/&/g, '&')
    .replace(//g, '>')
    .replace(/"/g, '"')
    .replace(/'/g, ''');
  
  localStorage.setItem(key, sanitized);
}

// Safe retrieval and usage of data
function getSafeUserContent(key) {
  const content = localStorage.getItem(key);
  
  // Validate content before use
  if (typeof content !== 'string') {
    return '';
  }
  
  return content;
}

// Only store non-sensitive user preferences
const safePreferences = {
  theme: 'dark',
  fontSize: 'large',
  language: 'en'
};

// Never store full objects that might contain sensitive info
localStorage.setItem('userPreferences', JSON.stringify(safePreferences));

Practical Applications

Form Data Persistence

Prevent data loss in long forms by automatically saving user input:

// HTML example
<form id="application-form">
  <div class="form-group">
    <label for="full-name">Full Name:</label>
    <input type="text" id="full-name" name="fullName">
  </div>
  
  <div class="form-group">
    <label for="email">Email:</label>
    <input type="email" id="email" name="email">
  </div>
  
  <div class="form-group">
    <label for="cover-letter">Cover Letter:</label>
    <textarea id="cover-letter" name="coverLetter" rows="10"></textarea>
  </div>
  
  <div class="form-actions">
    <button type="submit">Submit Application</button>
    <button type="button" id="clear-saved-data">Clear Saved Data</button>
  </div>
  
  <div id="save-status" aria-live="polite"></div>
</form>

<script>
// Form autosave functionality
const formStorage = {
  formId: 'application-form',
  storageKey: 'savedApplicationForm',
  saveStatusElement: document.getElementById('save-status'),
  
  init: function() {
    const form = document.getElementById(this.formId);
    if (!form) return;
    
    // Load saved form data on page load
    this.loadFormData();
    
    // Save form data on input change (debounced)
    const inputs = form.querySelectorAll('input, select, textarea');
    inputs.forEach(input => {
      input.addEventListener('input', this.debounce(() => {
        this.saveFormData();
      }, 500));
    });
    
    // Handle form submission
    form.addEventListener('submit', () => {
      // Clear saved data on successful submission
      localStorage.removeItem(this.storageKey);
    });
    
    // Handle clear button
    document.getElementById('clear-saved-data').addEventListener('click', () => {
      localStorage.removeItem(this.storageKey);
      form.reset();
      this.showStatus('Saved data cleared');
    });
  },
  
  saveFormData: function() {
    const form = document.getElementById(this.formId);
    const formData = {};
    
    // Collect all form field values
    const inputs = form.querySelectorAll('input, select, textarea');
    inputs.forEach(input => {
      if (input.type === 'checkbox' || input.type === 'radio') {
        formData[input.name] = input.checked;
      } else {
        formData[input.name] = input.value;
      }
    });
    
    // Add timestamp
    formData.timestamp = Date.now();
    
    // Save to localStorage
    localStorage.setItem(this.storageKey, JSON.stringify(formData));
    
    // Update save status
    this.showStatus('Form data saved automatically');
  },
  
  loadFormData: function() {
    const form = document.getElementById(this.formId);
    const savedData = localStorage.getItem(this.storageKey);
    
    if (!savedData) return;
    
    try {
      const formData = JSON.parse(savedData);
      
      // Restore field values
      Object.keys(formData).forEach(key => {
        if (key === 'timestamp') return; // Skip timestamp
        
        const input = form.querySelector(`[name="${key}"]`);
        if (!input) return;
        
        if (input.type === 'checkbox' || input.type === 'radio') {
          input.checked = formData[key];
        } else {
          input.value = formData[key];
        }
      });
      
      // Show status with timestamp
      const savedTime = new Date(formData.timestamp).toLocaleString();
      this.showStatus(`Form data restored from ${savedTime}`);
      
    } catch (e) {
      console.error('Error loading saved form data:', e);
    }
  },
  
  showStatus: function(message) {
    if (!this.saveStatusElement) return;
    
    this.saveStatusElement.textContent = message;
    this.saveStatusElement.classList.add('active');
    
    // Hide after 3 seconds
    setTimeout(() => {
      this.saveStatusElement.classList.remove('active');
    }, 3000);
  },
  
  debounce: function(func, wait) {
    let timeout;
    return function(...args) {
      clearTimeout(timeout);
      timeout = setTimeout(() => func.apply(this, args), wait);
    };
  }
};

// Initialize form storage
document.addEventListener('DOMContentLoaded', function() {
  formStorage.init();
});
</script>

Theme Preferences

Remember user's theme preferences across visits:

<!-- HTML -->
<div class="theme-controls">
  <button id="theme-light" aria-pressed="false">Light Theme</button>
  <button id="theme-dark" aria-pressed="false">Dark Theme</button>
  <button id="theme-system" aria-pressed="true">System Default</button>
</div>

<script>
// Theme management
const themeManager = {
  themeKey: 'selectedTheme',
  
  init: function() {
    // Apply saved theme on page load
    this.applyTheme();
    
    // Set up event listeners
    document.getElementById('theme-light').addEventListener('click', () => this.setTheme('light'));
    document.getElementById('theme-dark').addEventListener('click', () => this.setTheme('dark'));
    document.getElementById('theme-system').addEventListener('click', () => this.setTheme('system'));
  },
  
  setTheme: function(theme) {
    // Save theme preference
    localStorage.setItem(this.themeKey, theme);
    
    // Apply the theme
    this.applyTheme();
    
    // Update button states
    this.updateButtons(theme);
  },
  
  applyTheme: function() {
    // Get saved theme or default to system
    const savedTheme = localStorage.getItem(this.themeKey) || 'system';
    
    // Remove existing theme classes
    document.body.classList.remove('theme-light', 'theme-dark');
    
    if (savedTheme === 'light') {
      document.body.classList.add('theme-light');
    } else if (savedTheme === 'dark') {
      document.body.classList.add('theme-dark');
    } else {
      // System theme based on user's preference
      if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
        document.body.classList.add('theme-dark');
      } else {
        document.body.classList.add('theme-light');
      }
      
      // Listen for system theme changes
      window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
        if (localStorage.getItem(this.themeKey) === 'system') {
          document.body.classList.remove('theme-light', 'theme-dark');
          document.body.classList.add(e.matches ? 'theme-dark' : 'theme-light');
        }
      });
    }
    
    // Update button states
    this.updateButtons(savedTheme);
  },
  
  updateButtons: function(theme) {
    // Reset all buttons
    document.querySelectorAll('.theme-controls button').forEach(btn => {
      btn.setAttribute('aria-pressed', 'false');
    });
    
    // Set active button
    document.getElementById(`theme-${theme}`).setAttribute('aria-pressed', 'true');
  }
};

// Initialize theme manager
document.addEventListener('DOMContentLoaded', function() {
  themeManager.init();
});
</script>

Shopping Cart

Implement a simple shopping cart that persists across page views:

<!-- HTML -->
<div class="product-list">
  <div class="product" data-id="p001" data-name="Product 1" data-price="19.99">
    <h3>Product 1</h3>
    <p>$19.99</p>
    <button class="add-to-cart">Add to Cart</button>
  </div>
  
  <div class="product" data-id="p002" data-name="Product 2" data-price="29.99">
    <h3>Product 2</h3>
    <p>$29.99</p>
    <button class="add-to-cart">Add to Cart</button>
  </div>
  
  <div class="product" data-id="p003" data-name="Product 3" data-price="39.99">
    <h3>Product 3</h3>
    <p>$39.99</p>
    <button class="add-to-cart">Add to Cart</button>
  </div>
</div>

<div class="cart-container">
  <h2>Shopping Cart</h2>
  <div id="cart-items"></div>
  <div id="cart-total">Total: $0.00</div>
  <button id="clear-cart">Clear Cart</button>
  <button id="checkout">Checkout</button>
</div>

<script>
// Shopping cart using localStorage
const shoppingCart = {
  cartKey: 'shoppingCart',
  
  init: function() {
    // Initialize the cart
    this.loadCart();
    this.updateCartDisplay();
    
    // Add to cart buttons
    document.querySelectorAll('.add-to-cart').forEach(button => {
      button.addEventListener('click', (event) => {
        const product = event.target.closest('.product');
        this.addToCart({
          id: product.dataset.id,
          name: product.dataset.name,
          price: parseFloat(product.dataset.price),
          quantity: 1
        });
      });
    });
    
    // Clear cart button
    document.getElementById('clear-cart').addEventListener('click', () => {
      this.clearCart();
    });
    
    // Checkout button
    document.getElementById('checkout').addEventListener('click', () => {
      alert(`Processing checkout for ${this.getCartTotal().toFixed(2)}`);
      // In a real app, you would redirect to checkout page
    });
  },
  
  loadCart: function() {
    // Get cart from localStorage or initialize empty cart
    const savedCart = localStorage.getItem(this.cartKey);
    this.cart = savedCart ? JSON.parse(savedCart) : [];
  },
  
  saveCart: function() {
    localStorage.setItem(this.cartKey, JSON.stringify(this.cart));
    this.updateCartDisplay();
  },
  
  addToCart: function(product) {
    // Check if product already in cart
    const existingProductIndex = this.cart.findIndex(item => item.id === product.id);
    
    if (existingProductIndex >= 0) {
      // Increment quantity if already in cart
      this.cart[existingProductIndex].quantity += 1;
    } else {
      // Add new product to cart
      this.cart.push(product);
    }
    
    this.saveCart();
  },
  
  removeFromCart: function(productId) {
    this.cart = this.cart.filter(item => item.id !== productId);
    this.saveCart();
  },
  
  updateQuantity: function(productId, quantity) {
    const product = this.cart.find(item => item.id === productId);
    
    if (product) {
      product.quantity = Math.max(1, quantity); // Ensure quantity is at least 1
      this.saveCart();
    }
  },
  
  clearCart: function() {
    this.cart = [];
    this.saveCart();
  },
  
  getCartTotal: function() {
    return this.cart.reduce((total, item) => {
      return total + (item.price * item.quantity);
    }, 0);
  },
  
  updateCartDisplay: function() {
    const cartContainer = document.getElementById('cart-items');
    const cartTotal = document.getElementById('cart-total');
    
    // Clear current display
    cartContainer.innerHTML = '';
    
    if (this.cart.length === 0) {
      cartContainer.innerHTML = '

Your cart is empty

'; cartTotal.textContent = 'Total: $0.00'; return; } // Create cart item elements this.cart.forEach(item => { const cartItem = document.createElement('div'); cartItem.className = 'cart-item'; cartItem.innerHTML = `
${item.name} $${item.price.toFixed(2)}
${item.quantity}
`; cartContainer.appendChild(cartItem); }); // Add event listeners to quantity buttons cartContainer.querySelectorAll('.decrease-quantity').forEach(button => { button.addEventListener('click', (e) => { const productId = e.target.dataset.id; const product = this.cart.find(item => item.id === productId); if (product && product.quantity > 1) { this.updateQuantity(productId, product.quantity - 1); } }); }); cartContainer.querySelectorAll('.increase-quantity').forEach(button => { button.addEventListener('click', (e) => { const productId = e.target.dataset.id; const product = this.cart.find(item => item.id === productId); if (product) { this.updateQuantity(productId, product.quantity + 1); } }); }); // Add event listeners to remove buttons cartContainer.querySelectorAll('.remove-item').forEach(button => { button.addEventListener('click', (e) => { this.removeFromCart(e.target.dataset.id); }); }); // Update total cartTotal.textContent = `Total: $${this.getCartTotal().toFixed(2)}`; } }; // Initialize shopping cart document.addEventListener('DOMContentLoaded', function() { shoppingCart.init(); }); </script>

Working with Storage Events

Synchronize data across tabs or windows with storage events:

<!-- HTML -->
<div class="todo-app">
  <h2>Shared Todo List</h2>
  <p>Changes made here will be reflected in other open tabs</p>
  
  <div class="todo-form">
    <input type="text" id="new-todo" placeholder="Add a new task...">
    <button id="add-todo">Add</button>
  </div>
  
  <ul id="todo-list"></ul>
  
  <div class="todo-actions">
    <button id="clear-completed">Clear Completed</button>
    <button id="clear-all">Clear All</button>
  </div>
</div>

<script>
// Todo list with cross-tab synchronization
const todoApp = {
  storageKey: 'sharedTodos',
  
  init: function() {
    // Initialize the todo list
    this.loadTodos();
    this.renderTodos();
    
    // Add todo form
    document.getElementById('add-todo').addEventListener('click', () => {
      this.addTodo();
    });
    
    document.getElementById('new-todo').addEventListener('keypress', (e) => {
      if (e.key === 'Enter') {
        this.addTodo();
      }
    });
    
    // Todo actions
    document.getElementById('clear-completed').addEventListener('click', () => {
      this.clearCompleted();
    });
    
    document.getElementById('clear-all').addEventListener('click', () => {
      this.clearAll();
    });
    
    // Listen for storage events from other tabs
    window.addEventListener('storage', (e) => {
      if (e.key === this.storageKey) {
        this.loadTodos();
        this.renderTodos();
      }
    });
  },
  
  loadTodos: function() {
    const savedTodos = localStorage.getItem(this.storageKey);
    this.todos = savedTodos ? JSON.parse(savedTodos) : [];
  },
  
  saveTodos: function() {
    localStorage.setItem(this.storageKey, JSON.stringify(this.todos));
  },
  
  addTodo: function() {
    const input = document.getElementById('new-todo');
    const todoText = input.value.trim();
    
    if (todoText) {
      this.todos.push({
        id: Date.now().toString(),
        text: todoText,
        completed: false,
        createdAt: new Date().toISOString()
      });
      
      this.saveTodos();
      this.renderTodos();
      
      // Clear input
      input.value = '';
      input.focus();
    }
  },
  
  toggleTodo: function(id) {
    this.todos = this.todos.map(todo => {
      if (todo.id === id) {
        return { ...todo, completed: !todo.completed };
      }
      return todo;
    });
    
    this.saveTodos();
    this.renderTodos();
  },
  
  deleteTodo: function(id) {
    this.todos = this.todos.filter(todo => todo.id !== id);
    this.saveTodos();
    this.renderTodos();
  },
  
  clearCompleted: function() {
    this.todos = this.todos.filter(todo => !todo.completed);
    this.saveTodos();
    this.renderTodos();
  },
  
  clearAll: function() {
    this.todos = [];
    this.saveTodos();
    this.renderTodos();
  },
  
  renderTodos: function() {
    const todoList = document.getElementById('todo-list');
    todoList.innerHTML = '';
    
    if (this.todos.length === 0) {
      const emptyMessage = document.createElement('li');
      emptyMessage.className = 'empty-message';
      emptyMessage.textContent = 'No tasks added yet';
      todoList.appendChild(emptyMessage);
      return;
    }
    
    this.todos.forEach(todo => {
      const todoItem = document.createElement('li');
      todoItem.className = `todo-item${todo.completed ? ' completed' : ''}`;
      
      todoItem.innerHTML = `
        
${todo.text}
`; todoList.appendChild(todoItem); }); // Add event listeners todoList.querySelectorAll('.todo-checkbox').forEach(checkbox => { checkbox.addEventListener('change', (e) => { this.toggleTodo(e.target.dataset.id); }); }); todoList.querySelectorAll('.delete-todo').forEach(button => { button.addEventListener('click', (e) => { this.deleteTodo(e.target.dataset.id); }); }); } }; // Initialize todo app document.addEventListener('DOMContentLoaded', function() { todoApp.init(); }); </script>

Test this: Open the same page in multiple browser tabs to see changes synchronize automatically.

Feature Detection and Fallbacks

Ensure your application works even if Web Storage isn't available:

// Comprehensive storage helper with fallback
const storageHelper = {
  storage: null,
  
  // Initialize storage with feature detection
  init: function() {
    // Check if localStorage is available
    if (this.isLocalStorageAvailable()) {
      this.storage = window.localStorage;
      console.log('Using localStorage');
      return true;
    } 
    // Check if sessionStorage is available as fallback
    else if (this.isSessionStorageAvailable()) {
      this.storage = window.sessionStorage;
      console.log('Using sessionStorage as fallback');
      return true;
    } 
    // Use memory storage as last resort
    else {
      this.storage = this.createMemoryStorage();
      console.log('Using in-memory storage fallback');
      return false;
    }
  },
  
  // Test localStorage availability
  isLocalStorageAvailable: function() {
    try {
      const testKey = '__storage_test__';
      localStorage.setItem(testKey, testKey);
      const result = localStorage.getItem(testKey);
      localStorage.removeItem(testKey);
      return result === testKey;
    } catch (e) {
      return false;
    }
  },
  
  // Test sessionStorage availability
  isSessionStorageAvailable: function() {
    try {
      const testKey = '__storage_test__';
      sessionStorage.setItem(testKey, testKey);
      const result = sessionStorage.getItem(testKey);
      sessionStorage.removeItem(testKey);
      return result === testKey;
    } catch (e) {
      return false;
    }
  },
  
  // Create in-memory storage fallback
  createMemoryStorage: function() {
    const memoryStorage = {};
    let storageSize = 0;
    
    return {
      data: {},
      
      setItem: function(key, value) {
        this.data[key] = String(value);
      },
      
      getItem: function(key) {
        return this.data[key] || null;
      },
      
      removeItem: function(key) {
        delete this.data[key];
      },
      
      clear: function() {
        this.data = {};
      },
      
      key: function(index) {
        return Object.keys(this.data)[index] || null;
      },
      
      get length() {
        return Object.keys(this.data).length;
      }
    };
  },
  
  // Storage interface methods
  setItem: function(key, value) {
    try {
      this.storage.setItem(key, value);
      return true;
    } catch (e) {
      console.error('Error saving to storage:', e);
      return false;
    }
  },
  
  getItem: function(key) {
    return this.storage.getItem(key);
  },
  
  removeItem: function(key) {
    this.storage.removeItem(key);
  },
  
  clear: function() {
    this.storage.clear();
  },
  
  // JSON helpers
  setObject: function(key, object) {
    return this.setItem(key, JSON.stringify(object));
  },
  
  getObject: function(key) {
    const item = this.getItem(key);
    if (!item) return null;
    
    try {
      return JSON.parse(item);
    } catch (e) {
      console.error('Error parsing stored JSON:', e);
      return null;
    }
  }
};

// Initialize storage helper
document.addEventListener('DOMContentLoaded', function() {
  storageHelper.init();
  
  // Example usage
  storageHelper.setItem('username', 'john_doe');
  storageHelper.setObject('userPreferences', {
    theme: 'dark',
    fontSize: 16,
    notifications: true
  });
  
  console.log('Username:', storageHelper.getItem('username'));
  console.log('Preferences:', storageHelper.getObject('userPreferences'));
});

Browser Compatibility

Web Storage has excellent browser support:

The primary compatibility concerns are:

Practice Activities

Activity 1: User Preferences Manager

Create a user preferences manager that allows users to customize:

Implement the following:

Activity 2: Multi-Step Form

Create a multi-step form (3+ steps) that:

Activity 3: Cross-Tab Communication

Create a simple note-taking application that:

Summary

In this lecture, we've covered:

Web Storage provides a powerful and straightforward way to persist data on the client side, enhancing user experience by preserving state, saving preferences, and reducing data loss. With its excellent browser support and simple API, it's an essential tool for modern web development.

Remember that while Web Storage is useful for many scenarios, it has limitations for large datasets, complex querying, or sensitive information. For these cases, consider alternatives like IndexedDB, cookies with secure attributes, or server-side storage depending on your requirements.

In the next lecture, we'll explore the Geolocation API, which allows websites to access a user's geographical location with their permission.

Further Resources