Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1200)

Unified Diff: chrome/common/extensions/docs/templates/articles/app_codelab_import_todomvc.html

Issue 609433003: Updated Chrome Apps codelab from I/O 2013 Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Upload the rest of the images Created 6 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/common/extensions/docs/templates/articles/app_codelab_import_todomvc.html
diff --git a/chrome/common/extensions/docs/templates/articles/app_codelab_import_todomvc.html b/chrome/common/extensions/docs/templates/articles/app_codelab_import_todomvc.html
new file mode 100644
index 0000000000000000000000000000000000000000..9258d6254acc3169beead2c9210b547a451cb880
--- /dev/null
+++ b/chrome/common/extensions/docs/templates/articles/app_codelab_import_todomvc.html
@@ -0,0 +1,745 @@
+<h1 id="import-existing-app">
+ <span class="h1-step">Step 2:</span>
+ Import an Existing Web App
+</h1>
+
+<p class="note">
+ <strong>Want to start fresh from here?</strong>
+ Find the previous step's code in the <a href="https://github.com/mangini/io13-codelab/archive/master.zip">reference code zip</a> under <strong><em>cheat_code > solution_for_step1</strong></em>.
+</p>
+
+<p>In this step, you will learn:</p>
+
+<ul>
+ <li>How to adapt an existing web application for the Chrome Apps platform.</li>
+ <li>How to make your app scripts Content Security Policy (CSP) compliant.</li>
+ <li>How to implement local storage using the <code>chrome.storage.local</code> API.</li>
+</ul>
+
+<p>
+ <em>Estimated time to complete this step: 20 minutes.</em>
+ <br>
+ To preview what you will complete in this step, <a href="#launch">jump down to the bottom of this page &#8595;</a>.
+</p>
+
+<h2 id="todomvc">Import an existing Todo app</h2>
+
+<p>As a starting point, we will import the <a href="http://todomvc.com/vanilla-examples/vanillajs/">vanilla
+JavaScript version</a> of <a href="http://todomvc.com/">TodoMVC</a>, a common benchmark app, into our project.</p>
+
+<p>We've included a version of the TodoMVC app in the
+<a href="https://github.com/mangini/io13-codelab/archive/master.zip">reference code zip</a> in the <strong><em>todomvc</em></strong> folder.
+Copy all files (including folders) from <em>todomvc</em> into your project folder.</p>
+
+<figure>
+ <img src="{{static}}/images/app_codelab/copy-todomvc.png" alt="Copy todomvc folder into codelab folder">
+</figure>
+
+<p>You will be asked to replace <em>index.html</em>. Go ahead and accept.</p>
+
+<figure>
+ <img src="{{static}}/images/app_codelab/replace-index.png" alt="Replace index.html"><br>
+</figure>
+
+<p>You should now have the following file structure in your application folder:</p>
+
+<figure>
+ <img src="{{static}}/images/app_codelab/todomvc-copied.png" alt="New project folder">
+ <figcaption>The files highlighted in blue are from the <em>todomvc</em> folder.</figcaption>
+ <!--
+ <ul>
+ <li><strong>background.js</strong> (from step 1)</li>
+ <li><strong>bower_components/</strong> (from todomvc)</li>
+ <li><strong>bower.json</strong> (from todomvc)</li>
+ <li><strong>icon_128.png</strong> (from step 1)</li>
+ <li><strong>index.html</strong> (from todomvc)</li>
+ <li><strong>js/</strong> (from todomvc)</li>
+ <li><strong>manifest.json</strong> (from step 1)</li>
+ </ul>
+ -->
+</figure>
+
+
+<p>Reload your app now (<b>right-click > Reload App</b>). You should see the basic UI but you won't be able to add todos.</p>
+
+<h2 id="csp-compliance">Make scripts Content Security Policy (CSP) compliant</h2>
+
+<p>Open the DevTools Console (<strong>right-click > Inspect Element</strong>, then select the <strong>Console</strong> tab). You will see an error about refusing to execute an inline script:</p>
+
+<figure>
+ <img src="{{static}}/images/app_codelab/csp-console-error.png" alt="Todo app with CSP console log error">
+ <!--
+ <blockquote>
+ > Refused to execute inline script because it violates the following Content <br>
+ > Security Policy directive: "default-src 'self' chrome-extension-resource:". <br>
+ > Note that 'script-src' was not explicitly set, so 'default-src' is used as a <br>
+ > fallback. <br>
+ > index.html:42
+ </blockquote>
+ -->
+</figure>
+
+<p>Let's fix this error by making the app <a href="/apps/contentSecurityPolicy">Content Security Policy</a> 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. <code>&lt;button onclick=''&gt;</code>) and <code>&lt;script&gt;</code> tags with
+content inside the HTML.</p>
+
+<p>The solution is simple: move the inline content to a new file.</p>
+
+<p>1. Near the bottom of <strong><em>index.html</em></strong>, remove the inline
+JavaScript and instead include <em>js/bootstrap.js</em>:</p>
+
+<pre data-filename="index.html">
+&lt;script src="bower_components/director/build/director.js">&lt;/script>
+<strike>&lt;script&gt;</strike>
+<strike> // Bootstrap app data</strike>
+<strike> window.app = {};</strike>
+<strike>&lt;/script&gt;</strike>
+<b>&lt;script src="js/bootstrap.js"&gt;&lt;/script&gt;</b>
+&lt;script src="js/helpers.js">&lt;/script>
+&lt;script src="js/store.js">&lt;/script>
+</pre>
+
+<p>2. Create a file in the <strong><em>js</em></strong> folder named <strong><em>bootstrap.js</em></strong>. Move the previously inline code to be in this file:</p>
+
+<pre data-filename="bootstrap.js">
+// Bootstrap app data
+window.app = {};
+</pre>
+
+<p>You'll still have a non-working Todo app if you reload the app now but we're getting there.</p>
+
+<h2 id="convert-storage">Convert localStorage to chrome.storage.local</h2>
+
+<p>If you open the DevTools Console now, the previous error should be gone. However there is a new error about <code>window.localStorage</code> not being available:</p>
+
+<figure>
+ <img src="{{static}}/images/app_codelab/localStorage-console-error.png" alt="Todo app with localStorage console log error">
+ <!--
+ <blockquote>
+ > Uncaught window.localStorage is not available in packaged apps. Use <br>
+ > chrome.storage.local instead. store.js:21
+ </blockquote>
+ -->
+</figure>
+
+<p><a href="http://dev.w3.org/html5/webstorage/#the-localstorage-attribute"><code>LocalStorage</code></a>
+is not supported in Chrome Apps because <code>LocalStorage</code> is
+synchronous. Synchronous access to blocking resources (I/O) in a single threaded
+runtime could make your app become unresponsive.</p>
+
+<p>Chrome Apps have an equivalent API that can store objects asynchronously.
+This will help avoid the sometimes costly object->string->object serialization process.</p>
+
+<p>To address the error message in our app, we will need to convert <code>LocalStorage</code> to
+<code>chrome.storage.local</code>.</p>
+
+<h3 id="update-permissions">Update app permissions</h3>
+
+<p>In order to use <code>chrome.storage.local</code>, we need to request the <code>storage</code> permission. In <strong><em>manifest.json</em></strong>, add <code>"storage"</code> to the <code>permissions</code> array:</p>
+
+<pre data-filename="manifest.json">
+"permissions": [<b>"storage"</b>],
+</pre>
+
+<h3 id="get-and-set">Learn about local.storage.set() and local.storage.get()</h3>
+
+<p>To save and retrieve todo items, we'll need to know a bit about the <code>set()</code> and <code>get()</code> methods of the <code>chrome.storage</code> API.</p>
+
+<p>The <code>set()</code> method accepts an object of key-value pairs as its first parameter. An optional callback function is the second parameter. For example:</p>
+
+<pre>
+chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
+ console.log("Secret message saved");
+});
+</pre>
+
+<p>The <code>get()</code> 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.</p>
+
+<p>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:</p>
+
+<pre>
+chrome.storage.local.get(['secretMessage','timeSet'], function(data) {
+ console.log("The secret message:", data.secretMessage, "saved at:", data.timeSet);
+});
+</pre>
+
+<p>If you want to <code>get()</code> everything that is currently in <code>chrome.storage.local</code>,
+omit the first parameter:</p>
+
+<pre>
+chrome.storage.local.get(function(data) {
+ console.log(data);
+});
+</pre>
+
+<p>Unlike <code>localStorage</code>, you won't be able to inspect locally stored items using the DevTools Resources panel. However, you can interact with <code>chrome.storage</code> from the JavaScript Console like so:</p>
+
+<figure>
+ <img src="{{static}}/images/app_codelab/get-set-in-console.png" alt="Use the Console to debug chrome.storage">
+</figure>
+
+<h3 id="preview-changes">Preview required API changes</h3>
+
+<p>There are many remaining steps in converting the Todo app however they are all small changes to
+the API calls. Changing all the places where <code>localStorage</code> is currently being used
+will be time-consuming and error-prone &mdash; but required.</p>
+
+<p class="note">
+ To maximize your fun with this codelab, it'll be best if you overwrite your
+ <strong><em>store.js</em></strong>, <strong><em>controller.js</em></strong>, and <strong><em>model.js</em></strong>
+ with the ones from <strong><em>cheat_code/solution_for_step_2</em></strong> in the reference code zip.
+ <br><br>
+ Once you've done that, continue reading as we'll go over each of the changes individually.
+</p>
+
+<p>The key differences between <code>localStorage</code> and <code>chrome.storage</code> come from the async nature of <code>chrome.storage</code>:</p>
+
+<ul>
+ <li>
+ Instead of writing to <code>localStorage</code> using simple assignment, we need to use <code>chrome.storage.local.set()</code> with optional callbacks.
+<pre>
+var data = { todos: [] };
+localStorage[dbName] = JSON.stringify(data);
+</pre>
+versus
+<pre>
+var storage = {};
+storage[dbName] = { todos: [] };
+chrome.storage.local.set( storage, function() {
+ // optional callback
+});
+</pre>
+ </li>
+ <li>
+ Instead of accessing <code>localStorage[myStorageName]</code> directly, we need to use <code>chrome.storage.local.get(myStorageName,function(storage){...})</code> and then parse the returned <code>storage</code> object in the callback function.
+<pre>
+var todos = JSON.parse(localStorage[dbName]).todos;
+</pre>
+versus
+<pre>
+chrome.storage.local.get(dbName, function(storage) {
+ var todos = storage[dbName].todos;
+});
+</pre>
+ </li>
+ <li>
+ The use of <code>.bind(this)</code> is being used on all callbacks to ensure <code>this</code> refers to the <code>this</code> of the <code>Store</code> prototype. (More info on bound functions can be found on the MDN docs: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind">Function.prototype.bind()</a>.)
+<pre>
+function Store() {
+ this.scope = 'inside Store';
+ chrome.storage.local.set( {}, function() {
+ console.log(this.scope); // outputs: 'undefined'
+ });
+}
+new Store();
+</pre>
+versus
+<pre>
+function Store() {
+ this.scope = 'inside Store';
+ chrome.storage.local.set( {}, function() {
+ console.log(this.scope); // outputs: 'inside Store'
+ }<b>.bind(this)</b>);
+}
+new Store();
+</pre>
+ </li>
+</ul>
+
+<p>Keep these key differences in mind as we go over retrieving, saving, and removing todo items in the following sections.</p>
+
+<h3 id="retrieve-items">Retrieve todos items</h3>
+
+Let's update the Todo app in order to retrieve todo items:
+
+<p>1. The <code>Store</code> constructor method takes care of initializing the Todo app with all the existing todo items from the datastore. If this is the first time the app has been loaded, the datastore might not exist so the method checks if the datastore exists first. If it doesn't, it'll create an empty array of <code>todos</code> and save it to the datastore so there are no runtime read errors.</p>
+
+<p>In <strong><em>js/store.js</em></strong>, convert the use of <code>localStorage</code> in the constructor method to instead use
+<code>chrome.storage.local</code>:</p>
+
+<pre data-filename="store.js">
+function Store(name, callback) {
+ var data;
+ var dbName;
+
+ callback = callback || function () {};
+
+ dbName = this._dbName = name;
+
+ <strike>if (!localStorage[dbName]) {</strike>
+ <strike> data = {</strike>
+ <strike> todos: []</strike>
+ <strike> };</strike>
+ <strike> localStorage[dbName] = JSON.stringify(data);</strike>
+ <strike>}</strike>
+ <strike>callback.call(this, JSON.parse(localStorage[dbName]));</strike>
+
+ <b>chrome.storage.local.get(dbName, function(storage) {</b>
+ <b> if ( dbName in storage ) {</b>
+ <b> callback.call(this, storage[dbName].todos);</b>
+ <b> } else {</b>
+ <b> storage = {};</b>
+ <b> storage[dbName] = { todos: [] };</b>
+ <b> chrome.storage.local.set( storage, function() {</b>
+ <b> callback.call(this, storage[dbName].todos);</b>
+ <b> }.bind(this));</b>
+ <b> }</b>
+ <b>}.bind(this));</b>
+}
+</pre>
+
+<p>2. The <code>find()</code> method is used when reading todos from the Model. The returned results change based on whether you are filtering by "All", "Active", or "Completed".</p>
+
+<p>Convert <code>find()</code> to use <code>chrome.storage.local</code>:</p>
+
+<pre data-filename="store.js">
+Store.prototype.find = function (query, callback) {
+ if (!callback) {
+ return;
+ }
+
+ <strike>var todos = JSON.parse(localStorage[this._dbName]).todos;</strike>
+
+ <strike>callback.call(this, todos.filter(function (todo) {</strike>
+ <b>chrome.storage.local.get(this._dbName, function(storage) {</b>
+ <b> var todos = storage[this._dbName].todos.filter(function (todo) {</b>
+ <b> </b>for (var q in query) {
+ <b> </b> return query[q] === todo[q];
+ <b> </b>}
+ <b> });</b>
+ <b> callback.call(this, todos);</b>
+ <b>}.bind(this));</b>
+ <strike>}));</strike>
+};
+</pre>
+
+<p>3. Similiar to <code>find()</code>, <code>findAll()</code> gets all todos from the Model. Convert <code>findAll()</code> to use <code>chrome.storage.local</code>:</p>
+
+<pre data-filename="store.js">
+Store.prototype.findAll = function (callback) {
+ callback = callback || function () {};
+ <strike>callback.call(this, JSON.parse(localStorage[this._dbName]).todos);</strike>
+ <b>chrome.storage.local.get(this._dbName, function(storage) {</b>
+ <b> var todos = storage[this._dbName] && storage[this._dbName].todos || [];</b>
+ <b> callback.call(this, todos);</b>
+ <b>}.bind(this));</b>
+};
+</pre>
+
+<h3 id="save-items">Save todos items</h3>
+
+<p>The current <code>save()</code> 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
+<a href="http://en.wikipedia.org/wiki/Hazard_(computer_architecture)#Read_After_Write_.28RAW.29">Read-After-Write</a>.
+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.</p>
+
+<p>There are several ways to fix it so we will use this opportunity to slightly
+refactor <code>save()</code> by taking an array of todo IDs to be updated all at once:</p>
+
+<p>1. To start off, wrap everything already inside <code>save()</code>
+with a <code>chrome.storage.local.get()</code> callback:</p>
+
+<pre data-filename="store.js">
+Store.prototype.save = function (id, updateData, callback) {
+ <b>chrome.storage.local.get(this._dbName, function(storage) {</b>
+ <b> </b>var data = JSON.parse(localStorage[this._dbName]);
+ <b> </b>// ...
+ <b> </b>if (typeof id !== 'object') {
+ <b> </b> // ...
+ <b> </b>}else {
+ <b> </b> // ...
+ <b> </b>}
+ <b>}.bind(this));</b>
+};
+</pre>
+
+<p>2. Convert all the <code>localStorage</code> instances with <code>chrome.storage.local</code>:</p>
+
+<pre data-filename="store.js">
+Store.prototype.save = function (id, updateData, callback) {
+ chrome.storage.local.get(this._dbName, function(storage) {
+ <strike>var data = JSON.parse(localStorage[this._dbName]);</strike>
+ <b>var data = storage[this._dbName];</b>
+ var todos = data.todos;
+
+ callback = callback || function () {};
+
+ // If an ID was actually given, find the item and update each property
+ if ( typeof id !== 'object' ) {
+ // ...
+
+ <strike>localStorage[this._dbName] = JSON.stringify(data);</strike>
+ <strike>callback.call(this, JSON.parse(localStorage[this._dbName]).todos);</strike>
+ <b>chrome.storage.local.set(storage, function() {</b>
+ <b> chrome.storage.local.get(this._dbName, function(storage) {</b>
+ <b> callback.call(this, storage[this._dbName].todos);</b>
+ <b> }.bind(this));</b>
+ <b>}.bind(this));</b>
+ } else {
+ callback = updateData;
+
+ updateData = id;
+
+ // Generate an ID
+ updateData.id = new Date().getTime();
+
+ <strike>localStorage[this._dbName] = JSON.stringify(data);</strike>
+ <strike>callback.call(this, [updateData]);</strike>
+ <b>chrome.storage.local.set(storage, function() {</b>
+ <b> callback.call(this, [updateData]);</b>
+ <b>}.bind(this));</b>
+ }
+ }.bind(this));
+};
+</pre>
+
+<p>3. Then update the logic to operate on an array instead of a single item:</p>
+
+<pre data-filename="store.js">
+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' <b>|| Array.isArray(id)</b> ) {
+ <b>var ids = [].concat( id );</b>
+ <b>ids.forEach(function(id) {</b>
+ for (var i = 0; i &lt; todos.length; i++) {
+ if (todos[i].id == id) {
+ for (var x in updateData) {
+ todos[i][x] = updateData[x];
+ }
+ }
+ }
+ <b>});</b>
+
+ 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();
+
+ <b>todos.push(updateData);</b>
+ chrome.storage.local.set(storage, function() {
+ callback.call(this, [updateData]);
+ }.bind(this));
+ }
+ }.bind(this));
+};
+</pre>
+
+<h3 id="complete-items">Mark todo items as complete</h3>
+
+<p>Now that we are operating on arrays, we'll need to change how we handle a user clicking on the <b>Clear completed (#)</b> button:</p>
+
+<p>1. In <strong><em>controller.js</em></strong>, update <code>toggleAll()</code> to call <code>toggleComplete()</code>
+only once with an array of todos instead of marking a todo as completed
+one by one. Also delete the call to <code>_filter()</code> since we'll be adjusting <code>toggleComplete</code>'s <code>_filter()</code>.</p>
+
+<pre data-filename="controller.js">
+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) {
+ <b>var ids = [];</b>
+ data.forEach(function (item) {
+ <strike>this.toggleComplete(item.id, e.target, true);</strike>
+ <b>ids.push(item.id);</b>
+ }.bind(this));
+ <b>this.toggleComplete(ids, e.target, false);</b>
+ }.bind(this));
+
+ <strike>this._filter();</strike>
+};
+</pre>
+
+<p>2. Now we need to update <code>toggleComplete()</code> to accept both a single todo or an array of todos. This includes moving <code>filter()</code> to be inside the <code>update()</code>, instead of outside.</p>
+
+<pre data-filename="controller.js">
+Controller.prototype.toggleComplete = function (<strike>id</strike> <b>ids</b>, checkbox, silent) {
+ var completed = checkbox.checked ? 1 : 0;
+ this.model.update(<strike>id</strike> <b>ids</b>, { completed: completed }, function () {
+ <b>if ( ids.constructor != Array ) {</b>
+ <b> ids = [ ids ];</b>
+ <b>}</b>
+ <b>ids.forEach( function(id) {</b>
+ 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;
+ <b>});</b>
+
+ <b>if (!silent) {</b>
+ <b> this._filter();</b>
+ <b>}</b>
+
+ }<b>.bind(this)</b>);
+
+ <strike>if (!silent) {</strike>
+ <strike> this._filter();</strike>
+ <strike>}</strike>
+};
+</pre>
+
+<h3 id="count-items">Count todo items</h3>
+
+<p>After switching to async storage, there is a minor bug that shows up when getting the number of todos. We'll need to wrap the count operation in a callback function:</p>
+
+<p>1. In <strong><em>model.js</em></strong>, update <code>getCount()</code> to accept a callback:</p>
+
+<pre data-filename="model.js">
+ Model.prototype.getCount = function (<b>callback</b>) {
+ 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++;
+ });
+ <b>if (callback) callback(todos);</b>
+ });
+ <strike>return todos;</strike>
+};
+</pre>
+
+<p>2. Back in <strong><em>controller.js</em></strong>, update <code>_updateCount()</code> to use
+the async <code>getCount()</code> you edited in the previous step:</p>
+
+<pre data-filename="controller.js">
+Controller.prototype._updateCount = function () {
+ <strike>var todos = this.model.getCount();</strike>
+ <b>this.model.getCount(function(todos) {</b>
+ <b> </b>this.$todoItemCounter.innerHTML = this.view.itemCounter(todos.active);
+ <b> </b>
+ <b> </b>this.$clearCompleted.innerHTML = this.view.clearCompletedButton(todos.completed);
+ <b> </b>this.$clearCompleted.style.display = todos.completed > 0 ? 'block' : 'none';
+ <b> </b>
+ <b> </b>this.$toggleAll.checked = todos.completed === todos.total;
+ <b> </b>
+ <b> </b>this._toggleFrame(todos);
+ <b>}.bind(this));</b>
+
+};
+</pre>
+
+<p>We are almost there! If you reload the app now, you will be able to insert new
+todos without any console errors.</p>
+
+<h3 id="remove-items">Remove todos items</h3>
+
+<p>Now that we can save todo items, we're close to being done!
+However we get errors when we attempt to <em>remove</em> our todo items:</p>
+
+<figure>
+ <img src="{{static}}/images/app_codelab/remove-todo-console-error.png" alt="Todo app with localStorage console log error">
+</figure>
+
+<p>1. In <strong><em>store.js</em></strong>, we'll need to convert all the <code>localStorage</code> instances to use <code>chrome.storage.local</code>:</p>
+
+<p>a) To start off, wrap everything already inside <code>remove()</code> with a <code>get()</code> callback:</p>
+
+<pre data-filename="store.js">
+Store.prototype.remove = function (id, callback) {
+ <b>chrome.storage.local.get(this._dbName, function(storage) {</b>
+ <b> </b>var data = JSON.parse(localStorage[this._dbName]);
+ <b> </b>var todos = data.todos;
+ <b> </b>
+ <b> </b>for (var i = 0; i < todos.length; i++) {
+ <b> </b> if (todos[i].id == id) {
+ <b> </b> todos.splice(i, 1);
+ <b> </b> break;
+ <b> </b> }
+ <b> </b>}
+ <b> </b>
+ <b> </b>localStorage[this._dbName] = JSON.stringify(data);
+ <b> </b>callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
+ <b>}.bind(this));</b>
+};
+</pre>
+
+<p>b) Then convert the contents within the <code>get()</code> callback:</p>
+
+<pre data-filename="store.js">
+Store.prototype.remove = function (id, callback) {
+ chrome.storage.local.get(this._dbName, function(storage) {
+ <strike>var data = JSON.parse(localStorage[this._dbName]);</strike>
+ <b>var data = storage[this._dbName];</b>
+ var todos = data.todos;
+
+ for (var i = 0; i &lt; todos.length; i++) {
+ if (todos[i].id == id) {
+ todos.splice(i, 1);
+ break;
+ }
+ }
+
+ <strike>localStorage[this._dbName] = JSON.stringify(data);</strike>
+ <strike>callback.call(this, JSON.parse(localStorage[this._dbName]).todos);</strike>
+ <b>chrome.storage.local.set(storage, function() {</b>
+ <b> callback.call(this, todos);</b>
+ <b>}.bind(this));</b>
+ }.bind(this));
+};
+</pre>
+
+<p>2. The same Read-After-Write data hazard issue previously present in the
+<code>save()</code> method is also present when removing items so we'll need
+to update a few more places to allow for batch operations on a list of todo IDs.</p>
+
+<p>a) Still in <em>store.js</em>, update <code>remove()</code>:</p>
+
+<pre data-filename="store.js">
+Store.prototype.remove = function (id, callback) {
+ chrome.storage.local.get(this._dbName, function(storage) {
+ var data = storage[this._dbName];
+ var todos = data.todos;
+
+ <b>var ids = [].concat(id);</b>
+ <b>ids.forEach( function(id) {</b>
+ <b> </b>for (var i = 0; i &lt; todos.length; i++) {
+ <b> </b> if (todos[i].id == id) {
+ <b> </b> todos.splice(i, 1);
+ <b> </b> break;
+ <b> </b> }
+ <b> </b>}
+ <b>});</b>
+
+ chrome.storage.local.set(storage, function() {
+ callback.call(this, todos);
+ }.bind(this));
+ }.bind(this));
+};
+</pre>
+
+<p>b) In <strong><em>controller.js</em></strong>, change <code>removeCompletedItems()</code> to
+make it call <code>removeItem()</code> on all IDs at once:</p>
+
+<pre data-filename="controller.js">
+Controller.prototype.removeCompletedItems = function () {
+ this.model.read({ completed: 1 }, function (data) {
+ <b>var ids = [];</b>
+ data.forEach(function (item) {
+ <strike>this.removeItem(item.id);</strike>
+ <b>ids.push(item.id);</b>
+ }.bind(this));
+ <b>this.removeItem(ids);</b>
+ }.bind(this));
+
+ this._filter();
+};
+</pre>
+
+<p>c) Finally, still in <em>controller.js</em>, change the <code>removeItem()</code> to support
+ removing multiple items from the DOM at once, and move the <code>_filter()</code> call to be inside the callback:</p>
+
+<pre data-filename="controller.js">
+Controller.prototype.removeItem = function (id) {
+ this.model.remove(id, function () {
+ <b>var ids = [].concat(id);</b>
+ <b>ids.forEach( function(id) {</b>
+ <b> </b>this.$todoList.removeChild($$('[data-id="' + id + '"]'));
+ <b>}.bind(this));</b>
+ <b>this._filter();</b>
+ }.bind(this));
+ <strike>this._filter();</strike>
+};
+</pre>
+
+<h3 id="drop-items">Drop all todo items</h3>
+
+<p>There is one more method in <em>store.js</em> using <code>localStorage</code>:</p>
+
+<pre data-filename="store.js">
+Store.prototype.drop = function (callback) {
+ localStorage[this._dbName] = JSON.stringify({todos: []});
+ callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
+};
+</pre>
+
+<p>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 <code><a href="/apps/storage#method-StorageArea-remove">chrome.storage.local.clear()</a></code>.</p>
+
+<h2 id="launch">Launch your finished Todo app</h2>
+
+<p>You are done Step 2! Reload your app and you should now have
+a fully working Chrome packaged version of TodoMVC.</p>
+
+<figure>
+ <img src="{{static}}/images/app_codelab/step2-completed.gif" alt="The finished Todo app after Step 2">
+</figure>
+
+<p class="note">
+ <strong>Troubleshooting</strong>
+ <br>
+ Remember to always check the DevTools Console to see if there are any error messages.
+</p>
+
+<h2 id="recap">Recap APIs referenced in this step</h2>
+
+<p>For more detailed information about some of the APIs introduced in this step, refer to:</p>
+
+<ul>
+ <li>
+ <a href="/apps/contentSecurityPolicy" title="Read 'Content Security Policy' in the Chrome developer docs">Content Security Policy</a>
+ <a href="#csp-compliance" class="anchor-link-icon" title="This feature mentioned in 'Make scripts Content Security Policy (CSP) compliant'">&#8593;</a>
+ </li>
+ <li>
+ <a href="/apps/declare_permissions" title="Read 'Declare Permissions' in the Chrome developer docs">Declare Permissions</a>
+ <a href="#update-permissions" class="anchor-link-icon" title="This feature mentioned in 'Update app permissions'">&#8593;</a>
+ </li>
+ <li>
+ <a href="/apps/storage" title="Read 'chrome.storage' in the Chrome developer docs">chrome.storage</a>
+ <a href="#get-and-set" class="anchor-link-icon" title="This feature mentioned in 'Learn about local.storage.set() and local.storage.get()'">&#8593;</a>
+ </li>
+ <li>
+ <a href="/apps/storage#method-StorageArea-get" title="Read 'chrome.storage.local.get()' in the Chrome developer docs">chrome.storage.local.get()</a>
+ <a href="#get-and-set" class="anchor-link-icon" title="This feature mentioned in 'Learn about local.storage.set() and local.storage.get()'">&#8593;</a>
+ <a href="#retrieve-items" class="anchor-link-icon" title="This feature mentioned in 'Retrieve todos items'">&#8593;</a>
+ </li>
+ <li>
+ <a href="/apps/storage#method-StorageArea-set" title="Read 'chrome.storage.local.set()' in the Chrome developer docs">chrome.storage.local.set()</a>
+ <a href="/apps/storage#method-StorageArea-get" title="Read 'chrome.storage.local.get()' in the Chrome developer docs">chrome.storage.local.get()</a>
+ <a href="#save-items" class="anchor-link-icon" title="This feature mentioned in 'Save todos items'">&#8593;</a>
+ </li>
+ <li>
+ <a href="/apps/storage#method-StorageArea-remove" title="Read 'chrome.storage.local.remove()' in the Chrome developer docs">chrome.storage.local.remove()</a>
+ <a href="#remove-items" class="anchor-link-icon" title="This feature mentioned in 'Remove todos items'">&#8593;</a>
+ </li>
+ <li>
+ <a href="/apps/storage#method-StorageArea-remove" title="Read 'chrome.storage.local.clear()' in the Chrome developer docs">chrome.storage.local.clear()</a>
+ <a href="#remove-items" class="anchor-link-icon" title="This feature mentioned in 'Drop all todo items'">&#8593;</a>
+ </li>
+</ul>
+
+<p>Ready to continue onto the next step? Go to <a href="app_codelab_alarms.html">Step 3 - Add alarms and notifications &raquo;</a></p>

Powered by Google App Engine
This is Rietveld 408576698