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

Side by Side 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, 2 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 unified diff | Download patch
OLDNEW
(Empty)
1 <h1 id="import-existing-app">
2 <span class="h1-step">Step 2:</span>
3 Import an Existing Web App
4 </h1>
5
6 <p class="note">
7 <strong>Want to start fresh from here?</strong>
8 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>.
9 </p>
10
11 <p>In this step, you will learn:</p>
12
13 <ul>
14 <li>How to adapt an existing web application for the Chrome Apps platform.</li >
15 <li>How to make your app scripts Content Security Policy (CSP) compliant.</li>
16 <li>How to implement local storage using the <code>chrome.storage.local</code> API.</li>
17 </ul>
18
19 <p>
20 <em>Estimated time to complete this step: 20 minutes.</em>
21 <br>
22 To preview what you will complete in this step, <a href="#launch">jump down to the bottom of this page &#8595;</a>.
23 </p>
24
25 <h2 id="todomvc">Import an existing Todo app</h2>
26
27 <p>As a starting point, we will import the <a href="http://todomvc.com/vanilla-e xamples/vanillajs/">vanilla
28 JavaScript version</a> of <a href="http://todomvc.com/">TodoMVC</a>, a common be nchmark app, into our project.</p>
29
30 <p>We've included a version of the TodoMVC app in the
31 <a href="https://github.com/mangini/io13-codelab/archive/master.zip">reference c ode zip</a> in the <strong><em>todomvc</em></strong> folder.
32 Copy all files (including folders) from <em>todomvc</em> into your project folde r.</p>
33
34 <figure>
35 <img src="{{static}}/images/app_codelab/copy-todomvc.png" alt="Copy todomvc fo lder into codelab folder">
36 </figure>
37
38 <p>You will be asked to replace <em>index.html</em>. Go ahead and accept.</p>
39
40 <figure>
41 <img src="{{static}}/images/app_codelab/replace-index.png" alt="Replace index. html"><br>
42 </figure>
43
44 <p>You should now have the following file structure in your application folder:< /p>
45
46 <figure>
47 <img src="{{static}}/images/app_codelab/todomvc-copied.png" alt="New project f older">
48 <figcaption>The files highlighted in blue are from the <em>todomvc</em> folder .</figcaption>
49 <!--
50 <ul>
51 <li><strong>background.js</strong> (from step 1)</li>
52 <li><strong>bower_components/</strong> (from todomvc)</li>
53 <li><strong>bower.json</strong> (from todomvc)</li>
54 <li><strong>icon_128.png</strong> (from step 1)</li>
55 <li><strong>index.html</strong> (from todomvc)</li>
56 <li><strong>js/</strong> (from todomvc)</li>
57 <li><strong>manifest.json</strong> (from step 1)</li>
58 </ul>
59 -->
60 </figure>
61
62
63 <p>Reload your app now (<b>right-click > Reload App</b>). You should see the bas ic UI but you won't be able to add todos.</p>
64
65 <h2 id="csp-compliance">Make scripts Content Security Policy (CSP) compliant</h2 >
66
67 <p>Open the DevTools Console (<strong>right-click > Inspect Element</strong>, th en select the <strong>Console</strong> tab). You will see an error about refusin g to execute an inline script:</p>
68
69 <figure>
70 <img src="{{static}}/images/app_codelab/csp-console-error.png" alt="Todo app w ith CSP console log error">
71 <!--
72 <blockquote>
73 > Refused to execute inline script because it violates the following Content <br>
74 > Security Policy directive: "default-src 'self' chrome-extension-resource:" . <br>
75 > Note that 'script-src' was not explicitly set, so 'default-src' is used as a <br>
76 > fallback. <br>
77 > index.html:42
78 </blockquote>
79 -->
80 </figure>
81
82 <p>Let's fix this error by making the app <a href="/apps/contentSecurityPolicy"> Content Security Policy</a> compliant.
83 One of the most common CSP non-compliances is caused by inline JavaScript. Examp les of inline JavaScript include event
84 handlers as DOM attributes (e.g. <code>&lt;button onclick=''&gt;</code>) and <co de>&lt;script&gt;</code> tags with
85 content inside the HTML.</p>
86
87 <p>The solution is simple: move the inline content to a new file.</p>
88
89 <p>1. Near the bottom of <strong><em>index.html</em></strong>, remove the inline
90 JavaScript and instead include <em>js/bootstrap.js</em>:</p>
91
92 <pre data-filename="index.html">
93 &lt;script src="bower_components/director/build/director.js">&lt;/script>
94 <strike>&lt;script&gt;</strike>
95 <strike> // Bootstrap app data</strike>
96 <strike> window.app = {};</strike>
97 <strike>&lt;/script&gt;</strike>
98 <b>&lt;script src="js/bootstrap.js"&gt;&lt;/script&gt;</b>
99 &lt;script src="js/helpers.js">&lt;/script>
100 &lt;script src="js/store.js">&lt;/script>
101 </pre>
102
103 <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>
104
105 <pre data-filename="bootstrap.js">
106 // Bootstrap app data
107 window.app = {};
108 </pre>
109
110 <p>You'll still have a non-working Todo app if you reload the app now but we're getting there.</p>
111
112 <h2 id="convert-storage">Convert localStorage to chrome.storage.local</h2>
113
114 <p>If you open the DevTools Console now, the previous error should be gone. Howe ver there is a new error about <code>window.localStorage</code> not being availa ble:</p>
115
116 <figure>
117 <img src="{{static}}/images/app_codelab/localStorage-console-error.png" alt="T odo app with localStorage console log error">
118 <!--
119 <blockquote>
120 > Uncaught window.localStorage is not available in packaged apps. Use <br>
121 > chrome.storage.local instead. store.js:21
122 </blockquote>
123 -->
124 </figure>
125
126 <p><a href="http://dev.w3.org/html5/webstorage/#the-localstorage-attribute"><cod e>LocalStorage</code></a>
127 is not supported in Chrome Apps because <code>LocalStorage</code> is
128 synchronous. Synchronous access to blocking resources (I/O) in a single threaded
129 runtime could make your app become unresponsive.</p>
130
131 <p>Chrome Apps have an equivalent API that can store objects asynchronously.
132 This will help avoid the sometimes costly object->string->object serialization p rocess.</p>
133
134 <p>To address the error message in our app, we will need to convert <code>LocalS torage</code> to
135 <code>chrome.storage.local</code>.</p>
136
137 <h3 id="update-permissions">Update app permissions</h3>
138
139 <p>In order to use <code>chrome.storage.local</code>, we need to request the <co de>storage</code> permission. In <strong><em>manifest.json</em></strong>, add <c ode>"storage"</code> to the <code>permissions</code> array:</p>
140
141 <pre data-filename="manifest.json">
142 "permissions": [<b>"storage"</b>],
143 </pre>
144
145 <h3 id="get-and-set">Learn about local.storage.set() and local.storage.get()</h3 >
146
147 <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>
148
149 <p>The <code>set()</code> method accepts an object of key-value pairs as its fir st parameter. An optional callback function is the second parameter. For example :</p>
150
151 <pre>
152 chrome.storage.local.set({secretMessage:'Psst!',timeSet:Date.now()}, function() {
153 console.log("Secret message saved");
154 });
155 </pre>
156
157 <p>The <code>get()</code> method accepts an optional first parameter for the dat astore keys you wish to retreive. A single key can be passed as a string; multip le keys can be arranged into an array of strings or a dictionary object.</p>
158
159 <p>The second parameter, which is required, is a callback function. In the retur ned object, use the keys requested in the first parameter to access the stored v alues. For example:</p>
160
161 <pre>
162 chrome.storage.local.get(['secretMessage','timeSet'], function(data) {
163 console.log("The secret message:", data.secretMessage, "saved at:", data.timeS et);
164 });
165 </pre>
166
167 <p>If you want to <code>get()</code> everything that is currently in <code>chrom e.storage.local</code>,
168 omit the first parameter:</p>
169
170 <pre>
171 chrome.storage.local.get(function(data) {
172 console.log(data);
173 });
174 </pre>
175
176 <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>
177
178 <figure>
179 <img src="{{static}}/images/app_codelab/get-set-in-console.png" alt="Use the C onsole to debug chrome.storage">
180 </figure>
181
182 <h3 id="preview-changes">Preview required API changes</h3>
183
184 <p>There are many remaining steps in converting the Todo app however they are al l small changes to
185 the API calls. Changing all the places where <code>localStorage</code> is curren tly being used
186 will be time-consuming and error-prone &mdash; but required.</p>
187
188 <p class="note">
189 To maximize your fun with this codelab, it'll be best if you overwrite your
190 <strong><em>store.js</em></strong>, <strong><em>controller.js</em></strong>, a nd <strong><em>model.js</em></strong>
191 with the ones from <strong><em>cheat_code/solution_for_step_2</em></strong> in the reference code zip.
192 <br><br>
193 Once you've done that, continue reading as we'll go over each of the changes i ndividually.
194 </p>
195
196 <p>The key differences between <code>localStorage</code> and <code>chrome.storag e</code> come from the async nature of <code>chrome.storage</code>:</p>
197
198 <ul>
199 <li>
200 Instead of writing to <code>localStorage</code> using simple assignment, we need to use <code>chrome.storage.local.set()</code> with optional callbacks.
201 <pre>
202 var data = { todos: [] };
203 localStorage[dbName] = JSON.stringify(data);
204 </pre>
205 versus
206 <pre>
207 var storage = {};
208 storage[dbName] = { todos: [] };
209 chrome.storage.local.set( storage, function() {
210 // optional callback
211 });
212 </pre>
213 </li>
214 <li>
215 Instead of accessing <code>localStorage[myStorageName]</code> directly, we n eed to use <code>chrome.storage.local.get(myStorageName,function(storage){...})< /code> and then parse the returned <code>storage</code> object in the callback f unction.
216 <pre>
217 var todos = JSON.parse(localStorage[dbName]).todos;
218 </pre>
219 versus
220 <pre>
221 chrome.storage.local.get(dbName, function(storage) {
222 var todos = storage[dbName].todos;
223 });
224 </pre>
225 </li>
226 <li>
227 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> pro totype. (More info on bound functions can be found on the MDN docs: <a href="htt ps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Fu nction/bind">Function.prototype.bind()</a>.)
228 <pre>
229 function Store() {
230 this.scope = 'inside Store';
231 chrome.storage.local.set( {}, function() {
232 console.log(this.scope); // outputs: 'undefined'
233 });
234 }
235 new Store();
236 </pre>
237 versus
238 <pre>
239 function Store() {
240 this.scope = 'inside Store';
241 chrome.storage.local.set( {}, function() {
242 console.log(this.scope); // outputs: 'inside Store'
243 }<b>.bind(this)</b>);
244 }
245 new Store();
246 </pre>
247 </li>
248 </ul>
249
250 <p>Keep these key differences in mind as we go over retrieving, saving, and remo ving todo items in the following sections.</p>
251
252 <h3 id="retrieve-items">Retrieve todos items</h3>
253
254 Let's update the Todo app in order to retrieve todo items:
255
256 <p>1. The <code>Store</code> constructor method takes care of initializing the T odo app with all the existing todo items from the datastore. If this is the firs t time the app has been loaded, the datastore might not exist so the method chec ks 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 err ors.</p>
257
258 <p>In <strong><em>js/store.js</em></strong>, convert the use of <code>localStora ge</code> in the constructor method to instead use
259 <code>chrome.storage.local</code>:</p>
260
261 <pre data-filename="store.js">
262 function Store(name, callback) {
263 var data;
264 var dbName;
265
266 callback = callback || function () {};
267
268 dbName = this._dbName = name;
269
270 <strike>if (!localStorage[dbName]) {</strike>
271 <strike> data = {</strike>
272 <strike> todos: []</strike>
273 <strike> };</strike>
274 <strike> localStorage[dbName] = JSON.stringify(data);</strike>
275 <strike>}</strike>
276 <strike>callback.call(this, JSON.parse(localStorage[dbName]));</strike>
277
278 <b>chrome.storage.local.get(dbName, function(storage) {</b>
279 <b> if ( dbName in storage ) {</b>
280 <b> callback.call(this, storage[dbName].todos);</b>
281 <b> } else {</b>
282 <b> storage = {};</b>
283 <b> storage[dbName] = { todos: [] };</b>
284 <b> chrome.storage.local.set( storage, function() {</b>
285 <b> callback.call(this, storage[dbName].todos);</b>
286 <b> }.bind(this));</b>
287 <b> }</b>
288 <b>}.bind(this));</b>
289 }
290 </pre>
291
292 <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>
293
294 <p>Convert <code>find()</code> to use <code>chrome.storage.local</code>:</p>
295
296 <pre data-filename="store.js">
297 Store.prototype.find = function (query, callback) {
298 if (!callback) {
299 return;
300 }
301
302 <strike>var todos = JSON.parse(localStorage[this._dbName]).todos;</strike>
303
304 <strike>callback.call(this, todos.filter(function (todo) {</strike>
305 <b>chrome.storage.local.get(this._dbName, function(storage) {</b>
306 <b> var todos = storage[this._dbName].todos.filter(function (todo) {</b>
307 <b> </b>for (var q in query) {
308 <b> </b> return query[q] === todo[q];
309 <b> </b>}
310 <b> });</b>
311 <b> callback.call(this, todos);</b>
312 <b>}.bind(this));</b>
313 <strike>}));</strike>
314 };
315 </pre>
316
317 <p>3. Similiar to <code>find()</code>, <code>findAll()</code> gets all todos fro m the Model. Convert <code>findAll()</code> to use <code>chrome.storage.local</c ode>:</p>
318
319 <pre data-filename="store.js">
320 Store.prototype.findAll = function (callback) {
321 callback = callback || function () {};
322 <strike>callback.call(this, JSON.parse(localStorage[this._dbName]).todos);</st rike>
323 <b>chrome.storage.local.get(this._dbName, function(storage) {</b>
324 <b> var todos = storage[this._dbName] && storage[this._dbName].todos || [];</ b>
325 <b> callback.call(this, todos);</b>
326 <b>}.bind(this));</b>
327 };
328 </pre>
329
330 <h3 id="save-items">Save todos items</h3>
331
332 <p>The current <code>save()</code> method presents a challenge. It depends on tw o async
333 operations (get and set) that operate on the whole monolithic JSON storage
334 every time. Any batch updates on more than one todo item, like "mark all todos a s
335 completed", will result in a data hazard known as
336 <a href="http://en.wikipedia.org/wiki/Hazard_(computer_architecture)#Read_After_ Write_.28RAW.29">Read-After-Write</a>.
337 This issue wouldn't happen if we were using a more appropriate data storage,
338 like IndexedDB, but we are trying to minimize the conversion effort for this
339 codelab.</p>
340
341 <p>There are several ways to fix it so we will use this opportunity to slightly
342 refactor <code>save()</code> by taking an array of todo IDs to be updated all at once:</p>
343
344 <p>1. To start off, wrap everything already inside <code>save()</code>
345 with a <code>chrome.storage.local.get()</code> callback:</p>
346
347 <pre data-filename="store.js">
348 Store.prototype.save = function (id, updateData, callback) {
349 <b>chrome.storage.local.get(this._dbName, function(storage) {</b>
350 <b> </b>var data = JSON.parse(localStorage[this._dbName]);
351 <b> </b>// ...
352 <b> </b>if (typeof id !== 'object') {
353 <b> </b> // ...
354 <b> </b>}else {
355 <b> </b> // ...
356 <b> </b>}
357 <b>}.bind(this));</b>
358 };
359 </pre>
360
361 <p>2. Convert all the <code>localStorage</code> instances with <code>chrome.stor age.local</code>:</p>
362
363 <pre data-filename="store.js">
364 Store.prototype.save = function (id, updateData, callback) {
365 chrome.storage.local.get(this._dbName, function(storage) {
366 <strike>var data = JSON.parse(localStorage[this._dbName]);</strike>
367 <b>var data = storage[this._dbName];</b>
368 var todos = data.todos;
369
370 callback = callback || function () {};
371
372 // If an ID was actually given, find the item and update each property
373 if ( typeof id !== 'object' ) {
374 // ...
375
376 <strike>localStorage[this._dbName] = JSON.stringify(data);</strike>
377 <strike>callback.call(this, JSON.parse(localStorage[this._dbName]).todos); </strike>
378 <b>chrome.storage.local.set(storage, function() {</b>
379 <b> chrome.storage.local.get(this._dbName, function(storage) {</b>
380 <b> callback.call(this, storage[this._dbName].todos);</b>
381 <b> }.bind(this));</b>
382 <b>}.bind(this));</b>
383 } else {
384 callback = updateData;
385
386 updateData = id;
387
388 // Generate an ID
389 updateData.id = new Date().getTime();
390
391 <strike>localStorage[this._dbName] = JSON.stringify(data);</strike>
392 <strike>callback.call(this, [updateData]);</strike>
393 <b>chrome.storage.local.set(storage, function() {</b>
394 <b> callback.call(this, [updateData]);</b>
395 <b>}.bind(this));</b>
396 }
397 }.bind(this));
398 };
399 </pre>
400
401 <p>3. Then update the logic to operate on an array instead of a single item:</p>
402
403 <pre data-filename="store.js">
404 Store.prototype.save = function (id, updateData, callback) {
405 chrome.storage.local.get(this._dbName, function(storage) {
406 var data = storage[this._dbName];
407 var todos = data.todos;
408
409 callback = callback || function () {};
410
411 // If an ID was actually given, find the item and update each property
412 if ( typeof id !== 'object' <b>|| Array.isArray(id)</b> ) {
413 <b>var ids = [].concat( id );</b>
414 <b>ids.forEach(function(id) {</b>
415 for (var i = 0; i &lt; todos.length; i++) {
416 if (todos[i].id == id) {
417 for (var x in updateData) {
418 todos[i][x] = updateData[x];
419 }
420 }
421 }
422 <b>});</b>
423
424 chrome.storage.local.set(storage, function() {
425 chrome.storage.local.get(this._dbName, function(storage) {
426 callback.call(this, storage[this._dbName].todos);
427 }.bind(this));
428 }.bind(this));
429 } else {
430 callback = updateData;
431
432 updateData = id;
433
434 // Generate an ID
435 updateData.id = new Date().getTime();
436
437 <b>todos.push(updateData);</b>
438 chrome.storage.local.set(storage, function() {
439 callback.call(this, [updateData]);
440 }.bind(this));
441 }
442 }.bind(this));
443 };
444 </pre>
445
446 <h3 id="complete-items">Mark todo items as complete</h3>
447
448 <p>Now that we are operating on arrays, we'll need to change how we handle a use r clicking on the <b>Clear completed (#)</b> button:</p>
449
450 <p>1. In <strong><em>controller.js</em></strong>, update <code>toggleAll()</code > to call <code>toggleComplete()</code>
451 only once with an array of todos instead of marking a todo as completed
452 one by one. Also delete the call to <code>_filter()</code> since we'll be adjust ing <code>toggleComplete</code>'s <code>_filter()</code>.</p>
453
454 <pre data-filename="controller.js">
455 Controller.prototype.toggleAll = function (e) {
456 var completed = e.target.checked ? 1 : 0;
457 var query = 0;
458 if (completed === 0) {
459 query = 1;
460 }
461 this.model.read({ completed: query }, function (data) {
462 <b>var ids = [];</b>
463 data.forEach(function (item) {
464 <strike>this.toggleComplete(item.id, e.target, true);</strike>
465 <b>ids.push(item.id);</b>
466 }.bind(this));
467 <b>this.toggleComplete(ids, e.target, false);</b>
468 }.bind(this));
469
470 <strike>this._filter();</strike>
471 };
472 </pre>
473
474 <p>2. Now we need to update <code>toggleComplete()</code> to accept both a singl e todo or an array of todos. This includes moving <code>filter()</code> to be in side the <code>update()</code>, instead of outside.</p>
475
476 <pre data-filename="controller.js">
477 Controller.prototype.toggleComplete = function (<strike>id</strike> <b>ids</b>, checkbox, silent) {
478 var completed = checkbox.checked ? 1 : 0;
479 this.model.update(<strike>id</strike> <b>ids</b>, { completed: completed }, fu nction () {
480 <b>if ( ids.constructor != Array ) {</b>
481 <b> ids = [ ids ];</b>
482 <b>}</b>
483 <b>ids.forEach( function(id) {</b>
484 var listItem = $$('[data-id="' + id + '"]');
485
486 if (!listItem) {
487 return;
488 }
489
490 listItem.className = completed ? 'completed' : '';
491
492 // In case it was toggled from an event and not by clicking the checkbox
493 listItem.querySelector('input').checked = completed;
494 <b>});</b>
495
496 <b>if (!silent) {</b>
497 <b> this._filter();</b>
498 <b>}</b>
499
500 }<b>.bind(this)</b>);
501
502 <strike>if (!silent) {</strike>
503 <strike> this._filter();</strike>
504 <strike>}</strike>
505 };
506 </pre>
507
508 <h3 id="count-items">Count todo items</h3>
509
510 <p>After switching to async storage, there is a minor bug that shows up when get ting the number of todos. We'll need to wrap the count operation in a callback f unction:</p>
511
512 <p>1. In <strong><em>model.js</em></strong>, update <code>getCount()</code> to a ccept a callback:</p>
513
514 <pre data-filename="model.js">
515 Model.prototype.getCount = function (<b>callback</b>) {
516 var todos = {
517 active: 0,
518 completed: 0,
519 total: 0
520 };
521 this.storage.findAll(function (data) {
522 data.each(function (todo) {
523 if (todo.completed === 1) {
524 todos.completed++;
525 } else {
526 todos.active++;
527 }
528 todos.total++;
529 });
530 <b>if (callback) callback(todos);</b>
531 });
532 <strike>return todos;</strike>
533 };
534 </pre>
535
536 <p>2. Back in <strong><em>controller.js</em></strong>, update <code>_updateCount ()</code> to use
537 the async <code>getCount()</code> you edited in the previous step:</p>
538
539 <pre data-filename="controller.js">
540 Controller.prototype._updateCount = function () {
541 <strike>var todos = this.model.getCount();</strike>
542 <b>this.model.getCount(function(todos) {</b>
543 <b> </b>this.$todoItemCounter.innerHTML = this.view.itemCounter(todos.active) ;
544 <b> </b>
545 <b> </b>this.$clearCompleted.innerHTML = this.view.clearCompletedButton(todos .completed);
546 <b> </b>this.$clearCompleted.style.display = todos.completed > 0 ? 'block' : 'none';
547 <b> </b>
548 <b> </b>this.$toggleAll.checked = todos.completed === todos.total;
549 <b> </b>
550 <b> </b>this._toggleFrame(todos);
551 <b>}.bind(this));</b>
552
553 };
554 </pre>
555
556 <p>We are almost there! If you reload the app now, you will be able to insert ne w
557 todos without any console errors.</p>
558
559 <h3 id="remove-items">Remove todos items</h3>
560
561 <p>Now that we can save todo items, we're close to being done!
562 However we get errors when we attempt to <em>remove</em> our todo items:</p>
563
564 <figure>
565 <img src="{{static}}/images/app_codelab/remove-todo-console-error.png" alt="To do app with localStorage console log error">
566 </figure>
567
568 <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>
569
570 <p>a) To start off, wrap everything already inside <code>remove()</code> with a <code>get()</code> callback:</p>
571
572 <pre data-filename="store.js">
573 Store.prototype.remove = function (id, callback) {
574 <b>chrome.storage.local.get(this._dbName, function(storage) {</b>
575 <b> </b>var data = JSON.parse(localStorage[this._dbName]);
576 <b> </b>var todos = data.todos;
577 <b> </b>
578 <b> </b>for (var i = 0; i < todos.length; i++) {
579 <b> </b> if (todos[i].id == id) {
580 <b> </b> todos.splice(i, 1);
581 <b> </b> break;
582 <b> </b> }
583 <b> </b>}
584 <b> </b>
585 <b> </b>localStorage[this._dbName] = JSON.stringify(data);
586 <b> </b>callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
587 <b>}.bind(this));</b>
588 };
589 </pre>
590
591 <p>b) Then convert the contents within the <code>get()</code> callback:</p>
592
593 <pre data-filename="store.js">
594 Store.prototype.remove = function (id, callback) {
595 chrome.storage.local.get(this._dbName, function(storage) {
596 <strike>var data = JSON.parse(localStorage[this._dbName]);</strike>
597 <b>var data = storage[this._dbName];</b>
598 var todos = data.todos;
599
600 for (var i = 0; i &lt; todos.length; i++) {
601 if (todos[i].id == id) {
602 todos.splice(i, 1);
603 break;
604 }
605 }
606
607 <strike>localStorage[this._dbName] = JSON.stringify(data);</strike>
608 <strike>callback.call(this, JSON.parse(localStorage[this._dbName]).todos);</ strike>
609 <b>chrome.storage.local.set(storage, function() {</b>
610 <b> callback.call(this, todos);</b>
611 <b>}.bind(this));</b>
612 }.bind(this));
613 };
614 </pre>
615
616 <p>2. The same Read-After-Write data hazard issue previously present in the
617 <code>save()</code> method is also present when removing items so we'll need
618 to update a few more places to allow for batch operations on a list of todo IDs. </p>
619
620 <p>a) Still in <em>store.js</em>, update <code>remove()</code>:</p>
621
622 <pre data-filename="store.js">
623 Store.prototype.remove = function (id, callback) {
624 chrome.storage.local.get(this._dbName, function(storage) {
625 var data = storage[this._dbName];
626 var todos = data.todos;
627
628 <b>var ids = [].concat(id);</b>
629 <b>ids.forEach( function(id) {</b>
630 <b> </b>for (var i = 0; i &lt; todos.length; i++) {
631 <b> </b> if (todos[i].id == id) {
632 <b> </b> todos.splice(i, 1);
633 <b> </b> break;
634 <b> </b> }
635 <b> </b>}
636 <b>});</b>
637
638 chrome.storage.local.set(storage, function() {
639 callback.call(this, todos);
640 }.bind(this));
641 }.bind(this));
642 };
643 </pre>
644
645 <p>b) In <strong><em>controller.js</em></strong>, change <code>removeCompletedIt ems()</code> to
646 make it call <code>removeItem()</code> on all IDs at once:</p>
647
648 <pre data-filename="controller.js">
649 Controller.prototype.removeCompletedItems = function () {
650 this.model.read({ completed: 1 }, function (data) {
651 <b>var ids = [];</b>
652 data.forEach(function (item) {
653 <strike>this.removeItem(item.id);</strike>
654 <b>ids.push(item.id);</b>
655 }.bind(this));
656 <b>this.removeItem(ids);</b>
657 }.bind(this));
658
659 this._filter();
660 };
661 </pre>
662
663 <p>c) Finally, still in <em>controller.js</em>, change the <code>removeItem()</c ode> to support
664 removing multiple items from the DOM at once, and move the <code>_filter()</c ode> call to be inside the callback:</p>
665
666 <pre data-filename="controller.js">
667 Controller.prototype.removeItem = function (id) {
668 this.model.remove(id, function () {
669 <b>var ids = [].concat(id);</b>
670 <b>ids.forEach( function(id) {</b>
671 <b> </b>this.$todoList.removeChild($$('[data-id="' + id + '"]'));
672 <b>}.bind(this));</b>
673 <b>this._filter();</b>
674 }.bind(this));
675 <strike>this._filter();</strike>
676 };
677 </pre>
678
679 <h3 id="drop-items">Drop all todo items</h3>
680
681 <p>There is one more method in <em>store.js</em> using <code>localStorage</code> :</p>
682
683 <pre data-filename="store.js">
684 Store.prototype.drop = function (callback) {
685 localStorage[this._dbName] = JSON.stringify({todos: []});
686 callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
687 };
688 </pre>
689
690 <p>This method is not being called in the current app so, if you want an extra c hallenge, try implementing it on your own.
691 Hint: Have a look at <code><a href="/apps/storage#method-StorageArea-remove">chr ome.storage.local.clear()</a></code>.</p>
692
693 <h2 id="launch">Launch your finished Todo app</h2>
694
695 <p>You are done Step 2! Reload your app and you should now have
696 a fully working Chrome packaged version of TodoMVC.</p>
697
698 <figure>
699 <img src="{{static}}/images/app_codelab/step2-completed.gif" alt="The finished Todo app after Step 2">
700 </figure>
701
702 <p class="note">
703 <strong>Troubleshooting</strong>
704 <br>
705 Remember to always check the DevTools Console to see if there are any error me ssages.
706 </p>
707
708 <h2 id="recap">Recap APIs referenced in this step</h2>
709
710 <p>For more detailed information about some of the APIs introduced in this step, refer to:</p>
711
712 <ul>
713 <li>
714 <a href="/apps/contentSecurityPolicy" title="Read 'Content Security Policy' in the Chrome developer docs">Content Security Policy</a>
715 <a href="#csp-compliance" class="anchor-link-icon" title="This feature menti oned in 'Make scripts Content Security Policy (CSP) compliant'">&#8593;</a>
716 </li>
717 <li>
718 <a href="/apps/declare_permissions" title="Read 'Declare Permissions' in the Chrome developer docs">Declare Permissions</a>
719 <a href="#update-permissions" class="anchor-link-icon" title="This feature m entioned in 'Update app permissions'">&#8593;</a>
720 </li>
721 <li>
722 <a href="/apps/storage" title="Read 'chrome.storage' in the Chrome developer docs">chrome.storage</a>
723 <a href="#get-and-set" class="anchor-link-icon" title="This feature mentione d in 'Learn about local.storage.set() and local.storage.get()'">&#8593;</a>
724 </li>
725 <li>
726 <a href="/apps/storage#method-StorageArea-get" title="Read 'chrome.storage.l ocal.get()' in the Chrome developer docs">chrome.storage.local.get()</a>
727 <a href="#get-and-set" class="anchor-link-icon" title="This feature mentione d in 'Learn about local.storage.set() and local.storage.get()'">&#8593;</a>
728 <a href="#retrieve-items" class="anchor-link-icon" title="This feature menti oned in 'Retrieve todos items'">&#8593;</a>
729 </li>
730 <li>
731 <a href="/apps/storage#method-StorageArea-set" title="Read 'chrome.storage.l ocal.set()' in the Chrome developer docs">chrome.storage.local.set()</a>
732 <a href="/apps/storage#method-StorageArea-get" title="Read 'chrome.storage.l ocal.get()' in the Chrome developer docs">chrome.storage.local.get()</a>
733 <a href="#save-items" class="anchor-link-icon" title="This feature mentioned in 'Save todos items'">&#8593;</a>
734 </li>
735 <li>
736 <a href="/apps/storage#method-StorageArea-remove" title="Read 'chrome.storag e.local.remove()' in the Chrome developer docs">chrome.storage.local.remove()</a >
737 <a href="#remove-items" class="anchor-link-icon" title="This feature mention ed in 'Remove todos items'">&#8593;</a>
738 </li>
739 <li>
740 <a href="/apps/storage#method-StorageArea-remove" title="Read 'chrome.storag e.local.clear()' in the Chrome developer docs">chrome.storage.local.clear()</a>
741 <a href="#remove-items" class="anchor-link-icon" title="This feature mention ed in 'Drop all todo items'">&#8593;</a>
742 </li>
743 </ul>
744
745 <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>
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698