Use Dropbox for Persistent Storage with an Angular Application
Tags: angular, typescript, tutorial
Published: 2020-03-06 09:50:06

In this post, I show an example of using Dropbox with an Angular application. I'll show how to authenticate to Dropbox with Angular and how to read and write files to Dropbox. This can be used to add persistent storage to the application without needing a custom backend service or API. First, I'll go over the functionality of the app, then show how to create the Angular piece. Finally, I'll detail how to add Dropbox integration. This post requires the [Angular CLI](https://cli.angular.io/) and some familiarity with Angular but you don't need to be an expert. [NodeJs and NPM are also required.](https://nodejs.org) You must also have a [Dropbox](https://www.dropbox.com) account. The free version works. The complete example is [available on GitHub.](https://github.com/cwl157/angular-dropbox)  

### Application overview

We're going to create an application that allows the user to manage a list of books and use Dropbox for storage. The application performs create, read, and delete operations. It's an angular application with no routing to keep it simple. The top of the page contains a form for entering new Books.  
![Enter Book Form](https://dev-to-uploads.s3.amazonaws.com/i/ecr5j510flh4wspy7w4k.PNG)  

The bottom part of the page displays the list of books with a button to remove a book from the list.  
![List Books](https://dev-to-uploads.s3.amazonaws.com/i/tq08w8f067ji9ypwqr92.PNG)  

### Create the Angular Application

We're going to use the Angular CLI to create a new application. If you don't have it installed, [click here](https://cli.angular.io/) to download it. Once installed, from a terminal / command line / powershell run `ng new` to create a new app. The CLI will prompt for a name, I called it 'angular-dropbox' and ask if you would like to include routing. Routing is not required for this project so answer 'N'. Finally, it asks which stylesheet format you want to use, pick CSS (the default). Next run `ng serve` to verify the basic Angular application is working out of the box.  

To keep this application simple and the focus on Dropbox integration, I added everything to the main app component. First, add a couple lines to `app.component.css` to format the table to display books.
```css
    table, th, td {
        border: 1px solid black;
    }
```
Next, we're going to create a regular typescript class to hold the Book data named `book.entity.ts`.
```typescript
    export class BookEntity {
        public Title: string;
        public Author: string;
        public Length: number | null;
        public PublishedDate: Date | null;

        public Reset(): void {
            this.Title = "";
            this.Author = "";
            this.Length = null;
            this.PublishedDate = null;
        }
    }
```
Now it's time to setup the html template. Remove everything from `app.component.html` and replace it with the template below.This has the form at the top of the page, binding data to the `newBook` object in the typescript file, which we'll define later. It also loops through a list of books to display in the table.
```html
    <label for="title">Title: </label><input id="title" type="text" [(ngModel)]="newBook.Title" />
    <br /><br />
    <label for="Author">Author: </label><input id="author" type="text" [(ngModel)]="newBook.Author" />
    <br /><br />
    <label for="length">Length (In Pages): </label><input id="length" type="number" min="0" [(ngModel)]="newBook.Length" />
    <br /><br />
    <label for="published">Date Published: </label><input id="published" type="date" [(ngModel)]="newBook.PublishedDate" />
    <br /><br />
    <button (click)="addBook()">Add Book</button>

    <h1>Books</h1>
      <table>
    <thead>
      <tr>
        <th>Title</th>
        <th>Author</th>
        <th>Length (In Pages)</th>
        <th>Publish Date</th>
        <th>Delete</th>
      </tr>
    </thead>
    <tbody>
      <tr *ngFor="let book of Books; let i = index">
        <td>{{book.Title}}</td>
        <td>{{book.Author}}</td>
        <td>{{book.Length}}</td>
        <td>{{book.PublishedDate}}</td>
        <td><button (click)="removeBook(i)">X</button></td>
      </tr>
    </tbody>
    </table>
```
Since our application binds data using a form, we need to import the `FormsModule` in `app.module.ts`. The file looks like this after it's added.
```typescript
    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { FormsModule }   from '@angular/forms';

    import { AppComponent } from './app.component';

    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        FormsModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
```
Finally, we need to modify the `app.component.ts` file. There is a `BookEntity` object to store the new book being added and an array of `BookEntity` objects to keep track of the list to display. There are functions to add and remove a book. The `Books` property is static with a getter function and that is so we can populate it later when we add Dropbox integration. `ngOnInit()` will be used at that time also.
```typescript
    import { Component, OnInit } from '@angular/core';

    import { BookEntity } from './book.entity';

    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })

    export class AppComponent implements OnInit {
      newBook: BookEntity;
      static books: BookEntity[];

      constructor() {
        this.newBook = new BookEntity;
        AppComponent.books = [];
      }

      ngOnInit() {
        // Load dropbox data here...
      }

      get Books():BookEntity[] {
        return AppComponent.books;
      }

      public addBook() {
        let b: BookEntity = new BookEntity;
        b.Title = this.newBook.Title
        b.Author = this.newBook.Author;
        b.Length = this.newBook.Length;
        b.PublishedDate = this.newBook.PublishedDate;
        AppComponent.books.push(b);

        this.newBook.Reset();
      }

      public removeBook(i) {
         AppComponent.books.splice(i, 1);

      }
    }
```
At this point, we have a working angular application that allows us to add and remove books. Run `ng serve` to test it. Next, we'll add Dropbox to persist the list of books.  

### Dropbox Integration

The first step is to add our application to Dropbox. Login to Dropbox and go to the [Dropbox App Console.](https://www.dropbox.com/developers/apps) Click Create app in the upper right corner. Under step 1 choose "dropbox API", step 2 choose the first option, App folder– Access to a single folder created specifically for your app. This means the application only has access to a specific folder in Dropbox, not all of Dropbox. This works better for our app because it allows us to store all the app data in 1 place and is more secure because our app can't access all of the user's files in Dropbox. Finally, give the app a name.  

Once the app is created, we need to configure the OAuth2 settings to allow us to connect from the Dropbox Javascript API. Enter a redirect url to localhost and make sure the port matches the port the angular application is running on. By default, the Angular CLI uses port `4200`. Also, make sure "Allow implicit grant" is set to "Allow". Below are some sample settings.  
![oauth settings](https://dev-to-uploads.s3.amazonaws.com/i/r8zbgj65lguwqqrex1eb.PNG)  

The second step is to get the Dropbox Javascript SDK and include it in the project. I made a local copy of it from the [Dropbox Javascript SDK examples](https://github.com/dropbox/dropbox-sdk-js/tree/master/examples) on GitHub. It's also included in the [GitHub repo](https://github.com/cwl157/angular-dropbox) for this project. You need to add it to the build scripts in the `angular.json` file.
```json
    "build": {
              "builder": "@angular-devkit/build-angular:browser",
              "options": {
                "outputPath": "dist/angular-dropbox",
                "index": "src/index.html",
                "main": "src/main.ts",
                "polyfills": "src/polyfills.ts",
                "tsConfig": "src/tsconfig.app.json",
                "assets": [
                  "src/favicon.ico",
                  "src/assets"
                ],
                "styles": [
                  "src/styles.css"
                ],
                "scripts": [
                  "src/Dropbox-sdk.min.js"
                ],
                "es5BrowserSupport": true
              }
```
Stop and restart `ng serve`. To test if the SDK was loaded, open the developer tools in the browser and type `Dropbox` in the console. If a function is returned, the Dropbox Javascript SDK has been loaded. If undefined is returned, Angular is not including the Dropbox SDK in the build / serve.  

Next, we're going to add the ability to authenticate to Dropbox. This requires adding a link to login to Dropbox and then be returned to the app with an authentication token returned in the query string. This authentication token let's us make API requests to Dropbox. If this were a server-side app, we could store this authentication token for future use but since this application is entirely client-side, I'm only storing the token in memory in a local variable in the app component. This means the user needs to re-authenticate to Dropbox each time they navigate to the app. If the application had multiple components we could store this token in an Angular service when the app is initialized to share it across components. Since this app is entirely client-side in the browser, this is the most secure way to manage the authentication token.  

There are a couple utility functions I took from the [Dropbox examples on GitHub](https://github.com/dropbox/dropbox-sdk-js/tree/master/examples/javascript) that parses the auth token out of the query string. These are in Javascript in the examples so I converted them to typescript so I can import them into the `app.component.ts` file. I added them to a file called `utils.ts` which is shown below.
```typescript
    /* This functions below are from the Dropbox SDK examples to retreive the auth token from the query string */
    export class Utils {

        private static parseQueryString(str: string): Object {
            var ret: {[k: string]: string[] | string} = Object.create(null);

            if (typeof str !== 'string') {
              return ret;
            }

            str = str.trim().replace(/^(\?|#|&)/, '');

            if (!str) {
              return ret;
            }

            str.split('&').forEach(function (param) {
              var parts = param.replace(/\+/g, ' ').split('=');
              // Firefox (pre 40) decodes `%3D` to `=`
              // https://github.com/sindresorhus/query-string/pull/37
              var key = parts.shift();
              var val = parts.length > 0 ? parts.join('=') : undefined;

              key = decodeURIComponent(key);

              // missing `=` should be `null`:
              // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters
              val = val === undefined ? null : decodeURIComponent(val);

              var retVal = ret[key];
              if (ret[key] === undefined) {
                ret[key] = val;
              } else if (Array.isArray(retVal)) {
                retVal.push(val);
              } else {
                ret[key] = [<string> ret[key], val];
              }
            });

            return ret;
          }

    // Parses the url and gets the access token if it is in the urls hash
    public static getAccessTokenFromUrl(): string {
        return <string> Utils.parseQueryString(window.location.hash)['access_token'];
      }
    }
```
In `app.component.html` add the following to the top of the template. This adds a link to connect to Dropbox if we're not currently connected, or displays "Conncted to dropbox" if we are connected. I also included an optional "Loading..." text that will display while we retrieve existing books from Dropbox.
```html
    <a href="{{authUrl}}" [hidden]="isAuthenticated">Connect to Dropbox</a>
    <p [hidden]="!isAuthenticated">Connected to dropbox</p>
    <p [hidden]="!IsLoading">Loading...</p>
```
When the application loads, we use the functions we added to `utils.ts` to pull the auth token out of the query string, if it exists. If it does exist, we assume we're connected to Dropbox. To do this, import utils into `app.component.ts` by adding this line to the top of the file. `import { Utils } from './utils';`. Directly under the import statements, add `declare var Dropbox: any;` to use the global Dropbox object from the Dropbox Javascript SDK.  

Then add the following local variables to `app.component.ts`.
```typescript
    CLIENT_ID: string = // App key from Dropbox;
    FILE_NAME: string = "/BookList.txt"; // Or whatever you want the file to be named where the data is stored
    authUrl: string;
    dropboxToken: string
    isAuthenticated: boolean
    static isLoading: boolean;
```
This is what the new constructor looks like. We initialize our new variables and use the Dropbox API to setup the authentication url. Then we try to pull an existing auth token out of the url to see if we're currently authenticated or not.
```typescript
    constructor() {
        this.isAuthenticated = false;
        this.newBook = new BookEntity;
        AppComponent.books = [];
        AppComponent.isLoading = false;

        var dbx = new Dropbox({ clientId: this.CLIENT_ID });
        this.authUrl = dbx.getAuthenticationUrl('http://localhost:4200/auth');

        this.dropboxToken = Utils.getAccessTokenFromUrl();
        this.isAuthenticated = this.dropboxToken !== undefined;
      }
```
The next step is to preload the book list if there is an auth token available when the application loads. We do this in the `ngOnInit()` function. We store the books data as JSON in the text file in Dropbox. This uses the Dropbox API to download the file and then parse the JSON contents into the `books` array. There is a slight delay while this happens so I use the `AppComponent.isLoading` flag as a simple way to notify the user that the books are loading. I made it static because it needs to be accessed from within the inner scope of the file reader callback and the outer scope of the component, similar to the `AppComponent.books` property. They also need to be available to the template so there are getters to make them available as instance variables. Below is the full code for the `ngOnInit` function and the `AppComponent.isLoading()` getter function.
```typescript
     ngOnInit() {
        if (this.dropboxToken !== undefined) {
        var dbx = new Dropbox({ accessToken: this.dropboxToken });
        AppComponent.isLoading = true;
        dbx.filesDownload({path:  this.FILE_NAME}).then(function(response) {
        let reader = new FileReader();
        let blob: Blob = response.fileBlob;
    					reader.addEventListener("loadend", function(e) {
                AppComponent.books = JSON.parse(<string>reader.result);
                AppComponent.isLoading = false;
              });
             reader.readAsText(blob);
        })
        .catch(function(error: any) {
          AppComponent.isLoading = false;
        });
      }
     }

      get IsLoading():boolean {
        return AppComponent.isLoading;
      }
```
The final step is to add a function to convert the `AppComponent.books` list into JSON text, add it to the file and send the file to Dropbox. When a new book is added or removed, I create the entire JSON string for the whole updated book list and add it to the file. The function for that is below.
```typescript
    private saveToDropbox() {
        var dbx = new Dropbox({ accessToken: this.dropboxToken });
        dbx.filesUpload({contents:JSON.stringify(AppComponent.books), path: this.FILE_NAME, mode: {".tag": 'overwrite'}, autorename: false, mute: true }).then(function(response) {
        }).catch(function(error) {
          // If it errors because of a dropbox problem, reload the page so the user can re-connect to dropbox	
          alert("Failed to save to dropbox");
          console.log(JSON.stringify(error));
          window.location.href = '/';
        });
      }
```
The only thing left is to call this function at the end of `addBook()` and `removeBook()` to save the new book list to Dropbox.  

### Conclusion

If you made it this far, you should have an Angular application that persists it's data to Dropbox. Congratulations! Over this journey, we created an Angular application to add and remove books from a list and persist that list of books to Dropbox. We configured our app to connect to Dropbox using OAuth authentication. We manage the auth token in a completely client-side way and use the Dropbox API to read and write a file to Dropbox. The complete example is also [available on GitHub.](https://github.com/cwl157/angular-dropbox)