Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 <meta name="doc-family" content="apps"> | |
| 2 <h1>Build Apps with AngularJS</h1> | |
| 3 <!--Article written by Eric Bidelman--> | |
| 4 <p> | |
| 5 This guide gets you started building packaged apps | |
| 6 with the <a href="http://angularjs.org/">AngularJS</a> MVC framework. | |
| 7 To illustrate Angular in action, | |
| 8 we'll be referencing an actual app built using the framework, | |
| 9 the Google Drive Uploader. | |
| 10 The <a href="https://github.com/GoogleChrome/chrome-app-samples/tree/master/gdoc s">source code</a> | |
| 11 is available on GitHub. | |
| 12 </p> | |
| 13 | |
| 14 <h2 id="first">About the app</h2> | |
| 15 | |
| 16 <img src="{{static}}/images/uploader.png" | |
| 17 width="296" | |
| 18 height="347" | |
| 19 style="float: right; padding-left: 5px" | |
| 20 alt="Google Drive Uploader"> | |
| 21 | |
| 22 <p> | |
| 23 The Google Drive Uploader allows users to quickly view and interact | |
| 24 with files stored in their Google Drive account | |
| 25 as well as upload new files using the | |
| 26 <a href="http://www.html5rocks.com/en/tutorials/dnd/basics/">HTML Drag and Drop APIs</a>. | |
| 27 It's a great example of building an app which talks | |
| 28 to one of <a href="https://developers.google.com/apis-explorer/#p/">Google's API s</a>; | |
| 29 in this case, the Google Drive API. | |
| 30 </p> | |
| 31 | |
| 32 <p class="note"> | |
| 33 <strong>Note: </strong> | |
| 34 You can also build apps which talk to 3rd party APIs/services | |
| 35 that are OAuth2-enabled. | |
| 36 See <a href="http://developer.chrome.com/trunk/apps/app_identity.html#non">non-G oogle Account authentication</a>. | |
| 37 </p> | |
| 38 | |
| 39 <p> | |
| 40 The Uploader uses OAuth2 to access the user's data. The | |
| 41 <a href="http://developer.chrome.com/trunk/apps/experimental.identity.html">chro me.experimental.identity API</a> | |
| 42 handles fetching an OAuth token for the logged-in user, | |
| 43 so the hard work is done for us! | |
| 44 Once we have a long-lived access token, | |
| 45 the apps uses the | |
| 46 <a href="https://developers.google.com/drive/get-started">Google Drive API</a> | |
| 47 to access the user's data. | |
| 48 </p> | |
| 49 | |
| 50 <p> | |
| 51 Key features this app uses: | |
| 52 </p> | |
| 53 | |
| 54 <ul> | |
| 55 <li>Angular JS's autodetection for | |
| 56 <a href="http://developer.chrome.com/trunk/apps/app_csp.html">CSP</a></l i> | |
| 57 <li>Render a list of files fetched from the | |
| 58 <a href="https://developers.google.com/drive/get-started">Google Drive A PI</a></li> | |
| 59 <li><a href="http://www.html5rocks.com/en/tutorials/file/filesystem/">HTML5 Filesystem API</a> | |
| 60 to store file icons offline</li> | |
| 61 <li><a href="http://www.html5rocks.com/en/tutorials/dnd/basics/">HTML5 Drag and Drop</a> | |
| 62 for importing/uploading new files from the desktop</li> | |
| 63 <li>XHR2 to load images, cross-domain</li> | |
| 64 <li><a href="http://developer.chrome.com/trunk/apps/app_identity.html">chrom e.experimental.identity API</a> | |
| 65 for OAuth authorization</li> | |
| 66 <li>Chromeless frames to define the app's own navbar look and feel</li> | |
| 67 </ul> | |
| 68 | |
| 69 <h2 id="second">Creating the manifest</h2> | |
| 70 | |
| 71 <p> | |
| 72 All packaged apps require a <code>manifest.json</code> file | |
| 73 which contains the information Chrome needs to launch the app. | |
| 74 The manifest contains relevant metadata and | |
| 75 lists any special permissions the app needs to run. | |
| 76 </p> | |
| 77 | |
| 78 <p> | |
| 79 A stripped down version of the Uploader's manifest like this: | |
|
ericbidelman
2012/10/30 18:56:52
looks like
mkearney
2012/11/06 00:46:11
Done.
| |
| 80 </p> | |
| 81 | |
| 82 <pre> | |
| 83 { | |
| 84 "name": "Google Drive Uploader", | |
| 85 "version": "0.0.1", | |
| 86 "manifest_version": 2, | |
| 87 "oauth2": { | |
| 88 "client_id": "665859454684.apps.googleusercontent.com", | |
| 89 "scopes": [ | |
| 90 "https://docs.google.com/feeds/", | |
| 91 "https://docs.googleusercontent.com/", | |
| 92 "https://spreadsheets.google.com/feeds/", | |
| 93 "https://www.googleapis.com/auth/drive" | |
| 94 ] | |
| 95 }, | |
| 96 ... | |
| 97 "permissions": [ | |
| 98 "experimental", | |
| 99 "https://docs.google.com/feeds/", | |
| 100 "https://docs.googleusercontent.com/", | |
| 101 "https://spreadsheets.google.com/feeds/", | |
| 102 "https://ssl.gstatic.com/", | |
| 103 "https://www.googleapis.com/" | |
| 104 ] | |
| 105 } | |
| 106 </pre> | |
| 107 | |
| 108 <p> | |
| 109 The most important parts of this manifest are the "oauth2" and "permissions" sec tions. | |
| 110 </p> | |
| 111 | |
| 112 <p> | |
| 113 The "oauth2" section defines the required parameters by OAuth2 to do its magic. | |
| 114 To create a "client_id", follow the instructions in | |
| 115 <a href="http://developer.chrome.com/apps/app_identity.html#client_id">Get your client id</a>. | |
| 116 The "scopes" list the authorization scopes | |
| 117 that the OAuth token will be valid for (for example, the APIs the app wants to a ccess). | |
| 118 </p> | |
| 119 | |
| 120 <p class="note"> | |
| 121 <strong>Note: </strong> | |
| 122 The Uploader actually uses the | |
| 123 <a href="https://developers.google.com/google-apps/documents-list/">Documents Li st API v3</a> | |
| 124 to access Google Drive content, | |
| 125 hence needing the first several scopes to access all the different types of file s. | |
| 126 However, when using the Google Drive API, you only need | |
| 127 <a href="https://www.googleapis.com/auth/drive">"https://www.googleapis.com/auth /drive"</a>. | |
| 128 </p> | |
| 129 | |
| 130 <p> | |
| 131 The "permissions" section includes the "experimental" bit | |
| 132 (necessary because the current chrome.identity API is still experimental) | |
| 133 and additional URLs that the app will access via XHR2. | |
| 134 The URL prefixes are required in order for Chrome | |
| 135 to know which cross-domain requests to allow. | |
| 136 </p> | |
| 137 | |
| 138 <h2 id="three">Creating the event page</h2> | |
| 139 | |
| 140 <p> | |
| 141 All packaged apps require a background script/page | |
| 142 to launch the app and respond to system events. | |
| 143 </p> | |
| 144 | |
| 145 <p> | |
| 146 In its | |
| 147 <a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/js /background.js">background.js</a> | |
| 148 script, | |
| 149 Drive Uploader opens a 500x600px window to the main page. | |
| 150 It also specifies a minimum height and width for the window | |
| 151 so the content doesn't become too crunched: | |
| 152 </p> | |
| 153 | |
| 154 <pre> | |
| 155 chrome.app.runtime.onLaunched.addListener(function(launchData) { | |
| 156 chrome.app.window.create('../main.html', { | |
| 157 width: 500, | |
| 158 height: 600, | |
| 159 minWidth: 500, | |
| 160 minHeight: 600, | |
| 161 frame: 'none' | |
| 162 }); | |
| 163 }); | |
| 164 </pre> | |
| 165 | |
| 166 <p> | |
| 167 The window is created as a chromeless window (frame: 'none'). | |
| 168 By default, windows render with the OS's default close/expand/minimize bar: | |
| 169 </p> | |
| 170 | |
| 171 <img src="{{static}}/images/noframe.png" | |
| 172 width="508" | |
| 173 height="75" | |
| 174 alt="Google Drive Uploader with no frame"> | |
| 175 | |
| 176 <p> | |
| 177 The Uploader uses frame: 'none' to render the window as a "blank slate" | |
|
ericbidelman
2012/10/30 18:56:52
<code>frame: 'none'</code>
mkearney
2012/11/06 00:46:11
Done.
| |
| 178 and creates a custom close button in <code>main.html</code>: | |
| 179 </p> | |
| 180 | |
| 181 <img src="{{static}}/images/customframe.png" | |
| 182 width="504" | |
| 183 height="50" | |
| 184 alt="Google Drive Uploader with custom frame"> | |
| 185 | |
| 186 <p> | |
| 187 The entire navigational area is wrapped in a <nav> (see next section). | |
| 188 To declutter the app a bit, | |
| 189 the custom close button is hidden until the user interacts with this the area: | |
| 190 </p> | |
| 191 | |
| 192 <pre> | |
| 193 <style> | |
| 194 nav:hover #close-button { | |
| 195 opacity: 1; | |
| 196 } | |
| 197 | |
| 198 #close-button { | |
| 199 float: right; | |
| 200 padding: 0 5px 2px 5px; | |
| 201 font-weight: bold; | |
| 202 opacity: 0; | |
| 203 -webkit-transition: all 0.3s ease-in-out; | |
| 204 } | |
| 205 </style> | |
| 206 | |
| 207 <button class="btn" id="close-button" title="Close">x</button> | |
| 208 </pre> | |
| 209 | |
| 210 <p> | |
| 211 In | |
| 212 <a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/js /app.js">app.js</a>, | |
| 213 this button is hooked up to <code>window.close()</code>. | |
| 214 </p> | |
| 215 | |
| 216 <h2 id="four">Designing the app the Angular way</h2> | |
| 217 | |
| 218 <p> | |
| 219 Angular is an MVC framework, so we need to define the app in such a way that a | |
| 220 model, view, and controller logically fall out of it. Luckily, this is trivial w hen using Angular. | |
| 221 </p> | |
| 222 | |
| 223 <p> | |
| 224 The View is the easiest, so let's start there. | |
| 225 </p> | |
| 226 | |
| 227 <h3 id="view">Creating the view</h3> | |
| 228 | |
| 229 <p> | |
| 230 <a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/ma in.html">main.html</a> | |
| 231 is the "V" in MVC; where we define HTML templates to render data into. | |
| 232 In Angular, templates are simple blocks of HTML with some special sauce. | |
| 233 </p> | |
| 234 | |
| 235 <p> | |
| 236 Ultimately we want to display the user's list of files. | |
| 237 For that, a simple <ul> list makes sense. | |
| 238 The Angular bits are highlighted in bold: | |
| 239 </p> | |
| 240 | |
| 241 <pre> | |
| 242 <ul> | |
| 243 <li <strong>data-ng-repeat="doc in docs"</strong>> | |
| 244 <img data-ng-src=<strong>"{{doc.icon}}"</strong>> <a href=<s trong>"{{doc.alternateLink}}"</strong>><strong>{{doc.title}} </strong></a> | |
| 245 <strong>{{doc.size}}</strong> | |
| 246 <span class="date"><strong>{{doc.updatedDate}}</strong></spa n> | |
| 247 </li> | |
| 248 </ul> | |
| 249 </pre> | |
| 250 | |
| 251 <p> | |
| 252 This reads exactly as it looks: | |
| 253 stamp out an <li> for every doc in our data model "docs". | |
|
ericbidelman
2012/10/30 18:56:52
Should this be a list?
mkearney
2012/11/06 00:46:11
Yes. I had to use < in place of literal <.
On
| |
| 254 Each item contains a file icon, link to open the file on the web, | |
| 255 and last updatedDate. | |
| 256 </p> | |
| 257 | |
| 258 <p class="note"> | |
| 259 <strong>Note: </strong> | |
| 260 To make the template valid HTML, | |
| 261 we're using data-* attributes for Angular's | |
|
ericbidelman
2012/10/30 18:56:52
<code>data-*</code>
mkearney
2012/11/06 00:46:11
Done.
| |
| 262 <a href="http://docs.angularjs.org/api/ng.directive:ngRepeat">nRepeat</a> iterat or, | |
| 263 but you don't have to. | |
| 264 You could easily write the repeater as <code><li ng-repeat="doc in docs"></co de>. | |
| 265 </p> | |
| 266 | |
| 267 <p> | |
| 268 Next, we need to tell Angular which controller will oversee this template's rend ering. | |
| 269 For that, we use the | |
| 270 <a href="http://docs.angularjs.org/api/ng.directive:ngController">ngController</ a> | |
| 271 directive to tell the DocsController to have reign over the template <body>: | |
|
ericbidelman
2012/10/30 18:56:52
<code>DocsController</code>
mkearney
2012/11/06 00:46:11
Done.
| |
| 272 </p> | |
| 273 | |
| 274 <pre> | |
| 275 <body <strong>data-ng-controller="DocsController"</strong>> | |
| 276 <section id="main"> | |
| 277 <ul> | |
| 278 <li data-ng-repeat="doc in docs"> | |
| 279 <img src="{{doc.icon}}"> <a href="{{doc.alternateLink }}">{{doc.title}}</a> {{doc.size}} | |
|
ericbidelman
2012/10/30 18:56:52
src= -> data-ng-src=
mkearney
2012/11/06 00:46:11
Done.
| |
| 280 <span class="date">{{doc.updatedDate}}</span> | |
| 281 </li> | |
| 282 </ul> | |
| 283 </section> | |
| 284 </body> | |
| 285 </pre> | |
| 286 | |
| 287 <p> | |
| 288 Keep in mind, | |
| 289 what you don't see here is us hooking up event listeners or properties for data binding. | |
| 290 Angular is going to do that heavy lifting for us! | |
|
ericbidelman
2012/10/30 18:56:52
is doing that heavy lifting...
mkearney
2012/11/06 00:46:11
Done.
| |
| 291 </p> | |
| 292 | |
| 293 <p> | |
| 294 The last step is to make Angular light up our templates. | |
| 295 The typical way to do that is include the | |
| 296 <a href="http://docs.angularjs.org/api/ng.directive:ngApp">ngApp</a> | |
| 297 directive all the way up on <html>: | |
| 298 </p> | |
| 299 | |
| 300 <pre> | |
| 301 <html <strong>data-ng-app="gDriveApp"</strong>> | |
| 302 </pre> | |
| 303 | |
| 304 <p> | |
| 305 You could also scope the app down | |
| 306 to a smaller portion of the page if you wanted to. | |
| 307 We only have one controller in this app, | |
| 308 but if we were to add more later, | |
| 309 putting <a href="http://docs.angularjs.org/api/ng.directive:ngApp">ngApp</a> | |
| 310 on the topmost element makes the entire page Angular-ready. | |
| 311 </p> | |
| 312 | |
| 313 <p> | |
| 314 The final product for <code>main.html</code> looks something like this: | |
| 315 </p> | |
| 316 | |
| 317 <pre> | |
| 318 <html <strong>data-ng-app="gDriveApp"</strong>> | |
| 319 <head> | |
| 320 … | |
| 321 <!-- crbug.com/120693: so we don't need target="_blank" on every anchor. --> | |
| 322 <base target="_blank"> | |
| 323 </head> | |
| 324 <body <strong>data-ng-controller="DocsController"</strong>> | |
| 325 <section id="main"> | |
| 326 <nav> | |
| 327 <h2>Google Drive Uploader</h2> | |
| 328 <button class="btn" <strong>data-ng-click="fetchDocs()"</strong>>Refresh& lt;/button> | |
| 329 <button class="btn" id="close-button" title="Close"></button> | |
| 330 </nav> | |
| 331 <ul> | |
| 332 <li <strong>data-ng-repeat="doc in docs"</strong>> | |
| 333 <img src=<strong>"{{doc.icon}}"</strong>> <a href=<strong> "{{doc.alternateLink}}"</strong>><strong>{{doc.title}}</stro ng></a> <strong>{{doc.size}}</strong> | |
|
ericbidelman
2012/10/30 18:56:52
src -> data-ng-src
mkearney
2012/11/06 00:46:11
Done.
| |
| 334 <span class="date"><strong>{{doc.updatedDate}}</strong></s pan> | |
| 335 </li> | |
| 336 </ul> | |
| 337 </section> | |
| 338 </pre> | |
| 339 | |
| 340 <h3 id="csp">A word on Content Security Policy</h3> | |
| 341 | |
| 342 <p> | |
| 343 Unlike many other JS MVC frameworks, | |
| 344 Angular v1.1.0+ requires no tweaks to work within a strict | |
| 345 <a href="http://developer.chrome.com/trunk/apps/app_csp.html">CSP</a>. | |
| 346 It just works, out of the box! | |
| 347 </p> | |
| 348 | |
| 349 <p> | |
| 350 However, if you're using an older version | |
| 351 of Angular between v1.0.1 and v1.1.0, | |
| 352 you'll need tell Angular to run in a "content security mode". | |
| 353 This is done by including the | |
| 354 <a href="http://docs.angularjs.org/api/ng.directive:ngCsp">ngCsp</a> | |
| 355 directive alongside <a href="http://docs.angularjs.org/api/ng.directive:ngApp">n gApp</a>: | |
| 356 </p> | |
| 357 | |
| 358 <pre> | |
| 359 <html data-ng-app data-ng-csp> | |
| 360 </pre> | |
| 361 | |
| 362 <h3 id="authorization">Handling authorization</h3> | |
| 363 | |
| 364 <p> | |
| 365 The data model isn't generated by the app itself. | |
| 366 Instead, it's populated from an external API (the Google Drive API). | |
| 367 Thus, there's a bit of work necessary in order to populate the app's data. | |
| 368 </p> | |
| 369 | |
| 370 <p> | |
| 371 Before we can make an API request, | |
| 372 we need to fetch an OAuth token for the user's Google Account. | |
| 373 For that, we've created a method to wrap the call | |
| 374 to <code>chrome.experimental.identity.getAuthToken()</code> and | |
| 375 store the <code>accessToken</code>, | |
| 376 which we can reuse for future calls to the Drive API. | |
| 377 </p> | |
| 378 | |
| 379 <pre> | |
| 380 GDocs.prototype.auth = function(opt_callback) { | |
| 381 try { | |
| 382 <strong>chrome.experimental.identity.getAuthToken({interactive: false}, func tion(token) {</strong> | |
| 383 if (token) { | |
| 384 this.accessToken = token; | |
| 385 opt_callback && opt_callback(); | |
| 386 } | |
| 387 }.bind(this)); | |
| 388 } catch(e) { | |
| 389 console.log(e); | |
| 390 } | |
| 391 }; | |
| 392 </pre> | |
| 393 | |
| 394 <p class="note"> | |
| 395 <strong>Note: </strong> | |
| 396 Passing the optional callback gives us the flexibility | |
| 397 of knowing when the OAuth token is ready. | |
| 398 </p> | |
| 399 | |
| 400 <p class="note"> | |
| 401 <strong>Note: </strong> | |
| 402 To simplify things a bit, | |
| 403 we've created a library, | |
| 404 <a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/js /gdocs.js">gdocs.js</a> | |
| 405 to handle API tasks. | |
| 406 </p> | |
| 407 | |
| 408 <p> | |
| 409 Once we have the token, | |
| 410 it's time to make requests against the Drive API and populate the model. | |
| 411 </p> | |
| 412 | |
| 413 <h3 id="skeleton">Skeleton controller</h3> | |
| 414 | |
| 415 <p> | |
| 416 The "model" for the Uploader is a simple array (called docs) | |
| 417 of objects that will get rendered as those <li>s in the template: | |
| 418 </p> | |
| 419 | |
| 420 <pre> | |
| 421 var gDriveApp = angular.module('gDriveApp', []); | |
| 422 | |
| 423 gDriveApp.factory('gdocs', function() { | |
| 424 var gdocs = new GDocs(); | |
| 425 return gdocs; | |
| 426 }); | |
| 427 | |
| 428 function DocsController($scope, $http, gdocs) { | |
| 429 $scope.docs = []; | |
| 430 | |
| 431 $scope.fetchDocs = function() { | |
| 432 ... | |
| 433 }; | |
| 434 | |
| 435 // Invoke on ctor call. Fetch docs after we have the oauth token. | |
| 436 gdocs.auth(function() { | |
| 437 $scope.fetchDocs(); | |
| 438 }); | |
| 439 | |
| 440 } | |
| 441 </pre> | |
| 442 | |
| 443 <p> | |
| 444 Notice that <code>gdocs.auth()</code> is called | |
| 445 as part of the DocsController constructor. | |
| 446 When Angular's internals create the controller, | |
| 447 we're insured to have a fresh OAuth token waiting for the user. | |
| 448 </p> | |
| 449 | |
| 450 <h2 id="five">Fetching data</h2> | |
| 451 | |
| 452 <p> | |
| 453 Template laid out. | |
| 454 Controller scaffolded. | |
| 455 OAuth token in hand. | |
| 456 Now what? | |
| 457 </p> | |
| 458 | |
| 459 <p> | |
| 460 It's time to define the main controller method, | |
| 461 <code>fetchDocs()</code>. | |
| 462 It's the workhorse of the controller, | |
| 463 responsible for requesting the user's files and | |
| 464 filing the docs array with data from API responses. | |
| 465 </p> | |
| 466 | |
| 467 <pre> | |
| 468 $scope.fetchDocs = function() { | |
| 469 $scope.docs = []; // First, clear out any old results | |
| 470 | |
| 471 // Response handler that doesn't cache file icons. | |
| 472 var successCallback = function(resp, status, headers, config) { | |
| 473 var docs = []; | |
| 474 var totalEntries = resp.feed.entry.length; | |
| 475 | |
| 476 resp.feed.entry.forEach(function(entry, i) { | |
| 477 var doc = { | |
| 478 title: entry.title.$t, | |
| 479 updatedDate: Util.formatDate(entry.updated.$t), | |
| 480 updatedDateFull: entry.updated.$t, | |
| 481 icon: gdocs.getLink(entry.link, | |
| 482 'http://schemas.google.com/docs/2007#icon').href, | |
| 483 alternateLink: gdocs.getLink(entry.link, 'alternate').href, | |
| 484 size: entry.docs$size ? '( ' + entry.docs$size.$t + ' bytes)' : null | |
| 485 }; | |
| 486 | |
| 487 $scope.docs.push(doc); | |
| 488 | |
| 489 // Only sort when last entry is seen. | |
| 490 if (totalEntries - 1 == i) { | |
| 491 $scope.docs.sort(Util.sortByDate); | |
| 492 } | |
| 493 }); | |
| 494 }; | |
| 495 | |
| 496 var config = { | |
| 497 params: {'alt': 'json'}, | |
| 498 headers: { | |
| 499 'Authorization': 'Bearer ' + gdocs.accessToken, | |
| 500 'GData-Version': '3.0' | |
| 501 } | |
| 502 }; | |
| 503 | |
| 504 $http.get(gdocs.DOCLIST_FEED, config).success(successCallback); | |
| 505 }; | |
| 506 </pre> | |
| 507 | |
| 508 <p> | |
| 509 <code>fetchDocs()</code> uses Angular's $http service | |
|
ericbidelman
2012/10/30 18:56:52
<code>$http</code>
mkearney
2012/11/06 00:46:11
Done.
| |
| 510 to retrieve the main feed over XHR. | |
| 511 The oauth access token is included in the Authorization header | |
|
ericbidelman
2012/10/30 18:56:52
<code>Authorization</code>
mkearney
2012/11/06 00:46:11
Done.
| |
| 512 along with other custom headers and parameters. | |
| 513 </p> | |
| 514 | |
| 515 <p> | |
| 516 The successCallback processes the API response and | |
|
ericbidelman
2012/10/30 18:56:52
<code>successCallback</code>
mkearney
2012/11/06 00:46:11
Done.
| |
| 517 creates a new doc object for each entry in the feed. | |
| 518 </p> | |
| 519 | |
| 520 <p> | |
| 521 If you run <code>fetchDocs()</code> right now, | |
| 522 everything works and the list of files shows up: | |
| 523 </p> | |
| 524 | |
| 525 <img src="{{static}}/images/listoffiles.png" | |
| 526 width="580" | |
| 527 height="680" | |
| 528 alt="Fetched list of files in Google Drive Uploader"> | |
| 529 | |
| 530 <p> | |
| 531 Woot! | |
| 532 </p> | |
| 533 | |
| 534 <p> | |
| 535 Wait,...we're missing those neat file icons. | |
| 536 What gives? | |
| 537 A quick check of the console shows a bunch of CSP-related errors: | |
| 538 </p> | |
| 539 | |
| 540 <img src="{{static}}/images/csperrors.png" | |
| 541 width="947" | |
| 542 height="84" | |
| 543 alt="CSP errors in developer console"> | |
| 544 | |
| 545 <p> | |
| 546 The reason is that we're trying | |
| 547 to set the icons <code>img.src</code> to external URLs. | |
| 548 This violates CSP. | |
| 549 For example: | |
| 550 <code>https://ssl.gstatic.com/docs/doclist/images/icon_10_document_list.png</cod e>. | |
| 551 To fix this, | |
| 552 we need to pull in these remote assets locally to the app. | |
| 553 </p> | |
| 554 | |
| 555 <h3 id="import">Importing remote image assets</h3> | |
| 556 | |
| 557 <p> | |
| 558 For CSP to stop yelling at us, | |
| 559 we use XHR2 to "import" the file icons as Blobs, | |
| 560 then set the <code>img.src</code> | |
| 561 to a <code>blob: URL</code> created by the app. | |
| 562 </p> | |
| 563 | |
| 564 <p> | |
| 565 Here's the updated <code>successCallback</code> | |
| 566 with the added XHR code: | |
| 567 </p> | |
| 568 | |
| 569 <pre> | |
| 570 var successCallback = function(resp, status, headers, config) { | |
| 571 var docs = []; | |
| 572 var totalEntries = resp.feed.entry.length; | |
| 573 | |
| 574 resp.feed.entry.forEach(function(entry, i) { | |
| 575 var doc = { | |
| 576 ... | |
| 577 }; | |
| 578 | |
| 579 <strong>$http.get(doc.icon, {responseType: 'blob'}).success(function(blob) { | |
| 580 console.log('Fetched icon via XHR'); | |
| 581 | |
| 582 blob.name = doc.iconFilename; // Add icon filename to blob. | |
| 583 | |
| 584 writeFile(blob); // Write is async, but that's ok. | |
| 585 | |
| 586 doc.icon = window.URL.createObjectURL(blob); | |
| 587 | |
| 588 $scope.docs.push(doc); | |
| 589 | |
| 590 // Only sort when last entry is seen. | |
| 591 if (totalEntries - 1 == i) { | |
| 592 $scope.docs.sort(Util.sortByDate); | |
| 593 } | |
| 594 });</strong> | |
| 595 }); | |
| 596 }; | |
| 597 </pre> | |
| 598 | |
| 599 <p> | |
| 600 Now that CSP is happy with us again, | |
| 601 we get nice file icons: | |
| 602 </p> | |
| 603 | |
| 604 <img src="{{static}}/images/fileicons.png" | |
| 605 width="580" | |
| 606 height="680" | |
| 607 alt="Google Drive Uploader with file icons"> | |
| 608 | |
| 609 <h2 id="six">Going offline: caching external resources</h2> | |
| 610 | |
| 611 <p> | |
| 612 The obvious optimization that needs to be made: | |
| 613 not make 100s of XHR requests for each file icon | |
| 614 on every call to <code>fetchDocs()</code>. | |
| 615 Verify this in the Developer Tools console | |
| 616 by pressing the "Refresh" button several times. | |
| 617 Every time, n images are fetched: | |
| 618 </p> | |
| 619 | |
| 620 <img src="{{static}}/images/fetchedicon.png" | |
| 621 width="118" | |
| 622 height="19" | |
| 623 alt="Console log 65: Fetched icon via XHR"> | |
| 624 | |
| 625 <p> | |
| 626 Let's modify <code>successCallback</code> | |
| 627 to add a caching layer. | |
| 628 The additions are highlighted in bold: | |
| 629 </p> | |
| 630 | |
| 631 <pre> | |
| 632 $scope.fetchDocs = function() { | |
| 633 ... | |
| 634 | |
| 635 // Response handler that caches file icons in the filesystem API. | |
| 636 var successCallbackWithFsCaching = function(resp, status, headers, config) { | |
| 637 var docs = []; | |
| 638 var totalEntries = resp.feed.entry.length; | |
| 639 | |
| 640 resp.feed.entry.forEach(function(entry, i) { | |
| 641 var doc = { | |
| 642 ... | |
| 643 }; | |
| 644 | |
| 645 <strong>// 'https://ssl.gstatic.com/doc_icon_128.png' -> 'doc_icon_128.png ' | |
| 646 doc.iconFilename = doc.icon.substring(doc.icon.lastIndexOf('/') + 1);</str ong> | |
| 647 | |
| 648 // If file exists, it we'll get back a FileEntry for the filesystem URL. | |
| 649 // Otherwise, the error callback will fire and we need to XHR it in and | |
| 650 // write it to the FS. | |
| 651 <strong>var fsURL = fs.root.toURL() + FOLDERNAME + '/' + doc.iconFilename; | |
| 652 window.webkitResolveLocalFileSystemURL(fsURL, function(entry) { | |
| 653 doc.icon = entry.toURL(); // should be === to fsURL, but whatevs.</stron g> | |
| 654 | |
| 655 $scope.docs.push(doc); // add doc to model. | |
| 656 | |
| 657 // Only want to sort and call $apply() when we have all entries. | |
| 658 if (totalEntries - 1 == i) { | |
| 659 $scope.docs.sort(Util.sortByDate); | |
| 660 $scope.$apply(function($scope) {}); // Inform angular that we made cha nges. | |
| 661 } | |
| 662 | |
| 663 <strong>}, function(e) { | |
| 664 // Error: file doesn't exist yet. XHR it in and write it to the FS. | |
| 665 | |
| 666 $http.get(doc.icon, {responseType: 'blob'}).success(function(blob) { | |
| 667 console.log('Fetched icon via XHR'); | |
| 668 | |
| 669 blob.name = doc.iconFilename; // Add icon filename to blob. | |
| 670 | |
| 671 writeFile(blob); // Write is async, but that's ok. | |
| 672 | |
| 673 doc.icon = window.URL.createObjectURL(blob); | |
| 674 | |
| 675 $scope.docs.push(doc); | |
| 676 | |
| 677 // Only sort when last entry is seen. | |
| 678 if (totalEntries - 1 == i) { | |
| 679 $scope.docs.sort(Util.sortByDate); | |
| 680 } | |
| 681 }); | |
| 682 | |
| 683 });</strong> | |
| 684 }); | |
| 685 }; | |
| 686 | |
| 687 var config = { | |
| 688 ... | |
| 689 }; | |
| 690 | |
| 691 $http.get(gdocs.DOCLIST_FEED, config).success(successCallbackWithFsCaching); | |
| 692 }; | |
| 693 </pre> | |
| 694 | |
| 695 <p> | |
| 696 Notice that in the <code>webkitResolveLocalFileSystemURL()</code> callback | |
| 697 we're calling <code>$scope.$apply()</code> | |
| 698 when the last entry is seen. | |
| 699 Normally calling <code>$apply()</code> isn't necessary. | |
| 700 Angular detects changes to data models automagically. | |
| 701 However in our case, | |
| 702 we have an addition layer of asynchronous callback | |
| 703 that Angular isn't aware of. | |
| 704 We must explicitly tell Angular when our model has been updated. | |
| 705 </p> | |
| 706 | |
| 707 <p> | |
| 708 On first run, | |
| 709 the icons won't be in the HTML5 filesystem and the calls to | |
|
ericbidelman
2012/10/30 18:56:52
Filesystem
mkearney
2012/11/06 00:46:11
Done.
| |
| 710 <code>window.webkitResolveLocalFileSystemURL()</code> will result | |
| 711 in its error callback being invoked. | |
| 712 For that case, | |
| 713 we can reuse the technique from before and fetch the images. | |
| 714 The only difference this time is that | |
| 715 each blob is written to the filesystem (see | |
| 716 <a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/js /app.js#L25">writeBlob()</a>). | |
| 717 The console verifies this behavior: | |
| 718 </p> | |
| 719 | |
| 720 <img src="{{static}}/images/writecompleted.png" | |
| 721 width="804" | |
| 722 height="42" | |
| 723 alt="Console log 100: Write completed"> | |
| 724 | |
| 725 <p> | |
| 726 Upon next run (or press of the "Refresh" button), | |
| 727 the URL passed to <code>webkitResolveLocalFileSystemURL()</code> exists | |
| 728 because the file has been previously cached. | |
| 729 The app sets the <code>doc.icon</code> | |
| 730 to the file's <code>filesystem: URL</code> and | |
| 731 avoids making the costly XHR for the icon. | |
| 732 </p> | |
| 733 | |
| 734 <h2 id="seven">Drag and drop uploading</h2> | |
| 735 | |
| 736 <p> | |
| 737 An uploader app is false advertising | |
| 738 if it can't upload files! | |
| 739 </p> | |
| 740 | |
| 741 <p> | |
| 742 <a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/js /app.js#L177">app.js</a> | |
| 743 handles this feature by implemenuing a small library | |
|
ericbidelman
2012/10/30 18:56:52
implementing
mkearney
2012/11/06 00:46:11
Done.
| |
| 744 around HTML5 Drag and Drop called <code>DnDFileController</code>. | |
| 745 It gives the ability to drag in files from the desktop | |
| 746 and have them uploaded to Google Drive. | |
| 747 </p> | |
| 748 | |
| 749 <p> | |
| 750 Simply adding this to the gdocs service does the job: | |
| 751 </p> | |
| 752 | |
| 753 <pre> | |
| 754 gDriveApp.factory('gdocs', function() { | |
| 755 var gdocs = new GDocs(); | |
| 756 | |
| 757 var dnd = new DnDFileController('body', function(files) { | |
| 758 var $scope = angular.element(this).scope(); | |
| 759 Util.toArray(files).forEach(function(file, i) { | |
| 760 gdocs.upload(file, function() { | |
| 761 $scope.fetchDocs(); | |
| 762 }); | |
| 763 }); | |
| 764 }); | |
| 765 | |
| 766 return gdocs; | |
| 767 }); | |
| 768 </pre> | |
| 769 | |
| 770 <p class="backtotop"><a href="#top">Back to top</a></p> | |
| OLD | NEW |