OLD | NEW |
(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 ↓</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><button onclick=''></code>) and <co
de><script></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 <script src="bower_components/director/build/director.js"></script> |
| 94 <strike><script></strike> |
| 95 <strike> // Bootstrap app data</strike> |
| 96 <strike> window.app = {};</strike> |
| 97 <strike></script></strike> |
| 98 <b><script src="js/bootstrap.js"></script></b> |
| 99 <script src="js/helpers.js"></script> |
| 100 <script src="js/store.js"></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 — 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 < 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 < 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 < 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'">↑</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'">↑</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()'">↑</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()'">↑</a> |
| 728 <a href="#retrieve-items" class="anchor-link-icon" title="This feature menti
oned in 'Retrieve todos items'">↑</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'">↑</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'">↑</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'">↑</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 »</a></p> |
OLD | NEW |