Address Book Management

This example demonstrates how to build a complete address book system where users can save, edit, delete, and manage multiple addresses. Perfect for e-commerce checkouts, delivery apps, and service booking platforms.

Live Demo

Add addresses to your address book, set a default address, and manage your saved locations. Data is stored in your browser's localStorage for demonstration purposes.

No saved addresses

Add your first address to get started

How It Works

This example demonstrates a complete address book system with CRUD operations and local storage persistence.

  1. Address Storage: Addresses are stored in localStorage for demonstration. In production, you would store addresses in a backend database with user authentication.
  2. Address Types: Users can categorise addresses as Home, Work, or Other with custom labels. This helps organise multiple addresses and provides quick selection at checkout.
  3. Default Address: One address can be marked as default, which is automatically pre-selected in checkout forms. Only one address can be default at a time.
  4. Component Parsing: The autocomplete response is parsed into structured components (street number, street name, city, county, postcode, country) for easy form population and validation.
  5. Edit Functionality: When editing, the autocomplete is reinitialised and existing address components are pre-populated in the form fields for easy modification.
  6. Practical Applications: This pattern is essential for e-commerce platforms, food delivery apps, service booking systems, and any application where users have multiple delivery or service addresses.
<script>
import { PlacesAutocomplete } from 'places-autocomplete-js';

