1 <meta name=
"doc-family" content=
"apps">
2 <h1>Build Apps with AngularJS
</h1>
3 <!--Article written by Eric Bidelman-->
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/samples/gdrive">source code
</a>
11 is available on GitHub.
14 <h2 id=
"first">About the app
</h2>
16 <img src=
"{{static}}/images/uploader.png"
19 style=
"float: right; padding-left: 5px"
20 alt=
"Google Drive Uploader">
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.
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>.
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,
46 <a href=
"https://developers.google.com/drive/get-started">Google Drive API
</a>
47 to access the user's data.
51 Key features this app uses:
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>
69 <h2 id=
"second">Creating the manifest
</h2>
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.
79 A stripped down version of the Uploader's manifest looks like this:
82 <pre data-filename=
"manifest.json">
84 "name":
"Google Drive Uploader",
86 "manifest_version":
2,
88 "client_id":
"665859454684.apps.googleusercontent.com",
90 "https://www.googleapis.com/auth/drive"
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/"
105 The most important parts of this manifest are the
"oauth2" and
"permissions" sections.
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).
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.
122 <h2 id=
"three">Creating the event page
</h2>
125 All Chrome Apps require a background script/page
126 to launch the app and respond to system events.
131 <a href=
"https://github.com/GoogleChrome/chrome-app-samples/blob/master/samples/gdrive/js/background.js">background.js
</a>
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:
138 <pre data-filename=
"background.js">
139 chrome.app.runtime.onLaunched.addListener(function(launchData) {
140 chrome.app.window.create('../main.html', {
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:
158 <img src=
"{{static}}/images/noframe.png"
161 alt=
"Google Drive Uploader with no frame">
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>:
168 <img src=
"{{static}}/images/customframe.png"
171 alt=
"Google Drive Uploader with custom frame">
174 The entire navigational area is wrapped in a
<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:
179 <pre data-filename=
"main.css">
181 nav:hover #close-button {
187 padding:
0 5px
2px
5px;
190 -webkit-transition: all
0.3s ease-in-out;
194 <pre data-filename=
"main.html">
195 <button
class=
"btn" id=
"close-button" title=
"Close">x
</button
>
200 <a href=
"https://github.com/GoogleChrome/chrome-app-samples/blob/master/samples/gdrive/js/app.js">app.js
</a>,
201 this button is hooked up to
<code>window.close()
</code>.
204 <h2 id=
"four">Designing the app the Angular way
</h2>
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.
212 The View is the easiest, so let's start there.
215 <h3 id=
"view">Creating the view
</h3>
218 <a href=
"https://github.com/GoogleChrome/chrome-app-samples/blob/master/samples/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.
224 Ultimately we want to display the user's list of files.
225 For that, a simple
<ul
> list makes sense.
226 The Angular bits are highlighted in bold:
229 <pre data-filename=
"main.html">
231 <li
<strong>data-ng-repeat=
"doc in docs"</strong>>
232 <img data-ng-src=
<strong>"{{doc.icon}}"</strong>> <a href=
<strong>"{{doc.alternateLink}}"</strong>><strong>{{doc.title}
}</strong></a
>
233 <strong>{{doc.size}
}</strong>
234 <span
class=
"date"><strong>{{doc.updatedDate}
}</strong></span
>
240 This reads exactly as it looks:
241 stamp out an
<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.
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><li
ng-repeat=
"doc in docs"></code>.
256 Next, we need to tell Angular which controller will oversee this template's rendering.
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
<body
>:
262 <pre data-filename=
"main.html">
263 <body
<strong>data-ng-controller=
"DocsController"</strong>>
264 <section
id=
"main">
266 <li
data-ng-repeat=
"doc in docs">
267 <img
data-ng-src=
"{{doc.icon}}"> <a
href=
"{{doc.alternateLink}}">{{doc.title}
}</a
> {{doc.size}
}
268 <span
class=
"date">{{doc.updatedDate}
}</span
>
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!
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
<html
>:
288 <pre data-filename=
"main.html">
289 <html
<strong>data-ng-app=
"gDriveApp"</strong>>
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.
302 The final product for
<code>main.html
</code> looks something like this:
305 <pre data-filename=
"main.html">
306 <html
<strong>data-ng-app=
"gDriveApp"</strong>>
309 <!-- crbug.com/120693: so we don't need target="_blank" on every anchor. -->
310 <base
target=
"_blank">
312 <body
<strong>data-ng-controller=
"DocsController"</strong>>
313 <section
id=
"main">
315 <h2
>Google Drive Uploader
</h2
>
316 <button
class=
"btn" <strong>data-ng-click=
"fetchDocs()"</strong>>Refresh
</button
>
317 <button
class=
"btn" id=
"close-button" title=
"Close"></button
>
320 <li
<strong>data-ng-repeat=
"doc in docs"</strong>>
321 <img data-ng-src=
<strong>"{{doc.icon}}"</strong>> <a href=
<strong>"{{doc.alternateLink}}"</strong>><strong>{{doc.title}
}</strong></a
> <strong>{{doc.size}
}</strong>
322 <span
class=
"date"><strong>{{doc.updatedDate}
}</strong></span
>
328 <h3 id=
"csp">A word on Content Security Policy
</h3>
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!
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>:
346 <pre data-filename=
"main.html">
347 <html data-ng-app data-ng-csp
>
350 <h3 id=
"authorization">Handling authorization
</h3>
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.
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.
367 <pre data-filename=
"gdocs.js">
368 GDocs.prototype.auth = function(opt_callback) {
370 <strong>chrome.identity.getAuthToken({interactive: false}, function(token) {
</strong>
372 this.accessToken = token;
373 opt_callback && opt_callback();
383 <strong>Note:
</strong>
384 Passing the optional callback gives us the flexibility
385 of knowing when the OAuth token is ready.
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/samples/gdrive/js/gdocs.js">gdocs.js
</a>
397 Once we have the token,
398 it's time to make requests against the Drive API and populate the model.
401 <h3 id=
"skeleton">Skeleton controller
</h3>
404 The
"model" for the Uploader is a simple array (called docs)
405 of objects that will get rendered as those
<li
>s in the template:
408 <pre data-filename=
"app.js">
409 var gDriveApp = angular.module('gDriveApp', []);
411 gDriveApp.factory('gdocs', function() {
412 var gdocs = new GDocs();
416 function DocsController($scope, $http, gdocs) {
419 $scope.fetchDocs = function() {
423 // Invoke on ctor call. Fetch docs after we have the oauth token.
424 gdocs.auth(function() {
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.
438 <h2 id=
"five">Fetching data
</h2>
442 Controller scaffolded.
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.
455 <pre data-filename=
"app.js">
456 $scope.fetchDocs = function() {
457 $scope.docs = []; // First, clear out any old results
459 // Response handler that doesn't cache file icons.
460 var successCallback = function(resp, status, headers, config) {
462 var totalEntries = resp.feed.entry.length;
464 resp.feed.entry.forEach(function(entry, i) {
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
475 $scope.docs.push(doc);
477 // Only sort when last entry is seen.
478 if (totalEntries -
1 == i) {
479 $scope.docs.sort(Util.sortByDate);
485 params: {'alt': 'json'},
487 'Authorization': 'Bearer ' + gdocs.accessToken,
488 'GData-Version': '
3.0'
492 $http.get(gdocs.DOCLIST_FEED, config).success(successCallback);
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.
505 The
<code>successCallback
</code> processes the API response and
506 creates a new doc object for each entry in the feed.
510 If you run
<code>fetchDocs()
</code> right now,
511 everything works and the list of files shows up:
514 <img src=
"{{static}}/images/listoffiles.png"
517 alt=
"Fetched list of files in Google Drive Uploader">
524 Wait,...we're missing those neat file icons.
526 A quick check of the console shows a bunch of CSP-related errors:
529 <img src=
"{{static}}/images/csperrors.png"
532 alt=
"CSP errors in developer console">
535 The reason is that we're trying
536 to set the icons
<code>img.src
</code> to external URLs.
539 <code>https://ssl.gstatic.com/docs/doclist/images/icon_10_document_list.png
</code>.
541 we need to pull in these remote assets locally to the app.
544 <h3 id=
"import">Importing remote image assets
</h3>
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.
554 Here's the updated
<code>successCallback
</code>
555 with the added XHR code:
558 <pre data-filename=
"app.js">
559 var successCallback = function(resp, status, headers, config) {
561 var totalEntries = resp.feed.entry.length;
563 resp.feed.entry.forEach(function(entry, i) {
568 <strong>$http.get(doc.icon, {responseType: 'blob'}).success(function(blob) {
569 console.log('Fetched icon via XHR');
571 blob.name = doc.iconFilename; // Add icon filename to blob.
573 writeFile(blob); // Write is async, but that's ok.
575 doc.icon = window.URL.createObjectURL(blob);
577 $scope.docs.push(doc);
579 // Only sort when last entry is seen.
580 if (totalEntries -
1 == i) {
581 $scope.docs.sort(Util.sortByDate);
589 Now that CSP is happy with us again,
590 we get nice file icons:
593 <img src=
"{{static}}/images/fileicons.png"
596 alt=
"Google Drive Uploader with file icons">
598 <h2 id=
"six">Going offline: caching external resources
</h2>
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:
609 <img src=
"{{static}}/images/fetchedicon.png"
612 alt=
"Console log 65: Fetched icon via XHR">
615 Let's modify
<code>successCallback
</code>
616 to add a caching layer.
617 The additions are highlighted in bold:
620 <pre data-filename=
"app.js">
621 $scope.fetchDocs = function() {
624 // Response handler that caches file icons in the filesystem API.
625 var successCallbackWithFsCaching = function(resp, status, headers, config) {
627 var totalEntries = resp.feed.entry.length;
629 resp.feed.entry.forEach(function(entry, i) {
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>
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>
644 $scope.docs.push(doc); // add doc to model.
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.
652 <strong>}, function(e) {
653 // Error: file doesn't exist yet. XHR it in and write it to the FS.
655 $http.get(doc.icon, {responseType: 'blob'}).success(function(blob) {
656 console.log('Fetched icon via XHR');
658 blob.name = doc.iconFilename; // Add icon filename to blob.
660 writeFile(blob); // Write is async, but that's ok.
662 doc.icon = window.URL.createObjectURL(blob);
664 $scope.docs.push(doc);
666 // Only sort when last entry is seen.
667 if (totalEntries -
1 == i) {
668 $scope.docs.sort(Util.sortByDate);
680 $http.get(gdocs.DOCLIST_FEED, config).success(successCallbackWithFsCaching);
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.
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.
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.
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/samples/gdrive/js/app.js#L27">writeFile()
</a>).
706 The console verifies this behavior:
709 <img src=
"{{static}}/images/writecompleted.png"
712 alt=
"Console log 100: Write completed">
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.
723 <h2 id=
"seven">Drag and drop uploading
</h2>
726 An uploader app is false advertising
727 if it can't upload files!
731 <a href=
"https://github.com/GoogleChrome/chrome-app-samples/blob/master/samples/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.
739 Simply adding this to the gdocs service does the job:
742 <pre data-filename=
"app.js">
743 gDriveApp.factory('gdocs', function() {
744 var gdocs = new GDocs();
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() {
759 <p class=
"backtotop"><a href=
"#top">Back to top
</a></p>