@composi/idb:

Usage

How to Use IDB

All @composi/idb methods return promises. That means to use them you'll need to use then and catch statements.

set

Set takes two arguments: a key and a value to store with it. The value can be any valid JavaScript type or even blobs, such as images, sound files, etc. Storing blobs can quickly take up all your availabe storage space, so use compression and only use the ones you aboslute need.

set returns a promise, so if you want to do something when the save completes, you need to use then.

idb.set('name', 'Joe')

// Or do something after setting the key/value:
idb.set('name', 'Joe')
  .then(() => {
    console.log('Saved the name.')
  })

If this is the first time in your code that you are using a method on the idb object, @composi/idb will create an instance of IndexedDB and try to connect to the default database and store. If they do not exist yet, they will be created so that the key/value can be saved.

get

The get method takes one argument: the key to retrieve. This is a string. This returns a promise, so to do anything with the returned value, you'll need to use a then function.

idb.get('products').then(products => {
  // Expecting an array of products:
  if (products && products.length) {
    // Do something with products
  }
})

Handle Failure

If get failes to return a value, probably because the key does not exist, it returns undefined. Because it returns something, you can't use a catch function to deal with this. Instead you'll need to use a conditional check see if you received anything, and if not, do something else. In the following example, notice how we first use an if statement to see if we got something, and then deal with a failed get in the else statement.

idb.get('users').then(users => {
  // Expecting an array of users:
  if (users && users.length) {
    render(<UserList data={users} />, 'section')
  } else {
    // No saved users so provide a default:
    const users = ['Ellen', 'Sam', 'Paula', 'Jeff']
    render(<UserList data={users} />, 'section')
  }
})

If this is the first time in your code that you are using a method on the idb object, @composi/idb will create an instance of IndexedDB. If the default database and store do not exist, it will create them. Otherwise it will try to get the key/value from the existing database.

remove

This function allows you to delete the database completely. Be careful when doing so.

idb.remove().then(() => {

})

clear

If you don't want to delete the database, but its content is old and stale, you can use this function to clear it out.

idb.clear().then(() => {

})

keys

keys() returns an array of all the keys in the database. You would use this when you want to know what keys are available.

idb.keys().then(keys => {
  if (keys.includes('saved-items')) {
    return idb.get('saved-items')
  }
})
  .then(items => {
    // Do something with saved items...
  })

Looking at the stored keys can give you some valuable insight into what was done in the last session.

Versioning and Timestamps

IndexedDB supports versioning and schema upgrades. @composi/idb does not. However, you can use set to store a version or timestamp so at each page load you can check that version or age of the data. If the version or timestamp are too old, you may choose to clear the database and populate it with new data.

// Set a version for the store:
idb.set('version', '3.5')

  // Next page load you can check the version:
idb.get('version')
  .then(version => {
    if (version && version < 5) {
      idb.clear()
    }
  })

Or you could save a timestamp and at page load check to see if the timestamp is older than 30 days:

// Set a timestap for the store:
idb.set('timestamp', new Date().getTime())

// On a later page load, get the timestamp and see if it is older than 30 days.
// For that we have some utilites to convert UTF to days.
const currentTime = new Date().getTime()
// One day in milliseconds.
const day = 1000 * 60 * 60 * 24
// Get the saved timestamp:
idb.get('timestamp')
  .then(timestamp => {
    // If timestamp is older than 30 days:
    if (timestamp && (currentTime - timestamp) / day > 30) {
      // Clear out all entries in the store:
      idb.clear()
    }
  })

Using with @composi/core

Here's an example of a todo list using @composi/idb and @composi/core with its runtime.

import { h, render, run, union } from '@composi/core'
import { clone } from '@composi/merge-objects'
import { idb } from '@composi/idb'

// id for list items
function id() {
  return Math.floor(Math.random() * 100000000 + Math.random() * 1000);
}

// Set state of footer bottons to show tasks.
const setButtonState = index => {
  const buttons = [false, false, false];
  buttons[index] = true;
  return buttons;
};

// Footer Component:
function Footer({ state, setButtonState, send }) {
  let count = 0;
  state.items.forEach(item => {
    if (item.active === true) {
      count += 1;
    }
  });
  return (
    <footer>
      <div id="totals-view">
        <span>{count} left.</span>
      </div>
      <p>Show: </p>
      <div id="show-todo-state">
        <button
          onclick={e => send(Msg.showAll(e))}
          id="show-all"
          class={state.selectedButton[0] ? "selected" : ""}
        >
          All
        </button>
        <button
          onclick={() => send(Msg.showActive())}
          id="show-active"
          class={state.selectedButton[1] ? "selected" : ""}
        >
          Active
        </button>
        <button
          onclick={e => send(Msg.showCompleted(e))}
          id="show-completed"
          class={state.selectedButton[2] ? "selected" : ""}
        >
          Completed
        </button>
      </div>
    </footer>
  );
}

