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