Use Dropbox for Persistent Storage with an Angular Application

cwl157

Carl Layton

Posted on March 6, 2020

Use Dropbox for Persistent Storage with an Angular Application

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 and some familiarity with Angular but you don't need to be an expert. NodeJs and NPM are also required. You must also have a Dropbox account. The free version works. The complete example is available on GitHub.

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

The bottom part of the page displays the list of books with a button to remove a book from the list.

List Books

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 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.

    table, th, td {
        border: 1px solid black;
    }
Enter fullscreen mode Exit fullscreen mode

Next, we're going to create a regular typescript class to hold the Book data named book.entity.ts.

    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;
        }
    }
Enter fullscreen mode Exit fullscreen mode

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.

    <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>
Enter fullscreen mode Exit fullscreen mode

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.

    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 { }
Enter fullscreen mode Exit fullscreen mode

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.

    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);

      }
    }
Enter fullscreen mode Exit fullscreen mode

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. 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

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 on GitHub. It's also included in the GitHub repo for this project. You need to add it to the build scripts in the angular.json file.

    "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
              }
Enter fullscreen mode Exit fullscreen mode

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 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.

    /* 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'];
      }
    }
Enter fullscreen mode Exit fullscreen mode

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.

    <a href="{{authUrl}}" [hidden]="isAuthenticated">Connect to Dropbox</a>
    <p [hidden]="!isAuthenticated">Connected to dropbox</p>
    <p [hidden]="!IsLoading">Loading...</p>
Enter fullscreen mode Exit fullscreen mode

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.

    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;
Enter fullscreen mode Exit fullscreen mode

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.

    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;
      }
Enter fullscreen mode Exit fullscreen mode

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.

     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;
      }
Enter fullscreen mode Exit fullscreen mode

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.

    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 = '/';
        });
      }
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
cwl157
Carl Layton

Posted on March 6, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related