> [!CAUTION]
> **Important:** Chrome will be removing support for Chrome Apps on all platforms. Chrome browser and the Chrome Web Store will continue to support extensions. [**Read the announcement**](https://blog.chromium.org/2020/08/changes-to-chrome-app-support-timeline.html) and learn more about [**migrating your app**](https://developer.chrome.com/apps/migration).

> [!NOTE]
> **Note:** **Want to start fresh from here?** Find the previous step's code in the [reference code zip](https://github.com/mangini/io13-codelab/archive/master.zip) under ***cheat_code \> solution_for_step1***.

In this step, you will learn:

- How to adapt an existing web application for the Chrome Apps platform.
- How to make your app scripts Content Security Policy (CSP) compliant.
- How to implement local storage using the [chrome.storage.local](https://developer.chrome.com/docs/extensions/reference/storage "Read 'chrome.storage.local' in the Chrome developer docs").

*Estimated time to complete this step: 20 minutes.*   

To preview what you will complete in this step, [jump down to the bottom of this page ↓](https://developer.chrome.com/docs/apps/app_codelab_import_todomvc#launch).

## Import an existing Todo app

As a starting point, import the [vanilla JavaScript version](https://todomvc.com/examples/vanillajs/) of [TodoMVC](https://todomvc.com/), a common benchmark
app, into your project.

We've included a version of the TodoMVC app in the [reference code zip](https://github.com/mangini/io13-codelab/archive/master.zip) in the ***todomvc***
folder. Copy all files (including folders) from *todomvc* into your project folder.

![Copy todomvc folder into codelab folder](https://developer.chrome.com/static/docs/apps/app_codelab_import_todomvc/image/copy-todomvc-folder-code-2921d3e41fc9e.png)

You will be asked to replace *index.html*. Go ahead and accept.

![Replace index.html](https://developer.chrome.com/static/docs/apps/app_codelab_import_todomvc/image/replace-indexhtml-db9baf1b1ecc4.png)

You should now have the following file structure in your application folder:

![New project folder](https://developer.chrome.com/static/docs/apps/app_codelab_import_todomvc/image/new-project-folder-8ebd9d3fa1ef3.png)

The files highlighted in blue are from the *todomvc* folder.

Reload your app now (**right-click \> Reload App**). You should see the basic UI but you won't be
able to add todos.

## Make scripts Content Security Policy (CSP) compliant

Open the DevTools Console (**right-click \> Inspect Element** , then select the **Console** tab). You
will see an error about refusing to execute an inline script:

![Todo app with CSP console log error](https://developer.chrome.com/static/docs/apps/app_codelab_import_todomvc/image/todo-app-csp-console-log-40f2a4d37397f.png)

Let's fix this error by making the app [Content Security Policy](https://developer.chrome.com/apps/contentSecurityPolicy) compliant. One of the most
common CSP non-compliances is caused by inline JavaScript. Examples of inline JavaScript include
event handlers as DOM attributes (e.g. `<button onclick=''>`) and `<script>` tags with content
inside the HTML.

The solution is simple: move the inline content to a new file.

1. Near the bottom of ***index.html*** , remove the inline JavaScript and instead include
*js/bootstrap.js*:

    <script src="bower_components/director/build/director.js"></script>
    <script>
      // Bootstrap app data
      window.app = {};
    </script>
    <script src="js/bootstrap.js"></script>
    <script src="js/helpers.js"></script>
    <script src="js/store.js"></script>

2. Create a file in the ***js*** folder named ***bootstrap.js***. Move the previously inline code
to be in this file:

    // Bootstrap app data
    window.app = {};

You'll still have a non-working Todo app if you reload the app now but you're getting closer.

## Convert localStorage to chrome.storage.local

If you open the DevTools Console now, the previous error should be gone. There is a new error,
however, about `window.localStorage` not being available:

![Todo app with localStorage console log error](https://developer.chrome.com/static/docs/apps/app_codelab_import_todomvc/image/todo-app-localstorage-co-b85dfb28afdd5.png)

Chrome Apps do not support [`localStorage`](http://dev.w3.org/html5/webstorage/#the-localstorage-attribute) as `localStorage` is synchronous. Synchronous access
to blocking resources (I/O) in a single-threaded runtime could make your app unresponsive.

Chrome Apps have an equivalent API that can store objects asynchronously. This will help avoid the
sometimes costly object-\>string-\>object serialization process.

To address the error message in our app, you need to convert `localStorage` to
[chrome.storage.local](https://developer.chrome.com/docs/extensions/reference/storage "Read 'chrome.storage.local' in the Chrome developer docs").

### Update app permissions

In order to use `chrome.storage.local`, you need to request the `storage` permission. In
***manifest.json*** , add `"storage"` to the `permissions` array:

    "permissions": ["storage"],

### Learn about local.storage.set() and local.storage.get()

To save and retrieve todo items, you need to know about the `set()` and `get()` methods of the
`chrome.storage` API.

The [set()](https://developer.chrome.com/docs/extensions/reference/storage#method-StorageArea-set "Read 'chrome.storage.local.set()' in the Chrome developer docs") method accepts an object of key-value pairs as its first parameter. An optional
callback function is the second parameter. For example:

    chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
      console.log("Secret message saved");
    });

The [get()](https://developer.chrome.com/docs/extensions/reference/storage#method-StorageArea-get "Read 'chrome.storage.local.get()' in the Chrome developer docs") method accepts an optional first parameter for the datastore keys you wish to
retreive. A single key can be passed as a string; multiple keys can be arranged into an array of
strings or a dictionary object.

The second parameter, which is required, is a callback function. In the returned object, use the
keys requested in the first parameter to access the stored values. For example:

    chrome.storage.local.get(['secretMessage','timeSet'], function(data) {
      console.log("The secret message:", data.secretMessage, "saved at:", data.timeSet);
    });

If you want to `get()` everything that is currently in `chrome.storage.local`, omit the first
parameter:

    chrome.storage.local.get(function(data) {
      console.log(data);
    });

Unlike `localStorage`, you won't be able to inspect locally stored items using the DevTools
Resources panel. You can, however, interact with `chrome.storage` from the JavaScript Console like
so:

![Use the Console to debug chrome.storage](https://developer.chrome.com/static/docs/apps/app_codelab_import_todomvc/image/use-console-debug-chrom-d0254b648f0cf.png)

### Preview required API changes

Most of the remaining steps in converting the Todo app are small changes to the API calls. Changing
all the places where `localStorage` is currently being used, though time-consuming and error-prone,
is required.

> [!NOTE]
> **Note:** To maximize your fun with this codelab, it'll be best if you overwrite your ***store.js*** , ***controller.js*** , and ***model.js*** with the ones from ***cheat_code/solution_for_step_2*** in the reference code zip. Once you've done that, continue reading as we'll go over each of the changes individually.

The key differences between `localStorage` and `chrome.storage` come from the async nature of
`chrome.storage`:

- Instead of writing to `localStorage` using simple assignment, you need to use
  `chrome.storage.local.set()` with optional callbacks.

      var data = { todos: [] };
      localStorage[dbName] = JSON.stringify(data);

  versus

      var storage = {};
      storage[dbName] = { todos: [] };
      chrome.storage.local.set( storage, function() {
        // optional callback
      });

- Instead of accessing `localStorage[myStorageName]` directly, you need to use
  `chrome.storage.local.get(myStorageName,function(storage){...})` and then parse the returned
  `storage` object in the callback function.

      var todos = JSON.parse(localStorage[dbName]).todos;

  versus

      chrome.storage.local.get(dbName, function(storage) {
        var todos = storage[dbName].todos;
      });

- The function `.bind(this)` is used on all callbacks to ensure `this` refers to the `this` of the
  `Store` prototype. (More info on bound functions can be found on the MDN docs:
  [Function.prototype.bind()](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Function/bind).)

      function Store() {
        this.scope = 'inside Store';
        chrome.storage.local.set( {}, function() {
          console.log(this.scope); // outputs: 'undefined'
        });
      }
      new Store();

  versus

      function Store() {
        this.scope = 'inside Store';
        chrome.storage.local.set( {}, function() {
          console.log(this.scope); // outputs: 'inside Store'
        }.bind(this));
      }
      new Store();

Keep these key differences in mind as we cover retrieving, saving, and removing todo items in the
following sections.

### Retrieve todo items

Let's update the Todo app in order to retrieve todo items:

1. The `Store` constructor method takes care of initializing the Todo app with all the existing
todo items from the datastore. The method first checks if the datastore exists. If it doesn't, it'll
create an empty array of `todos` and save it to the datastore so there are no runtime read errors.

In ***js/store.js*** , convert the use of `localStorage` in the constructor method to instead use
`chrome.storage.local`:

    function Store(name, callback) {
      var data;
      var dbName;

      callback = callback || function () {};

      dbName = this._dbName = name;

      if (!localStorage[dbName]) {
        data = {
          todos: []
        };
        localStorage[dbName] = JSON.stringify(data);
      }
      callback.call(this, JSON.parse(localStorage[dbName]));

      chrome.storage.local.get(dbName, function(storage) {
        if ( dbName in storage ) {
          callback.call(this, storage[dbName].todos);
        } else {
          storage = {};
          storage[dbName] = { todos: [] };
          chrome.storage.local.set( storage, function() {
            callback.call(this, storage[dbName].todos);
          }.bind(this));
        }
      }.bind(this));
    }

2. The `find()` method is used when reading todos from the Model. The returned results change based
on whether you are filtering by "All", "Active", or "Completed".

Convert `find()` to use `chrome.storage.local`:

    Store.prototype.find = function (query, callback) {
      if (!callback) {
        return;
      }

      var todos = JSON.parse(localStorage[this._dbName]).todos;

      callback.call(this, todos.filter(function (todo) {
      chrome.storage.local.get(this._dbName, function(storage) {
        var todos = storage[this._dbName].todos.filter(function (todo) {
          for (var q in query) {
             return query[q] === todo[q];
          }
          });
        callback.call(this, todos);
      }.bind(this));
      }));
    };

3. Similiar to `find()`, `findAll()` gets all todos from the Model. Convert `findAll()` to use
`chrome.storage.local`:

    Store.prototype.findAll = function (callback) {
      callback = callback || function () {};
      callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
      chrome.storage.local.get(this._dbName, function(storage) {
        var todos = storage[this._dbName] && storage[this._dbName].todos || [];
        callback.call(this, todos);
      }.bind(this));
    };

### Save todos items

The current `save()` method presents a challenge. It depends on two async operations (get and set)
that operate on the whole monolithic JSON storage every time. Any batch updates on more than one
todo item, like "mark all todos as completed", will result in a data hazard known as
[Read-After-Write](http://en.wikipedia.org/wiki/Hazard_(computer_architecture)#Read_After_Write_.28RAW.29). This issue wouldn't happen if we were using a more appropriate data storage,
like IndexedDB, but we are trying to minimize the conversion effort for this codelab.

There are several ways to fix it so we will use this opportunity to slightly refactor `save()` by
taking an array of todo IDs to be updated all at once:

1. To start off, wrap everything already inside `save()` with a `chrome.storage.local.get()`
callback:

    Store.prototype.save = function (id, updateData, callback) {
      chrome.storage.local.get(this._dbName, function(storage) {
        var data = JSON.parse(localStorage[this._dbName]);
        // ...
        if (typeof id !== 'object') {
          // ...
        }else {
          // ...
        }
      }.bind(this));
    };

2. Convert all the `localStorage` instances with `chrome.storage.local`:

    Store.prototype.save = function (id, updateData, callback) {
      chrome.storage.local.get(this._dbName, function(storage) {
        var data = JSON.parse(localStorage[this._dbName]);
        var data = storage[this._dbName];
        var todos = data.todos;

        callback = callback || function () {};

        // If an ID was actually given, find the item and update each property
        if ( typeof id !== 'object' ) {
          // ...

          localStorage[this._dbName] = JSON.stringify(data);
          callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
          chrome.storage.local.set(storage, function() {
            chrome.storage.local.get(this._dbName, function(storage) {
              callback.call(this, storage[this._dbName].todos);
            }.bind(this));
          }.bind(this));
        } else {
          callback = updateData;

          updateData = id;

          // Generate an ID
          updateData.id = new Date().getTime();

          localStorage[this._dbName] = JSON.stringify(data);
          callback.call(this, [updateData]);
          chrome.storage.local.set(storage, function() {
            callback.call(this, [updateData]);
          }.bind(this));
        }
      }.bind(this));
    };

3. Then update the logic to operate on an array instead of a single item:

    Store.prototype.save = function (id, updateData, callback) {
      chrome.storage.local.get(this._dbName, function(storage) {
        var data = storage[this._dbName];
        var todos = data.todos;

        callback = callback || function () {};

        // If an ID was actually given, find the item and update each property
        if ( typeof id !== 'object' || Array.isArray(id) ) {
          var ids = [].concat( id );
          ids.forEach(function(id) {
            for (var i = 0; i < todos.length; i++) {
              if (todos[i].id == id) {
                for (var x in updateData) {
                  todos[i][x] = updateData[x];
                }
              }
            }
          });

          chrome.storage.local.set(storage, function() {
            chrome.storage.local.get(this._dbName, function(storage) {
              callback.call(this, storage[this._dbName].todos);
            }.bind(this));
          }.bind(this));
        } else {
          callback = updateData;

          updateData = id;

          // Generate an ID
          updateData.id = new Date().getTime();

          todos.push(updateData);
          chrome.storage.local.set(storage, function() {
            callback.call(this, [updateData]);
          }.bind(this));
        }
      }.bind(this));
    };

### Mark todo items as complete

Now that app is operating on arrays, you need to change how the app handles a user clicking on the
**Clear completed (#)** button:

1. In ***controller.js*** , update `toggleAll()` to call `toggleComplete()` only once with an array
of todos instead of marking a todo as completed one by one. Also delete the call to `_filter()`
since you'll be adjusting the `toggleComplete` `_filter()`.

    Controller.prototype.toggleAll = function (e) {
      var completed = e.target.checked ? 1 : 0;
      var query = 0;
      if (completed === 0) {
        query = 1;
      }
      this.model.read({ completed: query }, function (data) {
        var ids = [];
        data.forEach(function (item) {
          this.toggleComplete(item.id, e.target, true);
          ids.push(item.id);
        }.bind(this));
        this.toggleComplete(ids, e.target, false);
      }.bind(this));

      this._filter();
    };

2. Now update `toggleComplete()` to accept both a single todo or an array of todos. This includes
moving `filter()` to be inside the `update()`, instead of outside.

    Controller.prototype.toggleComplete = function (ids, checkbox, silent) {
      var completed = checkbox.checked ? 1 : 0;
      this.model.update(ids, { completed: completed }, function () {
        if ( ids.constructor != Array ) {
          ids = [ ids ];
        }
        ids.forEach( function(id) {
          var listItem = $$('[data-id="' + id + '"]');
          
          if (!listItem) {
            return;
          }
          
          listItem.className = completed ? 'completed' : '';
          
          // In case it was toggled from an event and not by clicking the checkbox
          listItem.querySelector('input').checked = completed;
        });

        if (!silent) {
          this._filter();
        }

      }.bind(this));
    };

### Count todo items

After switching to async storage, there is a minor bug that shows up when getting the number of
todos. You'll need to wrap the count operation in a callback function:

1. In ***model.js*** , update `getCount()` to accept a callback:

      Model.prototype.getCount = function (callback) {
      var todos = {
        active: 0,
        completed: 0,
        total: 0
      };
      this.storage.findAll(function (data) {
        data.each(function (todo) {
          if (todo.completed === 1) {
            todos.completed++;
          } else {
            todos.active++;
          }
          todos.total++;
        });
        if (callback) callback(todos);
      });
      return todos;
    };

2. Back in ***controller.js*** , update `_updateCount()` to use the async `getCount()` you edited in
the previous step:

    Controller.prototype._updateCount = function () {
      var todos = this.model.getCount();
      this.model.getCount(function(todos) {
        this.$todoItemCounter.innerHTML = this.view.itemCounter(todos.active);

        this.$clearCompleted.innerHTML = this.view.clearCompletedButton(todos.completed);
        this.$clearCompleted.style.display = todos.completed > 0 ? 'block' : 'none';

        this.$toggleAll.checked = todos.completed === todos.total;

        this._toggleFrame(todos);
      }.bind(this));

    };

You are almost there! If you reload the app now, you will be able to insert new todos without any
console errors.

### Remove todos items

Now that the app can save todo items, you're close to being done! You still get errors when you
attempt to *remove* todo items:

![Todo app with localStorage console log error](https://developer.chrome.com/static/docs/apps/app_codelab_import_todomvc/image/todo-app-localstorage-co-b14b4916979b7.png)

1. In ***store.js*** , convert all the `localStorage` instances to use `chrome.storage.local`:

a) To start off, wrap everything already inside `remove()` with a `get()` callback:

    Store.prototype.remove = function (id, callback) {
      chrome.storage.local.get(this._dbName, function(storage) {
        var data = JSON.parse(localStorage[this._dbName]);
        var todos = data.todos;

        for (var i = 0; i < todos.length; i++) {
          if (todos[i].id == id) {
            todos.splice(i, 1);
            break;
          }
        }

        localStorage[this._dbName] = JSON.stringify(data);
        callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
      }.bind(this));
    };

b) Then convert the contents within the `get()` callback:

    Store.prototype.remove = function (id, callback) {
      chrome.storage.local.get(this._dbName, function(storage) {
        var data = JSON.parse(localStorage[this._dbName]);
        var data = storage[this._dbName];
        var todos = data.todos;

        for (var i = 0; i < todos.length; i++) {
          if (todos[i].id == id) {
            todos.splice(i, 1);
            break;
          }
        }

        localStorage[this._dbName] = JSON.stringify(data);
        callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
        chrome.storage.local.set(storage, function() {
          callback.call(this, todos);
        }.bind(this));
      }.bind(this));
    };

2. The same Read-After-Write data hazard issue previously present in the `save()` method is also
present when removing items so you will need to update a few more places to allow for batch
operations on a list of todo IDs.

a) Still in *store.js* , update `remove()`:

    Store.prototype.remove = function (id, callback) {
      chrome.storage.local.get(this._dbName, function(storage) {
        var data = storage[this._dbName];
        var todos = data.todos;

        var ids = [].concat(id);
        ids.forEach( function(id) {
          for (var i = 0; i < todos.length; i++) {
            if (todos[i].id == id) {
              todos.splice(i, 1);
              break;
            }
          }
        });

        chrome.storage.local.set(storage, function() {
          callback.call(this, todos);
        }.bind(this));
      }.bind(this));
    };

b) In ***controller.js*** , change `removeCompletedItems()` to make it call `removeItem()` on all IDs
at once:

    Controller.prototype.removeCompletedItems = function () {
      this.model.read({ completed: 1 }, function (data) {
        var ids = [];
        data.forEach(function (item) {
          this.removeItem(item.id);
          ids.push(item.id);
        }.bind(this));
        this.removeItem(ids);
      }.bind(this));

      this._filter();
    };

c) Finally, still in *controller.js* , change the `removeItem()` to support removing multiple items
from the DOM at once, and move the `_filter()` call to be inside the callback:

    Controller.prototype.removeItem = function (id) {
      this.model.remove(id, function () {
        var ids = [].concat(id);
        ids.forEach( function(id) {
          this.$todoList.removeChild($$('[data-id="' + id + '"]'));
        }.bind(this));
        this._filter();
      }.bind(this));
      this._filter();
    };

### Drop all todo items

There is one more method in *store.js* using `localStorage`:

    Store.prototype.drop = function (callback) {
      localStorage[this._dbName] = JSON.stringify({todos: []});
      callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
    };

This method is not being called in the current app so, if you want an extra challenge, try
implementing it on your own. Hint: Have a look at [`chrome.storage.local.clear()`](https://developer.chrome.com/docs/extensions/reference/storage#method-StorageArea-remove).

## Launch your finished Todo app

You are done Step 2! Reload your app and you should now have a fully working Chrome packaged version
of TodoMVC.

> [!NOTE]
> **Note:** **Troubleshooting**   
> Remember to always check the DevTools Console to see if there are any error messages.

## For more information

For more detailed information about some of the APIs introduced in this step, refer to:

- [Content Security Policy](https://developer.chrome.com/apps/contentSecurityPolicy "Read 'Content Security Policy' in the Chrome developer docs") [↑](https://developer.chrome.com/docs/apps/app_codelab_import_todomvc#csp-compliance "This feature mentioned in 'Make scripts Content Security Policy (CSP) compliant'")
- [Declare Permissions](https://developer.chrome.com/apps/declare_permissions "Read 'Declare Permissions' in the Chrome developer docs") [↑](https://developer.chrome.com/docs/apps/app_codelab_import_todomvc#update-permissions "This feature mentioned in 'Update app permissions'")
- [chrome.storage](https://developer.chrome.com/docs/extensions/reference/storage "Read 'chrome.storage' in the Chrome developer docs") [↑](https://developer.chrome.com/docs/apps/app_codelab_import_todomvc#get-and-set "This feature mentioned in 'Learn about local.storage.set() and local.storage.get()'")
- [chrome.storage.local.get()](https://developer.chrome.com/docs/extensions/reference/storage#method-StorageArea-get "Read 'chrome.storage.local.get()' in the Chrome developer docs") [↑](https://developer.chrome.com/docs/apps/app_codelab_import_todomvc#retrieve-items "This feature mentioned in 'Retrieve todos items'")
- [chrome.storage.local.set()](https://developer.chrome.com/docs/extensions/reference/storage#method-StorageArea-set "Read 'chrome.storage.local.set()' in the Chrome developer docs") [↑](https://developer.chrome.com/docs/apps/app_codelab_import_todomvc#save-items "This feature mentioned in 'Save todos items'")
- [chrome.storage.local.remove()](https://developer.chrome.com/docs/extensions/reference/storage#method-StorageArea-remove "Read 'chrome.storage.local.remove()' in the Chrome developer docs") [↑](https://developer.chrome.com/docs/apps/app_codelab_import_todomvc#remove-items "This feature mentioned in 'Remove todos items'")
- [chrome.storage.local.clear()](https://developer.chrome.com/docs/extensions/reference/storage#method-StorageArea-remove "Read 'chrome.storage.local.clear()' in the Chrome developer docs") [↑](https://developer.chrome.com/docs/apps/app_codelab_import_todomvc#remove-items "This feature mentioned in 'Drop all todo items'")

Ready to continue onto the next step? Go to [Step 3 - Add alarms and notifications \>\>](https://developer.chrome.com/docs/app_codelab_alarms)