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

Side by Side Diff: chrome/common/extensions/docs/templates/articles/angular_framework.html

Issue 11193011: New angular 'getting started' tutorial. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 8 years, 1 month 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 | Annotate | Revision Log
OLDNEW
(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 looks like this:
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 <code>frame: 'none'</code> to render the window as a "blank sl ate"
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 &lt;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 &lt;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 &lt;/style>
206
207 &lt;button class="btn" id="close-button" title="Close">x&lt;/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 &lt;ul> list makes sense.
238 The Angular bits are highlighted in bold:
239 </p>
240
241 <pre>
242 &lt;ul>
243 &lt;li <strong>data-ng-repeat="doc in docs"</strong>>
244 &lt;img data-ng-src=<strong>"&#123;{doc.icon}&#125;"</strong>> &lt;a href=<s trong>"&#123;{doc.alternateLink}&#125;"</strong>><strong>&#123;{doc.title}&#125; </strong>&lt;/a>
245 <strong>&#123;{doc.size}&#125;</strong>
246 &lt;span class="date"><strong>&#123;{doc.updatedDate}&#125;</strong>&lt;/spa n>
247 &lt;/li>
248 &lt;/ul>
249 </pre>
250
251 <p>
252 This reads exactly as it looks:
253 stamp out an &lt;li> for every doc in our data model "docs".
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 <code>data-*</code> attributes for Angular's
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>&lt;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 <code>DocsController</code> to have reign over the templat e <body>:
272 </p>
273
274 <pre>
275 &lt;body <strong>data-ng-controller="DocsController"</strong>>
276 &lt;section id="main">
277 &lt;ul>
278 &lt;li data-ng-repeat="doc in docs">
279 &lt;img data-ng-src="&#123;{doc.icon}&#125;"> &lt;a href="&#123;{doc.alter nateLink}&#125;">&#123;{doc.title}&#125;&lt;/a> &#123;{doc.size}&#125;
280 &lt;span class="date">&#123;{doc.updatedDate}&#125;&lt;/span>
281 &lt;/li>
282 &lt;/ul>
283 &lt;/section>
284 &lt;/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 doing that heavy lifting for us!
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 &lt;html>:
298 </p>
299
300 <pre>
301 &lt;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 &lt;html <strong>data-ng-app="gDriveApp"</strong>>
319 &lt;head>
320 …
321 <!-- crbug.com/120693: so we don't need target="_blank" on every anchor. -->
322 &lt;base target="_blank">
323 &lt;/head>
324 &lt;body <strong>data-ng-controller="DocsController"</strong>>
325 &lt;section id="main">
326 &lt;nav>
327 &lt;h2>Google Drive Uploader&lt;/h2>
328 &lt;button class="btn" <strong>data-ng-click="fetchDocs()"</strong>>Refresh& lt;/button>
329 &lt;button class="btn" id="close-button" title="Close">&lt;/button>
330 &lt;/nav>
331 &lt;ul>
332 &lt;li <strong>data-ng-repeat="doc in docs"</strong>>
333 &lt;img data-ng-src=<strong>"&#123;{doc.icon}&#125;"</strong>> &lt;a href= <strong>"&#123;{doc.alternateLink}&#125;"</strong>><strong>&#123;{doc.title}&#12 5;</strong>&lt;/a> <strong>&#123;{doc.size}&#125;</strong>
334 &lt;span class="date"><strong>&#123;{doc.updatedDate}&#125;</strong>&lt;/s pan>
335 &lt;/li>
336 &lt;/ul>
337 &lt;/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 &lt;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 &lt;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 <code>$http</code> service
510 to retrieve the main feed over XHR.
511 The oauth access token is included
512 in the <code>Authorization</code> header
513 along with other custom headers and parameters.
514 </p>
515
516 <p>
517 The <code>successCallback</code> processes the API response and
518 creates a new doc object for each entry in the feed.
519 </p>
520
521 <p>
522 If you run <code>fetchDocs()</code> right now,
523 everything works and the list of files shows up:
524 </p>
525
526 <img src="{{static}}/images/listoffiles.png"
527 width="580"
528 height="680"
529 alt="Fetched list of files in Google Drive Uploader">
530
531 <p>
532 Woot!
533 </p>
534
535 <p>
536 Wait,...we're missing those neat file icons.
537 What gives?
538 A quick check of the console shows a bunch of CSP-related errors:
539 </p>
540
541 <img src="{{static}}/images/csperrors.png"
542 width="947"
543 height="84"
544 alt="CSP errors in developer console">
545
546 <p>
547 The reason is that we're trying
548 to set the icons <code>img.src</code> to external URLs.
549 This violates CSP.
550 For example:
551 <code>https://ssl.gstatic.com/docs/doclist/images/icon_10_document_list.png</cod e>.
552 To fix this,
553 we need to pull in these remote assets locally to the app.
554 </p>
555
556 <h3 id="import">Importing remote image assets</h3>
557
558 <p>
559 For CSP to stop yelling at us,
560 we use XHR2 to "import" the file icons as Blobs,
561 then set the <code>img.src</code>
562 to a <code>blob: URL</code> created by the app.
563 </p>
564
565 <p>
566 Here's the updated <code>successCallback</code>
567 with the added XHR code:
568 </p>
569
570 <pre>
571 var successCallback = function(resp, status, headers, config) {
572 var docs = [];
573 var totalEntries = resp.feed.entry.length;
574
575 resp.feed.entry.forEach(function(entry, i) {
576 var doc = {
577 ...
578 };
579
580 <strong>$http.get(doc.icon, {responseType: 'blob'}).success(function(blob) {
581 console.log('Fetched icon via XHR');
582
583 blob.name = doc.iconFilename; // Add icon filename to blob.
584
585 writeFile(blob); // Write is async, but that's ok.
586
587 doc.icon = window.URL.createObjectURL(blob);
588
589 $scope.docs.push(doc);
590
591 // Only sort when last entry is seen.
592 if (totalEntries - 1 == i) {
593 $scope.docs.sort(Util.sortByDate);
594 }
595 });</strong>
596 });
597 };
598 </pre>
599
600 <p>
601 Now that CSP is happy with us again,
602 we get nice file icons:
603 </p>
604
605 <img src="{{static}}/images/fileicons.png"
606 width="580"
607 height="680"
608 alt="Google Drive Uploader with file icons">
609
610 <h2 id="six">Going offline: caching external resources</h2>
611
612 <p>
613 The obvious optimization that needs to be made:
614 not make 100s of XHR requests for each file icon
615 on every call to <code>fetchDocs()</code>.
616 Verify this in the Developer Tools console
617 by pressing the "Refresh" button several times.
618 Every time, n images are fetched:
619 </p>
620
621 <img src="{{static}}/images/fetchedicon.png"
622 width="118"
623 height="19"
624 alt="Console log 65: Fetched icon via XHR">
625
626 <p>
627 Let's modify <code>successCallback</code>
628 to add a caching layer.
629 The additions are highlighted in bold:
630 </p>
631
632 <pre>
633 $scope.fetchDocs = function() {
634 ...
635
636 // Response handler that caches file icons in the filesystem API.
637 var successCallbackWithFsCaching = function(resp, status, headers, config) {
638 var docs = [];
639 var totalEntries = resp.feed.entry.length;
640
641 resp.feed.entry.forEach(function(entry, i) {
642 var doc = {
643 ...
644 };
645
646 <strong>// 'https://ssl.gstatic.com/doc_icon_128.png' -> 'doc_icon_128.png '
647 doc.iconFilename = doc.icon.substring(doc.icon.lastIndexOf('/') + 1);</str ong>
648
649 // If file exists, it we'll get back a FileEntry for the filesystem URL.
650 // Otherwise, the error callback will fire and we need to XHR it in and
651 // write it to the FS.
652 <strong>var fsURL = fs.root.toURL() + FOLDERNAME + '/' + doc.iconFilename;
653 window.webkitResolveLocalFileSystemURL(fsURL, function(entry) {
654 doc.icon = entry.toURL(); // should be === to fsURL, but whatevs.</stron g>
655
656 $scope.docs.push(doc); // add doc to model.
657
658 // Only want to sort and call $apply() when we have all entries.
659 if (totalEntries - 1 == i) {
660 $scope.docs.sort(Util.sortByDate);
661 $scope.$apply(function($scope) {}); // Inform angular that we made cha nges.
662 }
663
664 <strong>}, function(e) {
665 // Error: file doesn't exist yet. XHR it in and write it to the FS.
666
667 $http.get(doc.icon, {responseType: 'blob'}).success(function(blob) {
668 console.log('Fetched icon via XHR');
669
670 blob.name = doc.iconFilename; // Add icon filename to blob.
671
672 writeFile(blob); // Write is async, but that's ok.
673
674 doc.icon = window.URL.createObjectURL(blob);
675
676 $scope.docs.push(doc);
677
678 // Only sort when last entry is seen.
679 if (totalEntries - 1 == i) {
680 $scope.docs.sort(Util.sortByDate);
681 }
682 });
683
684 });</strong>
685 });
686 };
687
688 var config = {
689 ...
690 };
691
692 $http.get(gdocs.DOCLIST_FEED, config).success(successCallbackWithFsCaching);
693 };
694 </pre>
695
696 <p>
697 Notice that in the <code>webkitResolveLocalFileSystemURL()</code> callback
698 we're calling <code>$scope.$apply()</code>
699 when the last entry is seen.
700 Normally calling <code>$apply()</code> isn't necessary.
701 Angular detects changes to data models automagically.
702 However in our case,
703 we have an addition layer of asynchronous callback
704 that Angular isn't aware of.
705 We must explicitly tell Angular when our model has been updated.
706 </p>
707
708 <p>
709 On first run,
710 the icons won't be in the HTML5 Filesystem and the calls to
711 <code>window.webkitResolveLocalFileSystemURL()</code> will result
712 in its error callback being invoked.
713 For that case,
714 we can reuse the technique from before and fetch the images.
715 The only difference this time is that
716 each blob is written to the filesystem (see
717 <a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/js /app.js#L25">writeBlob()</a>).
718 The console verifies this behavior:
719 </p>
720
721 <img src="{{static}}/images/writecompleted.png"
722 width="804"
723 height="42"
724 alt="Console log 100: Write completed">
725
726 <p>
727 Upon next run (or press of the "Refresh" button),
728 the URL passed to <code>webkitResolveLocalFileSystemURL()</code> exists
729 because the file has been previously cached.
730 The app sets the <code>doc.icon</code>
731 to the file's <code>filesystem: URL</code> and
732 avoids making the costly XHR for the icon.
733 </p>
734
735 <h2 id="seven">Drag and drop uploading</h2>
736
737 <p>
738 An uploader app is false advertising
739 if it can't upload files!
740 </p>
741
742 <p>
743 <a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/js /app.js#L177">app.js</a>
744 handles this feature by implementing a small library
745 around HTML5 Drag and Drop called <code>DnDFileController</code>.
746 It gives the ability to drag in files from the desktop
747 and have them uploaded to Google Drive.
748 </p>
749
750 <p>
751 Simply adding this to the gdocs service does the job:
752 </p>
753
754 <pre>
755 gDriveApp.factory('gdocs', function() {
756 var gdocs = new GDocs();
757
758 var dnd = new DnDFileController('body', function(files) {
759 var $scope = angular.element(this).scope();
760 Util.toArray(files).forEach(function(file, i) {
761 gdocs.upload(file, function() {
762 $scope.fetchDocs();
763 });
764 });
765 });
766
767 return gdocs;
768 });
769 </pre>
770
771 <p class="backtotop"><a href="#top">Back to top</a></p>
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698