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

Unified 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 side-by-side diff with in-line comments
Download patch
Index: chrome/common/extensions/docs/templates/articles/angular_framework.html
diff --git a/chrome/common/extensions/docs/templates/articles/angular_framework.html b/chrome/common/extensions/docs/templates/articles/angular_framework.html
new file mode 100644
index 0000000000000000000000000000000000000000..46495f7aefe929544495156f14d93eed4eb47054
--- /dev/null
+++ b/chrome/common/extensions/docs/templates/articles/angular_framework.html
@@ -0,0 +1,771 @@
+<meta name="doc-family" content="apps">
+<h1>Build Apps with AngularJS</h1>
+<!--Article written by Eric Bidelman-->
+<p>
+This guide gets you started building packaged apps
+with the <a href="http://angularjs.org/">AngularJS</a> MVC framework.
+To illustrate Angular in action,
+we'll be referencing an actual app built using the framework,
+the Google Drive Uploader.
+The <a href="https://github.com/GoogleChrome/chrome-app-samples/tree/master/gdocs">source code</a>
+is available on GitHub.
+</p>
+
+<h2 id="first">About the app</h2>
+
+<img src="{{static}}/images/uploader.png"
+ width="296"
+ height="347"
+ style="float: right; padding-left: 5px"
+ alt="Google Drive Uploader">
+
+<p>
+The Google Drive Uploader allows users to quickly view and interact
+with files stored in their Google Drive account
+as well as upload new files using the
+<a href="http://www.html5rocks.com/en/tutorials/dnd/basics/">HTML Drag and Drop APIs</a>.
+It's a great example of building an app which talks
+to one of <a href="https://developers.google.com/apis-explorer/#p/">Google's APIs</a>;
+in this case, the Google Drive API.
+</p>
+
+<p class="note">
+<strong>Note: </strong>
+You can also build apps which talk to 3rd party APIs/services
+that are OAuth2-enabled.
+See <a href="http://developer.chrome.com/trunk/apps/app_identity.html#non">non-Google Account authentication</a>.
+</p>
+
+<p>
+The Uploader uses OAuth2 to access the user's data. The
+<a href="http://developer.chrome.com/trunk/apps/experimental.identity.html">chrome.experimental.identity API</a>
+handles fetching an OAuth token for the logged-in user,
+so the hard work is done for us!
+Once we have a long-lived access token,
+the apps uses the
+<a href="https://developers.google.com/drive/get-started">Google Drive API</a>
+to access the user's data.
+</p>
+
+<p>
+Key features this app uses:
+</p>
+
+<ul>
+ <li>Angular JS's autodetection for
+ <a href="http://developer.chrome.com/trunk/apps/app_csp.html">CSP</a></li>
+ <li>Render a list of files fetched from the
+ <a href="https://developers.google.com/drive/get-started">Google Drive API</a></li>
+ <li><a href="http://www.html5rocks.com/en/tutorials/file/filesystem/">HTML5 Filesystem API</a>
+ to store file icons offline</li>
+ <li><a href="http://www.html5rocks.com/en/tutorials/dnd/basics/">HTML5 Drag and Drop</a>
+ for importing/uploading new files from the desktop</li>
+ <li>XHR2 to load images, cross-domain</li>
+ <li><a href="http://developer.chrome.com/trunk/apps/app_identity.html">chrome.experimental.identity API</a>
+ for OAuth authorization</li>
+ <li>Chromeless frames to define the app's own navbar look and feel</li>
+</ul>
+
+<h2 id="second">Creating the manifest</h2>
+
+<p>
+All packaged apps require a <code>manifest.json</code> file
+which contains the information Chrome needs to launch the app.
+The manifest contains relevant metadata and
+lists any special permissions the app needs to run.
+</p>
+
+<p>
+A stripped down version of the Uploader's manifest looks like this:
+</p>
+
+<pre>
+{
+ "name": "Google Drive Uploader",
+ "version": "0.0.1",
+ "manifest_version": 2,
+ "oauth2": {
+ "client_id": "665859454684.apps.googleusercontent.com",
+ "scopes": [
+ "https://docs.google.com/feeds/",
+ "https://docs.googleusercontent.com/",
+ "https://spreadsheets.google.com/feeds/",
+ "https://www.googleapis.com/auth/drive"
+ ]
+ },
+ ...
+ "permissions": [
+ "experimental",
+ "https://docs.google.com/feeds/",
+ "https://docs.googleusercontent.com/",
+ "https://spreadsheets.google.com/feeds/",
+ "https://ssl.gstatic.com/",
+ "https://www.googleapis.com/"
+ ]
+}
+</pre>
+
+<p>
+The most important parts of this manifest are the "oauth2" and "permissions" sections.
+</p>
+
+<p>
+The "oauth2" section defines the required parameters by OAuth2 to do its magic.
+To create a "client_id", follow the instructions in
+<a href="http://developer.chrome.com/apps/app_identity.html#client_id">Get your client id</a>.
+The "scopes" list the authorization scopes
+that the OAuth token will be valid for (for example, the APIs the app wants to access).
+</p>
+
+<p class="note">
+<strong>Note: </strong>
+The Uploader actually uses the
+<a href="https://developers.google.com/google-apps/documents-list/">Documents List API v3</a>
+to access Google Drive content,
+hence needing the first several scopes to access all the different types of files.
+However, when using the Google Drive API, you only need
+<a href="https://www.googleapis.com/auth/drive">"https://www.googleapis.com/auth/drive"</a>.
+</p>
+
+<p>
+The "permissions" section includes the "experimental" bit
+(necessary because the current chrome.identity API is still experimental)
+and additional URLs that the app will access via XHR2.
+The URL prefixes are required in order for Chrome
+to know which cross-domain requests to allow.
+</p>
+
+<h2 id="three">Creating the event page</h2>
+
+<p>
+All packaged apps require a background script/page
+to launch the app and respond to system events.
+</p>
+
+<p>
+In its
+<a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/js/background.js">background.js</a>
+script,
+Drive Uploader opens a 500x600px window to the main page.
+It also specifies a minimum height and width for the window
+so the content doesn't become too crunched:
+</p>
+
+<pre>
+chrome.app.runtime.onLaunched.addListener(function(launchData) {
+ chrome.app.window.create('../main.html', {
+ width: 500,
+ height: 600,
+ minWidth: 500,
+ minHeight: 600,
+ frame: 'none'
+ });
+});
+</pre>
+
+<p>
+The window is created as a chromeless window (frame: 'none').
+By default, windows render with the OS's default close/expand/minimize bar:
+</p>
+
+<img src="{{static}}/images/noframe.png"
+ width="508"
+ height="75"
+ alt="Google Drive Uploader with no frame">
+
+<p>
+The Uploader uses <code>frame: 'none'</code> to render the window as a "blank slate"
+and creates a custom close button in <code>main.html</code>:
+</p>
+
+<img src="{{static}}/images/customframe.png"
+ width="504"
+ height="50"
+ alt="Google Drive Uploader with custom frame">
+
+<p>
+The entire navigational area is wrapped in a &lt;nav> (see next section).
+To declutter the app a bit,
+the custom close button is hidden until the user interacts with this the area:
+</p>
+
+<pre>
+&lt;style>
+nav:hover #close-button {
+ opacity: 1;
+}
+
+#close-button {
+ float: right;
+ padding: 0 5px 2px 5px;
+ font-weight: bold;
+ opacity: 0;
+ -webkit-transition: all 0.3s ease-in-out;
+}
+&lt;/style>
+
+&lt;button class="btn" id="close-button" title="Close">x&lt;/button>
+</pre>
+
+<p>
+In
+<a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/js/app.js">app.js</a>,
+this button is hooked up to <code>window.close()</code>.
+</p>
+
+<h2 id="four">Designing the app the Angular way</h2>
+
+<p>
+Angular is an MVC framework, so we need to define the app in such a way that a
+model, view, and controller logically fall out of it. Luckily, this is trivial when using Angular.
+</p>
+
+<p>
+The View is the easiest, so let's start there.
+</p>
+
+<h3 id="view">Creating the view</h3>
+
+<p>
+<a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/main.html">main.html</a>
+is the "V" in MVC; where we define HTML templates to render data into.
+In Angular, templates are simple blocks of HTML with some special sauce.
+</p>
+
+<p>
+Ultimately we want to display the user's list of files.
+For that, a simple &lt;ul> list makes sense.
+The Angular bits are highlighted in bold:
+</p>
+
+<pre>
+&lt;ul>
+ &lt;li <strong>data-ng-repeat="doc in docs"</strong>>
+ &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>
+ &lt;span class="date"><strong>&#123;{doc.updatedDate}&#125;</strong>&lt;/span>
+ &lt;/li>
+&lt;/ul>
+</pre>
+
+<p>
+This reads exactly as it looks:
+stamp out an &lt;li> for every doc in our data model "docs".
+Each item contains a file icon, link to open the file on the web,
+and last updatedDate.
+</p>
+
+<p class="note">
+<strong>Note: </strong>
+To make the template valid HTML,
+we're using <code>data-*</code> attributes for Angular's
+<a href="http://docs.angularjs.org/api/ng.directive:ngRepeat">nRepeat</a> iterator,
+but you don't have to.
+You could easily write the repeater as <code>&lt;li ng-repeat="doc in docs"></code>.
+</p>
+
+<p>
+Next, we need to tell Angular which controller will oversee this template's rendering.
+For that, we use the
+<a href="http://docs.angularjs.org/api/ng.directive:ngController">ngController</a>
+directive to tell the <code>DocsController</code> to have reign over the template <body>:
+</p>
+
+<pre>
+&lt;body <strong>data-ng-controller="DocsController"</strong>>
+&lt;section id="main">
+ &lt;ul>
+ &lt;li data-ng-repeat="doc in docs">
+ &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;
+ &lt;span class="date">&#123;{doc.updatedDate}&#125;&lt;/span>
+ &lt;/li>
+ &lt;/ul>
+&lt;/section>
+&lt;/body>
+</pre>
+
+<p>
+Keep in mind,
+what you don't see here is us hooking up event listeners or properties for data binding.
+Angular is doing that heavy lifting for us!
+</p>
+
+<p>
+The last step is to make Angular light up our templates.
+The typical way to do that is include the
+<a href="http://docs.angularjs.org/api/ng.directive:ngApp">ngApp</a>
+directive all the way up on &lt;html>:
+</p>
+
+<pre>
+&lt;html <strong>data-ng-app="gDriveApp"</strong>>
+</pre>
+
+<p>
+You could also scope the app down
+to a smaller portion of the page if you wanted to.
+We only have one controller in this app,
+but if we were to add more later,
+putting <a href="http://docs.angularjs.org/api/ng.directive:ngApp">ngApp</a>
+on the topmost element makes the entire page Angular-ready.
+</p>
+
+<p>
+The final product for <code>main.html</code> looks something like this:
+</p>
+
+<pre>
+&lt;html <strong>data-ng-app="gDriveApp"</strong>>
+&lt;head>
+ …
+ <!-- crbug.com/120693: so we don't need target="_blank" on every anchor. -->
+ &lt;base target="_blank">
+&lt;/head>
+&lt;body <strong>data-ng-controller="DocsController"</strong>>
+&lt;section id="main">
+ &lt;nav>
+ &lt;h2>Google Drive Uploader&lt;/h2>
+ &lt;button class="btn" <strong>data-ng-click="fetchDocs()"</strong>>Refresh&lt;/button>
+ &lt;button class="btn" id="close-button" title="Close">&lt;/button>
+ &lt;/nav>
+ &lt;ul>
+ &lt;li <strong>data-ng-repeat="doc in docs"</strong>>
+ &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>
+ &lt;span class="date"><strong>&#123;{doc.updatedDate}&#125;</strong>&lt;/span>
+ &lt;/li>
+ &lt;/ul>
+&lt;/section>
+</pre>
+
+<h3 id="csp">A word on Content Security Policy</h3>
+
+<p>
+Unlike many other JS MVC frameworks,
+Angular v1.1.0+ requires no tweaks to work within a strict
+<a href="http://developer.chrome.com/trunk/apps/app_csp.html">CSP</a>.
+It just works, out of the box!
+</p>
+
+<p>
+However, if you're using an older version
+of Angular between v1.0.1 and v1.1.0,
+you'll need tell Angular to run in a "content security mode".
+This is done by including the
+<a href="http://docs.angularjs.org/api/ng.directive:ngCsp">ngCsp</a>
+directive alongside <a href="http://docs.angularjs.org/api/ng.directive:ngApp">ngApp</a>:
+</p>
+
+<pre>
+&lt;html data-ng-app data-ng-csp>
+</pre>
+
+<h3 id="authorization">Handling authorization</h3>
+
+<p>
+The data model isn't generated by the app itself.
+Instead, it's populated from an external API (the Google Drive API).
+Thus, there's a bit of work necessary in order to populate the app's data.
+</p>
+
+<p>
+Before we can make an API request,
+we need to fetch an OAuth token for the user's Google Account.
+For that, we've created a method to wrap the call
+to <code>chrome.experimental.identity.getAuthToken()</code> and
+store the <code>accessToken</code>,
+which we can reuse for future calls to the Drive API.
+</p>
+
+<pre>
+GDocs.prototype.auth = function(opt_callback) {
+ try {
+ <strong>chrome.experimental.identity.getAuthToken({interactive: false}, function(token) {</strong>
+ if (token) {
+ this.accessToken = token;
+ opt_callback && opt_callback();
+ }
+ }.bind(this));
+ } catch(e) {
+ console.log(e);
+ }
+};
+</pre>
+
+<p class="note">
+<strong>Note: </strong>
+Passing the optional callback gives us the flexibility
+of knowing when the OAuth token is ready.
+</p>
+
+<p class="note">
+<strong>Note: </strong>
+To simplify things a bit,
+we've created a library,
+<a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/js/gdocs.js">gdocs.js</a>
+to handle API tasks.
+</p>
+
+<p>
+Once we have the token,
+it's time to make requests against the Drive API and populate the model.
+</p>
+
+<h3 id="skeleton">Skeleton controller</h3>
+
+<p>
+The "model" for the Uploader is a simple array (called docs)
+of objects that will get rendered as those &lt;li>s in the template:
+</p>
+
+<pre>
+var gDriveApp = angular.module('gDriveApp', []);
+
+gDriveApp.factory('gdocs', function() {
+ var gdocs = new GDocs();
+ return gdocs;
+});
+
+function DocsController($scope, $http, gdocs) {
+ $scope.docs = [];
+
+ $scope.fetchDocs = function() {
+ ...
+ };
+
+ // Invoke on ctor call. Fetch docs after we have the oauth token.
+ gdocs.auth(function() {
+ $scope.fetchDocs();
+ });
+
+}
+</pre>
+
+<p>
+Notice that <code>gdocs.auth()</code> is called
+as part of the DocsController constructor.
+When Angular's internals create the controller,
+we're insured to have a fresh OAuth token waiting for the user.
+</p>
+
+<h2 id="five">Fetching data</h2>
+
+<p>
+Template laid out.
+Controller scaffolded.
+OAuth token in hand.
+Now what?
+</p>
+
+<p>
+It's time to define the main controller method,
+<code>fetchDocs()</code>.
+It's the workhorse of the controller,
+responsible for requesting the user's files and
+filing the docs array with data from API responses.
+</p>
+
+<pre>
+$scope.fetchDocs = function() {
+ $scope.docs = []; // First, clear out any old results
+
+ // Response handler that doesn't cache file icons.
+ var successCallback = function(resp, status, headers, config) {
+ var docs = [];
+ var totalEntries = resp.feed.entry.length;
+
+ resp.feed.entry.forEach(function(entry, i) {
+ var doc = {
+ title: entry.title.$t,
+ updatedDate: Util.formatDate(entry.updated.$t),
+ updatedDateFull: entry.updated.$t,
+ icon: gdocs.getLink(entry.link,
+ 'http://schemas.google.com/docs/2007#icon').href,
+ alternateLink: gdocs.getLink(entry.link, 'alternate').href,
+ size: entry.docs$size ? '( ' + entry.docs$size.$t + ' bytes)' : null
+ };
+
+ $scope.docs.push(doc);
+
+ // Only sort when last entry is seen.
+ if (totalEntries - 1 == i) {
+ $scope.docs.sort(Util.sortByDate);
+ }
+ });
+ };
+
+ var config = {
+ params: {'alt': 'json'},
+ headers: {
+ 'Authorization': 'Bearer ' + gdocs.accessToken,
+ 'GData-Version': '3.0'
+ }
+ };
+
+ $http.get(gdocs.DOCLIST_FEED, config).success(successCallback);
+};
+</pre>
+
+<p>
+<code>fetchDocs()</code> uses Angular's <code>$http</code> service
+to retrieve the main feed over XHR.
+The oauth access token is included
+in the <code>Authorization</code> header
+along with other custom headers and parameters.
+</p>
+
+<p>
+The <code>successCallback</code> processes the API response and
+creates a new doc object for each entry in the feed.
+</p>
+
+<p>
+If you run <code>fetchDocs()</code> right now,
+everything works and the list of files shows up:
+</p>
+
+<img src="{{static}}/images/listoffiles.png"
+ width="580"
+ height="680"
+ alt="Fetched list of files in Google Drive Uploader">
+
+<p>
+Woot!
+</p>
+
+<p>
+Wait,...we're missing those neat file icons.
+What gives?
+A quick check of the console shows a bunch of CSP-related errors:
+</p>
+
+<img src="{{static}}/images/csperrors.png"
+ width="947"
+ height="84"
+ alt="CSP errors in developer console">
+
+<p>
+The reason is that we're trying
+to set the icons <code>img.src</code> to external URLs.
+This violates CSP.
+For example:
+<code>https://ssl.gstatic.com/docs/doclist/images/icon_10_document_list.png</code>.
+To fix this,
+we need to pull in these remote assets locally to the app.
+</p>
+
+<h3 id="import">Importing remote image assets</h3>
+
+<p>
+For CSP to stop yelling at us,
+we use XHR2 to "import" the file icons as Blobs,
+then set the <code>img.src</code>
+to a <code>blob: URL</code> created by the app.
+</p>
+
+<p>
+Here's the updated <code>successCallback</code>
+with the added XHR code:
+</p>
+
+<pre>
+var successCallback = function(resp, status, headers, config) {
+ var docs = [];
+ var totalEntries = resp.feed.entry.length;
+
+ resp.feed.entry.forEach(function(entry, i) {
+ var doc = {
+ ...
+ };
+
+ <strong>$http.get(doc.icon, {responseType: 'blob'}).success(function(blob) {
+ console.log('Fetched icon via XHR');
+
+ blob.name = doc.iconFilename; // Add icon filename to blob.
+
+ writeFile(blob); // Write is async, but that's ok.
+
+ doc.icon = window.URL.createObjectURL(blob);
+
+ $scope.docs.push(doc);
+
+ // Only sort when last entry is seen.
+ if (totalEntries - 1 == i) {
+ $scope.docs.sort(Util.sortByDate);
+ }
+ });</strong>
+ });
+};
+</pre>
+
+<p>
+Now that CSP is happy with us again,
+we get nice file icons:
+</p>
+
+<img src="{{static}}/images/fileicons.png"
+ width="580"
+ height="680"
+ alt="Google Drive Uploader with file icons">
+
+<h2 id="six">Going offline: caching external resources</h2>
+
+<p>
+The obvious optimization that needs to be made:
+not make 100s of XHR requests for each file icon
+on every call to <code>fetchDocs()</code>.
+Verify this in the Developer Tools console
+by pressing the "Refresh" button several times.
+Every time, n images are fetched:
+</p>
+
+<img src="{{static}}/images/fetchedicon.png"
+ width="118"
+ height="19"
+ alt="Console log 65: Fetched icon via XHR">
+
+<p>
+Let's modify <code>successCallback</code>
+to add a caching layer.
+The additions are highlighted in bold:
+</p>
+
+<pre>
+$scope.fetchDocs = function() {
+ ...
+
+ // Response handler that caches file icons in the filesystem API.
+ var successCallbackWithFsCaching = function(resp, status, headers, config) {
+ var docs = [];
+ var totalEntries = resp.feed.entry.length;
+
+ resp.feed.entry.forEach(function(entry, i) {
+ var doc = {
+ ...
+ };
+
+ <strong>// 'https://ssl.gstatic.com/doc_icon_128.png' -> 'doc_icon_128.png'
+ doc.iconFilename = doc.icon.substring(doc.icon.lastIndexOf('/') + 1);</strong>
+
+ // If file exists, it we'll get back a FileEntry for the filesystem URL.
+ // Otherwise, the error callback will fire and we need to XHR it in and
+ // write it to the FS.
+ <strong>var fsURL = fs.root.toURL() + FOLDERNAME + '/' + doc.iconFilename;
+ window.webkitResolveLocalFileSystemURL(fsURL, function(entry) {
+ doc.icon = entry.toURL(); // should be === to fsURL, but whatevs.</strong>
+
+ $scope.docs.push(doc); // add doc to model.
+
+ // Only want to sort and call $apply() when we have all entries.
+ if (totalEntries - 1 == i) {
+ $scope.docs.sort(Util.sortByDate);
+ $scope.$apply(function($scope) {}); // Inform angular that we made changes.
+ }
+
+ <strong>}, function(e) {
+ // Error: file doesn't exist yet. XHR it in and write it to the FS.
+
+ $http.get(doc.icon, {responseType: 'blob'}).success(function(blob) {
+ console.log('Fetched icon via XHR');
+
+ blob.name = doc.iconFilename; // Add icon filename to blob.
+
+ writeFile(blob); // Write is async, but that's ok.
+
+ doc.icon = window.URL.createObjectURL(blob);
+
+ $scope.docs.push(doc);
+
+ // Only sort when last entry is seen.
+ if (totalEntries - 1 == i) {
+ $scope.docs.sort(Util.sortByDate);
+ }
+ });
+
+ });</strong>
+ });
+ };
+
+ var config = {
+ ...
+ };
+
+ $http.get(gdocs.DOCLIST_FEED, config).success(successCallbackWithFsCaching);
+};
+</pre>
+
+<p>
+Notice that in the <code>webkitResolveLocalFileSystemURL()</code> callback
+we're calling <code>$scope.$apply()</code>
+when the last entry is seen.
+Normally calling <code>$apply()</code> isn't necessary.
+Angular detects changes to data models automagically.
+However in our case,
+we have an addition layer of asynchronous callback
+that Angular isn't aware of.
+We must explicitly tell Angular when our model has been updated.
+</p>
+
+<p>
+On first run,
+the icons won't be in the HTML5 Filesystem and the calls to
+<code>window.webkitResolveLocalFileSystemURL()</code> will result
+in its error callback being invoked.
+For that case,
+we can reuse the technique from before and fetch the images.
+The only difference this time is that
+each blob is written to the filesystem (see
+<a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/js/app.js#L25">writeBlob()</a>).
+The console verifies this behavior:
+</p>
+
+<img src="{{static}}/images/writecompleted.png"
+ width="804"
+ height="42"
+ alt="Console log 100: Write completed">
+
+<p>
+Upon next run (or press of the "Refresh" button),
+the URL passed to <code>webkitResolveLocalFileSystemURL()</code> exists
+because the file has been previously cached.
+The app sets the <code>doc.icon</code>
+to the file's <code>filesystem: URL</code> and
+avoids making the costly XHR for the icon.
+</p>
+
+<h2 id="seven">Drag and drop uploading</h2>
+
+<p>
+An uploader app is false advertising
+if it can't upload files!
+</p>
+
+<p>
+<a href="https://github.com/GoogleChrome/chrome-app-samples/blob/master/gdocs/js/app.js#L177">app.js</a>
+handles this feature by implementing a small library
+around HTML5 Drag and Drop called <code>DnDFileController</code>.
+It gives the ability to drag in files from the desktop
+and have them uploaded to Google Drive.
+</p>
+
+<p>
+Simply adding this to the gdocs service does the job:
+</p>
+
+<pre>
+gDriveApp.factory('gdocs', function() {
+ var gdocs = new GDocs();
+
+ var dnd = new DnDFileController('body', function(files) {
+ var $scope = angular.element(this).scope();
+ Util.toArray(files).forEach(function(file, i) {
+ gdocs.upload(file, function() {
+ $scope.fetchDocs();
+ });
+ });
+ });
+
+ return gdocs;
+});
+</pre>
+
+<p class="backtotop"><a href="#top">Back to top</a></p>

Powered by Google App Engine
This is Rietveld 408576698