document.addEventListener('DOMContentLoaded', () => {
  let addresses = [];
  let autocomplete;
  let editingId = null;

  // Load addresses from localStorage
  function loadAddresses() {
    const stored = localStorage.getItem('pac-addresses');
    if (stored) {
      addresses = JSON.parse(stored);
      renderAddressList();
    }
  }

  // Save addresses to localStorage
  function saveAddresses() {
    localStorage.setItem('pac-addresses', JSON.stringify(addresses));
  }

  // Parse address components from Google Places
  function parseAddressComponents(components) {
    const mapped = components.reduce((acc, component) => {
      const type = component.types[0];
      acc[type] = component.longText;
      return acc;
    }, {});

    return {
      streetNumber: mapped.street_number || '',
      street: mapped.route || '',
      city: mapped.postal_town || mapped.locality || '',
      county: mapped.administrative_area_level_2 || 
              mapped.administrative_area_level_1 || '',
      postcode: mapped.postal_code || '',
      country: mapped.country || ''
    };
  }

  // Initialise autocomplete
  function initAutocomplete() {
    const container = document.getElementById('address-autocomplete');
    if (!container) return;

    if (autocomplete) {
      autocomplete.destroy();
    }

    autocomplete = new PlacesAutocomplete({
      containerId: 'address-autocomplete',
      googleMapsApiKey: 'YOUR_GOOGLE_MAPS_API_KEY',
      onResponse: (placeDetails) => {
        // Populate form with selected address
        const parsed = parseAddressComponents(placeDetails.addressComponents);
        
        document.getElementById('formatted-address').value = 
          placeDetails.formattedAddress;
        document.getElementById('street-number').value = parsed.streetNumber;
        document.getElementById('street-name').value = parsed.street;
        document.getElementById('city').value = parsed.city;
        document.getElementById('county').value = parsed.county;
        document.getElementById('postcode').value = parsed.postcode;
        document.getElementById('country').value = parsed.country;
        
        // Store coordinates
        document.getElementById('coordinates').value = 
          JSON.stringify(placeDetails.location);
      },
      onError: (error) => {
        console.error('Autocomplete Error:', error.message || error);
      },
      requestParams: {
        region: 'GB',
        includedRegionCodes: ['GB']
      },
      fetchFields: ['formattedAddress', 'addressComponents', 'location'],
      options: {
        placeholder: 'Search for your address...',
        clear_input: false
      }
    });
  }

  // Open add address form
  function openAddForm() {
    editingId = null;
    document.getElementById('form-title').textContent = 'Add New Address';
    document.getElementById('address-form').reset();
    
    // Set default if no addresses exist
    if (addresses.length === 0) {
      document.getElementById('is-default').checked = true;
    }
    
    document.getElementById('modal').classList.remove('hidden');
    setTimeout(() => initAutocomplete(), 100);
  }

  // Open edit address form
  function openEditForm(id) {
    editingId = id;
    const address = addresses.find(a => a.id === id);
    if (!address) return;

    document.getElementById('form-title').textContent = 'Edit Address';
    document.getElementById('address-type').value = address.type;
    document.getElementById('custom-label').value = 
      address.type === 'other' ? address.label : '';
    document.getElementById('formatted-address').value = 
      address.formattedAddress;
    document.getElementById('street-number').value = 
      address.components.streetNumber;
    document.getElementById('street-name').value = address.components.street;
    document.getElementById('city').value = address.components.city;
    document.getElementById('county').value = address.components.county;
    document.getElementById('postcode').value = address.components.postcode;
    document.getElementById('country').value = address.components.country;
    document.getElementById('is-default').checked = address.isDefault;
    document.getElementById('coordinates').value = 
      JSON.stringify(address.coordinates);

    document.getElementById('modal').classList.remove('hidden');
    setTimeout(() => initAutocomplete(), 100);
  }

  // Save address (add or update)
  function saveAddress(event) {
    event.preventDefault();

    const formData = {
      type: document.getElementById('address-type').value,
      customLabel: document.getElementById('custom-label').value,
      formattedAddress: document.getElementById('formatted-address').value,
      components: {
        streetNumber: document.getElementById('street-number').value,
        street: document.getElementById('street-name').value,
        city: document.getElementById('city').value,
        county: document.getElementById('county').value,
        postcode: document.getElementById('postcode').value,
        country: document.getElementById('country').value
      },
      isDefault: document.getElementById('is-default').checked,
      coordinates: JSON.parse(document.getElementById('coordinates').value || 'null')
    };

    if (!formData.formattedAddress) {
      alert('Please select an address');
      return;
    }

    // Determine label
    const typeLabels = { home: 'Home', work: 'Work', other: 'Other' };
    const label = formData.type === 'other' && formData.customLabel
      ? formData.customLabel
      : typeLabels[formData.type];

    const addressData = {
      id: editingId || Date.now().toString(),
      label: label,
      type: formData.type,
      formattedAddress: formData.formattedAddress,
      components: formData.components,
      coordinates: formData.coordinates,
      isDefault: formData.isDefault,
      createdAt: editingId 
        ? addresses.find(a => a.id === editingId).createdAt 
        : new Date().toISOString(),
      lastUsed: new Date().toISOString()
    };

    if (editingId) {
      // Update existing
      const index = addresses.findIndex(a => a.id === editingId);
      addresses[index] = addressData;
    } else {
      // Add new
      addresses.push(addressData);
    }

    // If set as default, remove default from others
    if (formData.isDefault) {
      addresses = addresses.map(a => ({
        ...a,
        isDefault: a.id === addressData.id
      }));
    }

    saveAddresses();
    closeModal();
    renderAddressList();
  }

  // Delete address
  function deleteAddress(id) {
    if (!confirm('Are you sure you want to delete this address?')) return;

    const wasDefault = addresses.find(a => a.id === id)?.isDefault;
    addresses = addresses.filter(a => a.id !== id);

    // Set first address as default if we deleted the default
    if (wasDefault && addresses.length > 0) {
      addresses[0].isDefault = true;
    }

    saveAddresses();
    renderAddressList();
  }

  // Set default address
  function setDefaultAddress(id) {
    addresses = addresses.map(a => ({
      ...a,
      isDefault: a.id === id
    }));
    saveAddresses();
    renderAddressList();
  }

  // Close modal
  function closeModal() {
    document.getElementById('modal').classList.add('hidden');
    if (autocomplete) {
      autocomplete.destroy();
    }
  }

  // Render address list
  function renderAddressList() {
    const container = document.getElementById('address-list');
    
    if (addresses.length === 0) {
      container.innerHTML = `
        <div class="empty-state">
          <p>No saved addresses</p>
          <p class="text-sm">Add your first address to get started</p>
        </div>
      `;
      return;
    }

    const icons = {
      home: '🏠',
      work: '💼',
      other: '👤'
    };

    container.innerHTML = addresses.map(address => `
      <div class="address-card">
        <div class="address-header">
          <div class="address-icon">${icons[address.type] || '📍'}</div>
          <div class="address-info">
            <h3>
              ${address.label}
              ${address.isDefault ? '<span class="default-badge">★ Default</span>' : ''}
            </h3>
            <p class="address-text">${address.formattedAddress}</p>
            <p class="postcode">Postcode: ${address.components.postcode}</p>
          </div>
        </div>
        <div class="address-actions">
          <button onclick="openEditForm('${address.id}')" title="Edit">✏️</button>
          ${!address.isDefault ? 
            `<button onclick="setDefaultAddress('${address.id}')" title="Set as default">⭐</button>` 
            : ''}
          <button onclick="deleteAddress('${address.id}')" title="Delete">🗑️</button>
        </div>
      </div>
    `).join('');
  }

  // Event listeners
  document.getElementById('add-address-btn').addEventListener('click', openAddForm);
  document.getElementById('address-form').addEventListener('submit', saveAddress);
  document.getElementById('cancel-btn').addEventListener('click', closeModal);
  
  document.getElementById('address-type').addEventListener('change', (e) => {
    const customLabelField = document.getElementById('custom-label-field');
    customLabelField.style.display = e.target.value === 'other' ? 'block' : 'none';
  });

  // Make functions globally available
  window.openEditForm = openEditForm;
  window.deleteAddress = deleteAddress;
  window.setDefaultAddress = setDefaultAddress;

  // Load addresses on page load
  loadAddresses();
});
</script>

<!-- Add Address Button -->
<button id="add-address-btn" class="btn-primary">+ Add New Address</button>

<!-- Address List -->
<div id="address-list" class="address-list">
  <!-- Addresses will be rendered here -->
</div>

