Recipe 5.3

Querying With Indexes

This example is a an employee database. Employees have a first name, last name, and department.

An index is also created for the department property. This lets the database easily query for employees matching a specific department.

Select a department in the dropdown to filter the employee list.

Demo

Filter by Department
Name Last name Department
Loading...
Loading Users

Code

JavaScript
// Once the database is opened, it will be assigned to this variable.
let employeeDb;

/**
 * Opens the database, creating the object store and index if needed.
 * Once the database is ready, `onSuccess` will be called with the database object.
 * 
 * @param onSuccess A callback function that is executed when the database is ready
 */
function openDatabase(onSuccess) {
  const request = indexedDB.open('employees');

  // Keep track of whether or not the database needs to be populated.
  let shouldPopulate = false;

  request.addEventListener('upgradeneeded', () => {
    const db = request.result;

    // New employee objects will be given an auto-generated 
    // `id` property which serves as its key.
    const employeesStore = db.createObjectStore('employees', {
      keyPath: 'id',
      autoIncrement: true,
    });

    // Create an index on the `department` property called `department`.
    employeesStore.createIndex('department', 'department');

    // If the upgradeneeded event was triggered, that means the database
    // didn't exist yet, so the database needs to be populated.
    // Data can't be inserted during this event, so the success handler
    // will check its value and populate the database if needed.
    shouldPopulate = true;
  });
  
  request.addEventListener('success', () => {
    const db = request.result;

    if (shouldPopulate) {
      // If the flag is set, populate the database, passing along the 
      // onSuccess callback
      populateDatabase(db, onSuccess);
    } else {
      // Otherwise, call the onSuccess callback
      onSuccess(db);
    }
  });
}

/**
 * Gets the employees for a given department, or all employees
 * if no department is given.
 * 
 * @param department The department to filter by (optional)
 * @param onSuccess A callback function that is executed when the employees are loaded
 */
function getEmployees(department, onSuccess) {
  const request = employeeDb
    .transaction(['employees'], 'readonly')
    .objectStore('employees')
    .index('department')
    .getAll(department);

  request.addEventListener('success', () => {
    console.log('Got employees:', request.result);
    onSuccess(request.result);
  });

  request.addEventListener('error', () => {
    console.log('Error loading employees:', request.error);
  });
}

openDatabase(db => {
  employeeDb = db;
  renderEmployees();
});

const departmentFilter = document.querySelector('#department-filter');
departmentFilter.addEventListener('change', event => {
  const department = departmentFilter.value;
  if (department === 'All') {
    renderEmployees();
  } else {
    renderEmployees(department);
  }
});

/**
 * Reads the employees from the database, and renders them in the table.
 * @param department The department to filter by (optional)
 */
function renderEmployees(department) {
  getEmployees(department, employees => {
    const tbody = document.querySelector('.table tbody');

    // Remove the current employee rows
    tbody.innerHTML = '';

    employees.forEach(employee => {
      const row = document.createElement('tr');

      const firstName = document.createElement('td');
      firstName.textContent = employee.firstName;
      row.appendChild(firstName);

      const lastName = document.createElement('td');
      lastName.textContent = employee.lastName;
      row.appendChild(lastName);

      const department = document.createElement('td');
      department.textContent = employee.department;
      row.appendChild(department);

      tbody.appendChild(row);
    })
  });
}

/**
 * Populates the database with seed data.
 * @param db the IndexedDB object
 * @param onSuccess A callback to execute once the database is populated
 */
function populateDatabase(db, onSuccess) {
  const employees = [
    { firstName: "John", lastName: "Doe", department: "Sales" },
    { firstName: "Jane", lastName: "Smith", department: "HR" },
    { firstName: "Michael", lastName: "Johnson", department: "Finance" },
    { firstName: "Emily", lastName: "Williams", department: "Marketing" },
    { firstName: "David", lastName: "Brown", department: "IT" },
    { firstName: "Sarah", lastName: "Miller", department: "Operations" },
    { firstName: "James", lastName: "Taylor", department: "Sales" },
    { firstName: "Linda", lastName: "Anderson", department: "HR" },
    { firstName: "Robert", lastName: "Clark", department: "Finance" },
    { firstName: "Karen", lastName: "White", department: "Marketing" }
  ];

  const transaction = db.transaction(['employees'], 'readwrite');
  const store = transaction.objectStore('employees');
  employees.forEach(employee => {
    store.add(employee);
  });

  transaction.addEventListener('complete', () => {
    console.log('Database was populated');
    onSuccess(db);
  });
}
HTML
<div>
  <div class="form-label">Filter by Department</div>
  <select id="department-filter" class="form-select">
    <option selected>All</option>
    <option>Finance</option>
    <option>HR</option>
    <option>IT</option>
    <option>Marketing</option>
    <option>Operations</option>
    <option>Sales</option>
  </select>
</div>

<table id="users" class="table">
  <thead>
    <tr>
      <th class="w-25">Name</th>
      <th class="w-25">Last name</th>
      <th class="w-25">Department</th>
    </tr>
  </thead>
  <tbody>
    <tr id="loader">
      <td colspan="3" class="text-center p-4">
        <div class="spinner-border" role="status">
          <span class="visually-hidden">Loading...</span>
        </div>
        <div>Loading Users</div>
      </td>
    </tr>
  </tbody>
</table>
Web API Cookbook
Joe Attardi