Note, this articles is part of a series (here). In the previous installment we started implementing "must have" functions. Our focus is to release a working (demonstration) version of our application, as soon as possible. In this installment we will continue that process.

Currently, our application allows a user to create a basic "resume" document. The user can add, delete and update, rows and columns. The most pressing issue is, the application is not peristing the user's work. That is what we are going to focus on in this installment, "persisting data locally".

Persisting data locally means, we want the user to be able to save their work on their local machine. The user can save their work while offline. Our application's storage architecture is similar to a traditional "desktop" application.

When we started designing this project, we said up front the intended audience consists of web developers. Thus, our intended audience should be familiar with the following concepts. The user must know how to navigate the file system, understand file names, file types.

HTML5 specifies several mechanism for a local persistence. The mechanism referred to as "local storage" is implemented the most across web browsers. I'll summarize "local storage" as an embedded database. An embedded database that is dedicated to a particular web browser. What that means to a user is, data saved while using Firefox is not accessible while using Chromium (or any other browser, vice versa, etc). So local storage has some limitations.

HTML5 also introduces a file system api. That means we can engineer a dialog that allows the user to choose a file location to save their work. When the user saves their work to a local file, that information is accessible from any HTML5 web browser.

In addition to those 2 options (local storage and external file), we have a user experience (UX) issue to consider. When the user saves there work as an external file, they have to choose a location and a file name. If a user saves there work to the web browser's "local storage", the user does not have to choose a file location. Thus, the users performs far less steps when saving their work "local storage" (versus external file). So we are going to give the user both options. We'll suggest that they save minor changes to their work in the web browser's local storage. When the user completes a major revision of their work, we recommend they save to an external file.

As mentioned above, out application's storage is similar to a "desktop" application. The analogy would be working with a spreadsheet program like MS Excel or LibreOffice. The spreadsheet programs by default, saves the user's work in a internal format. If the user wants to share their work with other folks or other applications, they export the data as a comma delimited file (csv).

JavaScript's internal data format is JavaScript Object Notation (JSON). So our "row" entity is represented in JSON as such:

{"row_id":1, "location":6}

Our rowList entity is represented as (I.E. JSON array):

[{"row_id":1,"location":1}, {"row_id":5,"location":2}]

We can also represent our rowList as a JSON object as such:

{"rowList": [{"row_id":1,"location":1}, {"row_id":5,"location":2}]}

So we can persist our grid by storing 3 JSON arrays:

rows
columns
row_columns

The same JSON data can be stored in either the web browser's embedded database (AKA local storage) or an external file (E.G. my_resume.json). The file type "json" is optional, but recommended. Later in the project the application will also allow the user to publish an html representation of their work. If you get used to assigning descriptive file types, you'll have a much easier time organizing your data.

So let's outline our new menu items.

Let's look at some of the implementation. Here is how our griid stores the lists (rows, columns and row_columns):

this.store = function() {
        try {
            localStorage.setItem("rows",JSON.stringify(this.rowList.list));
            localStorage.setItem("columns",JSON.stringify(this.columnList.list));
            localStorage.setItem("row_columns",JSON.stringify(this.list));
        } catch(error) {
            console.log("store() error : " + error);
        }
};

There is another caveat to mention regarding the web browser's local storage. The web browsers can set size limits per application. Thus, the above method could trigger the catch condition if a size threshold has been reached. For our scenario, that's not likely, but still possible. My resume is pretty long and I haven't reached the size threshold. Either way, worth noting.

Now the method to load the data from local storage in to our application (grid):

this.load = function(target_el) {
        try {
            this.rowList.length =0;
            this.columnList.length=0;
            this.list.length =0;

            this.rowList.importList(localStorage.getItem("rows"));
            this.columnList.importList(localStorage.getItem("columns"));
            this.importLocalList(localStorage.getItem("row_columns"));
            if (target_el) {
                target_el.appendChild(this.render());
            }

        } catch(error) {
            console.log("load() error "+ error );
        }
};

The reference to "target_el" is our effort to sequence our processing. As mentioned before, JavaScript programs by default do not process statements sequentially. The programmer is required to perform extra steps to insure sequence. Here, we want to repaint our document after the data has been loaded from the local storage. The localStorage.getItem calls are synchronous. That means each statement executes in the sequence as written. It also means that the 4th statement block. which renders our document, runs after the data is loaded.

If we have separated the local storage statements in one function and the call to render in another function, our the JavaScript engine would not run our statements in sequence we want. The method to load from local storage would be invoke asynchronously along with a call to render. In that scenario, we have no control over the sequence. Not obvious at all.

Another reason why I always string test everything. The unexpected sequence is identified when you include string testing in your regular programming routine.

Here is our detailed implementation which loads the row_column array maintained by our grid entity:

 this.importLocalList= function(import_list) {
        var json_list = JSON.parse(import_list);
        if (json_list && (json_list instanceof Array || json_list instanceof Object)) {
            var len = json_list.length;
            var dataRow = null;
            for (var nIndex = 0; nIndex < len; nIndex ++) {
                dataRow = json_list[nIndex];
                this.addRowColumn(new row_column(dataRow));
            }
        }
 };

What is with the old fashioned "for loop" ?
I am a polyglot. I program in many languages, many who's syntax are based on "C". For example, "Java", "C", "JavaScript" all have a similar syntax. It is not unusual for me to literally copy my Java or C code in to a JavaScript listing, then make the appropriate language adjustments. If you've implemented logic that works, why not use it as a basis of migrating to another environment. To summarize, the for look works pretty much the same in "C", "Java" and "JavaScript". So for me, there is an advantage, since I code in all 3 languages.

Now to the export file data implementation.

First we prepare our data, we represent it in JSON format:

 this.toJson = function() {
        var data = {
            "rows":JSON.stringify(this.rowList.list),
            "columns":JSON.stringify(this.columnList.list),
            "row_columns":JSON.stringify(this.list)
        };
        return data;
    };

Now we need to create a file and a link to the file. That means the browser will display a native file dialog to the user. That allows the user to select the location. We'll also include an input element inside our link. That way our user can edit the file name.

We use additional more HTML5 features. A "blob" allows us to construct a in memory representation of a file (including mime type). Once we have our "blob" constructed we need to publish a link. We use the HTML5 window.URL.createObjectURL() method:

  this.toBlob= function() {
        if (! window.URL) {
            // handle opera and safari
            showErrorMessage("Sorry this function is not supported by your web browser.");
            return;
        }
        var mime_type = "application/json";
        var data = JSON.stringify(this.toJson());
        var bb = new Blob([data], {type: mime_type});
        var a = document.createElement('a');
        a.download = container.querySelector('input[type="text"]').value;
        a.download = 'resume.json';
        a.href = window.URL.createObjectURL(bb);
        a.textContent = 'Download ready';
        a.dataset.downloadurl = [mime_type, a.download, a.href].join(':');
        return a;
    };

All of our updated code is committed to out git hub repository here: https://github.com/lorinpa/resume-publisher/tree/dev-1.0

I also changed the onreadystatechange handler in our root html document. Now that we've enabled the local storage, if the user re-loads, we now automatically repaint the page with the data stored in the local database.

About the Author:
Lorin M Klugman is an experienced developer focused on new technology.
Open for work (contract or hire).
Drop Lorin a note. Click here for address.
Please no recruiters :)
- Home