<!-- Modal -->
<div id="modal" class="modal hidden">
  <div class="modal-content">
    <h2 id="form-title">Add New Address</h2>
    
    <form id="address-form">
      <!-- Address Type -->
      <div class="form-group">
        <label>Address Type</label>
        <select id="address-type">
          <option value="home">Home</option>
          <option value="work">Work</option>
          <option value="other">Other</option>
        </select>
      </div>

      <!-- Custom Label (shown if Other selected) -->
      <div id="custom-label-field" class="form-group" style="display: none;">
        <label>Custom Label</label>
        <input type="text" id="custom-label" placeholder="e.g., Mum's House">
      </div>

      <!-- Address Autocomplete -->
      <div class="form-group">
        <label>Search Address</label>
        <div id="address-autocomplete"></div>
      </div>

      <!-- Hidden field for formatted address -->
      <input type="hidden" id="formatted-address">
      <input type="hidden" id="coordinates">

      <!-- Address Fields -->
      <div class="form-row">
        <div class="form-group">
          <label>Street Number</label>
          <input type="text" id="street-number">
        </div>
        <div class="form-group">
          <label>Street Name</label>
          <input type="text" id="street-name">
        </div>
      </div>

      <div class="form-row">
        <div class="form-group">
          <label>City</label>
          <input type="text" id="city">
        </div>
        <div class="form-group">
          <label>County</label>
          <input type="text" id="county">
        </div>
      </div>

      <div class="form-row">
        <div class="form-group">
          <label>Postcode</label>
          <input type="text" id="postcode">
        </div>
        <div class="form-group">
          <label>Country</label>
          <input type="text" id="country">
        </div>
      </div>

      <!-- Set as Default -->
      <div class="form-group">
        <label class="checkbox-label">
          <input type="checkbox" id="is-default">
          Set as default address
        </label>
      </div>

      <!-- Actions -->
      <div class="form-actions">
        <button type="button" id="cancel-btn" class="btn-secondary">Cancel</button>
        <button type="submit" class="btn-primary">Save Address</button>
      </div>
    </form>
  </div>
</div>

<style>
  .btn-primary {
    background: #4f46e5;
    color: white;
    padding: 0.75rem 1.5rem;
    border-radius: 0.5rem;
    border: none;
    cursor: pointer;
    font-weight: 500;
  }

  .address-list {
    margin-top: 1.5rem;
    display: flex;
    flex-direction: column;
    gap: 1rem;
  }

  .address-card {
    padding: 1rem;
    border: 1px solid #e5e7eb;
    border-radius: 0.5rem;
    background: white;
    display: flex;
    justify-content: space-between;
    align-items: start;
  }

  .address-header {
    display: flex;
    gap: 1rem;
    flex: 1;
  }

  .address-icon {
    font-size: 1.5rem;
    width: 2.5rem;
    height: 2.5rem;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #f3f4f6;
    border-radius: 0.5rem;
  }

  .address-info h3 {
    font-weight: 600;
    margin-bottom: 0.5rem;
  }

  .default-badge {
    background: #dcfce7;
    color: #166534;
    padding: 0.25rem 0.5rem;
    border-radius: 0.25rem;
    font-size: 0.75rem;
    margin-left: 0.5rem;
  }

  .address-text {
    color: #6b7280;
    font-size: 0.875rem;
  }

  .postcode {
    color: #9ca3af;
    font-size: 0.75rem;
    margin-top: 0.25rem;
  }

  .address-actions {
    display: flex;
    gap: 0.5rem;
  }

  .address-actions button {
    background: #f3f4f6;
    border: none;
    padding: 0.5rem;
    border-radius: 0.375rem;
    cursor: pointer;
    font-size: 1rem;
  }

  .address-actions button:hover {
    background: #e5e7eb;
  }

  .modal {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
  }

  .modal.hidden {
    display: none;
  }

  .modal-content {
    background: white;
    padding: 2rem;
    border-radius: 0.5rem;
    max-width: 600px;
    width: 90%;
    max-height: 90vh;
    overflow-y: auto;
  }

  .form-group {
    margin-bottom: 1rem;
  }

  .form-group label {
    display: block;
    font-weight: 500;
    margin-bottom: 0.5rem;
    color: #374151;
  }

  .form-group input,
  .form-group select {
    width: 100%;
    padding: 0.5rem;
    border: 1px solid #d1d5db;
    border-radius: 0.375rem;
  }

  .form-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem;
  }

  .checkbox-label {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    cursor: pointer;
  }

  .checkbox-label input {
    width: auto;
  }

  .form-actions {
    display: flex;
    justify-content: flex-end;
    gap: 0.75rem;
    margin-top: 1.5rem;
  }

  .btn-secondary {
    background: #f3f4f6;
    color: #374151;
    padding: 0.75rem 1.5rem;
    border-radius: 0.5rem;
    border: none;
    cursor: pointer;
  }

  .empty-state {
    text-align: center;
    padding: 3rem;
    color: #6b7280;
  }
</style>