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

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, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | 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 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 &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".
ericbidelman 2012/10/30 18:56:52 Should this be a list?
mkearney 2012/11/06 00:46:11 Yes. I had to use &lt; 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>&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 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 &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 src="&#123;{doc.icon}&#125;"> &lt;a href="&#123;{doc.alternateLink }&#125;">&#123;{doc.title}&#125;&lt;/a> &#123;{doc.size}&#125;
ericbidelman 2012/10/30 18:56:52 src= -> data-ng-src=
mkearney 2012/11/06 00:46:11 Done.
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 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 &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 src=<strong>"&#123;{doc.icon}&#125;"</strong>> &lt;a href=<strong> "&#123;{doc.alternateLink}&#125;"</strong>><strong>&#123;{doc.title}&#125;</stro ng>&lt;/a> <strong>&#123;{doc.size}&#125;</strong>
ericbidelman 2012/10/30 18:56:52 src -> data-ng-src
mkearney 2012/11/06 00:46:11 Done.
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 $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>
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698