// List Item Component:
function ListItem({ item, animateIn, animateOut, send }) {
  return (
    <li
      key={item.id}
      class={item.active ? "active" : ""}
      hidden={item.hidden}
      onmount={animateIn}
      onunmount={animateOut}
    >
      <button
        class="set-state"
        onclick={() => send(Msg.setActiveState(item.id))}
      >
        <svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1">
          <g
            id="Page-1"
            stroke="none"
            stroke-width="1"
            fill="none"
            fill-rule="evenodd"
          >
            <g id="selection-indicator">
              <path
                d="M2,13 L9.9294326,16.8406135 L17.1937075,1.90173332"
                id="checkmark"
                stroke="#007AFF"
                stroke-width="2"
              />
            </g>
          </g>
        </svg>
      </button>
      <h3 onclick={() => send(Msg.setActiveState(item.id))}>{item.value}</h3>
      <button onclick={() => send(Msg.deleteItem(item.id))} class="delete">
        <svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1">
          <g
            id="Page-1"
            stroke="none"
            stroke-width="1"
            fill="none"
            fill-rule="evenodd"
          >
            <g
              id="Delete"
              stroke="#FF0000"
              stroke-width="2"
              stroke-linecap="square"
            >
              <path d="M26.5,3.5 L3.5,26.5" id="Line" />
              <path d="M3.5,3.5 L26.5,26.5" id="Line" />
            </g>
          </g>
        </svg>
      </button>
    </li>
  );
}

// Define todo list.
function TodoList({ state, send }) {
  console.log(state)
  let value;
  function focusInput(input) {
    input.focus();
  }
  function updateValue(e) {
    value = e.target.value;
    e.target.focus();
  }
  function animateIn(el) {
    el.classList.add("add-item");
  }
  function animateOut(done, el) {
    el.classList.add("delete-item");
    setTimeout(() => {
      done();
    }, 1000);
  }
  return (
    <div class="parent-view">
      <p class="add-todo">
        <input type="text" onchange={updateValue} value={state.inputValue} />
        <button class="addItem" onclick={() => send(Msg.addItem(value))}>
          Add Item
        </button>
      </p>
      <ul class="todo-list">
        {state.items.map(item => (
          <ListItem {...{ item, animateIn, animateOut, send }} />
        ))}
      </ul>
      <Footer {...{ state, setButtonState, send }} />
    </div>
  );
}

// Default state:
let state = {
  newKey: 104,
  items: [
    { active: true, value: "Take a nap", id: id(), hidden: false },
    { active: false, value: "Eat a snack", id: id(), hidden: false },
    { active: true, value: "Talk with Mom", id: id(), hidden: false }
  ],
  selectedButton: [true, false, false],
  inputValue: ""
};

// Create a tagged union.
const Msg = union([
  "addItem",
  "deleteItem",
  "setActiveState",
  "showActive",
  "showCompleted",
  "showAll",
  "storeLocally"
]);

// Set up program.
const program = {
  init: [state],
  view(state, send) {
    render(<TodoList {...{ state, send }} />, "section")
  },
  update(msg, state) {
    return Msg.match(msg, {
      addItem: value => {
        if (value) {
          const prevState = clone(state);
          prevState.items.push({
            active: true,
            value,
            id: id(),
            hidden: false
          });
          // Preserve state in IndexedDB.
          // This is an async effect.
          idb.set('todos', prevState)
          return [prevState]
        }
      },
      deleteItem: id => {
        const prevState = clone(state);
        prevState.items = prevState.items.filter(item => item.id != id);
        // Preserve state in IndexedDB.
        // This is an async effect.
        idb.set('todos', prevState)
        return [prevState]
      },
      setActiveState: id => {
        const prevState = clone(state);
        const index = prevState.items.findIndex(item => {
          return item.id == id;
        });
        prevState.items[index].active = !prevState.items[index].active;
        return [prevState];
      },
      showActive: () => {
        const prevState = clone(state);
        prevState.items.map(item => {
          if (!item.active) item.hidden = true;
          else item.hidden = false;
        });
        prevState.selectedButton = setButtonState(1);
        return [prevState];
      },
      showCompleted: () => {
        const prevState = clone(state);
        prevState.items.map(item => {
          if (item.active) item.hidden = true;
          else item.hidden = false;
        });
        prevState.selectedButton = setButtonState(2);
        return [prevState];
      },
      showAll: () => {
        const prevState = clone(state);
        prevState.items.map(item => (item.hidden = false));
        prevState.selectedButton = setButtonState(0);
        return [prevState];
      },
      storeLocally: prevState => {
        idb.set('todos', prevState)
      }
    });
  }
};

// Before running program,
// check to see if there is anything stored in IndexedDB,
// if there is load it as state for the program.
// Otherwise run program with default state.
idb.get('todos').then(data => {
  if (data) {
    console.log('got data')
    program.init[0] = data
    run(program)
    // If no data use default:
  } else {
    console.log('no data so using defaults')
    // render(<TodoList state={state} />, "section")
    run(program)
    idb.set('todos', state)
  }
})