KnockoutJS - Upload CSV

Introduction

This is the third part of series on KnockoutJS. Previous two part are here: 
I was reading an article, recently, on ways to add value to a business and one of the key things, it talked about, was automation. The concept is simple; if you find yourself doing something over and over, find a way to automate it and you save ongoing time, thus, adding value to the business. Bringing batches of new users into a system is one of those things that kill a DevOp's time. So, I decided to play with KnockoutJS to see if it could help. Turns out it can!
 
The code is presented and can be downloaded as a C# MVC project, but can easily be stripped out and used independently. There are enough tutorials out on the Interwebs to explain how to use Knockout - go, Google up the details. This article is focused on how I used Knockout for providing a simple onboarding validation mechanism. Hopefully, it assists someone in a similar situation.

The screenshot below shows the finished project; with red lines, the major issues that need fixing. I also included some regex code to run a basic validation of email addresses.



This is the example CSV file. Note, that not all the data we require is there, and that is the problem.

 
Background

The objective of the project is to allow a user to upload a CSV file, parse its client-side (before going to the server), and show the user what data needs to be corrected before they can upload the file for proper integration.

The workflow is as follows:

  1. User selects a CSV file (sample provided).
  2. User clicks a button (weehoo...).
  3. Client-side code takes the CSV, parses it, and loads it into KnockoutJS array.
  4. KnockoutJS observables do their magic of displaying green/go, red/stop to the user.
Using the code

To keep things clean, I left the JavaScript and CSS in their own separate files. The main three files we will be working with are the HTML (index.cshtml), JavaScript (ViewScript.js), and CSS (KnockoutStyles.css).

The first thing to set-up in KO is the model. In this case, I am capturing basic user information. You will note, I have a field for password - in the production version. I didn't bring in the plain text password, but used a hash (the sample data is different, however, for illustration purposes).

  1. function userModel() {  
  2.     this.UserName = ko.observable();  
  3.     this.Password = ko.observable();  
  4.     this.FirstName = ko.observable();  
  5.     this.LastName = ko.observable();  
  6.     this.Email = ko.observable();  
The next step is to define the ViewModel array, and link the binding.
  1. // Define ViewModel which is an array of user details model  
  2. var userVM = {  
  3.     userModel: ko.observableArray([])  
  4.     }  
  5. };     
  6.   
  7. // Bind the VM to the UI  
  8. ko.applyBindings(userVM);  
Then, adding the KO data-bind markup to the HTML, 
  1. <table border="1">  
  2.     <thead>  
  3.     <tr>  
  4.         <th class="Pad">Username</th>  
  5.         <th class="Pad">Password</th>  
  6.         <th class="Pad">First name</th>  
  7.         <th class="Pad">Last name</th>  
  8.         <th class="Pad">Email address</th>  
  9.         <th class="Pad">  </th>  
  10.     </tr>  
  11.     </thead>  
  12.     <tbody data-bind="foreach: userModel">  
  13.     <tr data-bind="css: ValidCredentials">  
  14.         <td class="Pad"><input data-bind="value: UserName" class="Standard"/></td>  
  15.         <td class="Pad"><input data-bind="value: Password" class="Standard" /></td>  
  16.         <td class="Pad"><input data-bind="value: FirstName" class="Standard"/></td>  
  17.         <td class="Pad"><input data-bind="value: LastName"  class="Standard"/></td>  
  18.         <td class="Pad"><input data-bind="value: Email" class="Wide"/> <br /></td>  
  19.         <td class="Pad"></td>  
  20.     </tr>  
  21.     </tbody>  
  22. </table>  
 At the top of the file, I added a FILE input control and an anchor link.
  1. <form>  
  2.     <input type="file" id="UserFile" />  
  3.     <a href="#" id="lnkUpload">Upload CSV</a>   
  4. </form>  
The next step was putting some code in the Click event of the anchor, to take (client side) a CSV file the user selected, and parse it into a KO array. 
  1. $('#lnkUpload').click(function () {  
  2.     var FileToRead = document.getElementById('UserFile');  
  3.     if (FileToRead.files.length > 0) {  
  4.         var reader = new FileReader();  
  5.         // assign function to the OnLoad event of the FileReader  
  6.         // non synchronous event, therefore assign to other method  
  7.         reader.onload = Load_CSVData;  
  8.         // call the reader to capture the file  
  9.         reader.readAsText(FileToRead.files.item(0));  
  10.     }  
  11. });   

There, as one gotcha that had me head scratching for a while, I tried to use a jQuery selector to get the varFileToRead - this didn't work, however; so, I fell back to raw JavaScript and everything flowed smoothly again.

The FileReader "OnLoad" event that reads the file contents is not synchronous. So, I assigned it to another function (Load_CSVData) and then called the readAsText event which feeds back into that function.

Now, something to note here. The FileReader will only work if you are running from a WebServer (or within the IDE using the integrated web server etc.) - it does *not* work if you are simply running form txt files on the desktop.

The logic of the Load_CSVData method is, as follows:

  • Clear any items from the existing KO observable array.
  • Load up a local array (CSVLines) with the text data passed in from the FileReader, separating out each line using the "Split" function (delimiter = new line marker).
  • For each line being loaded, split this further (delimiter = the comma).
  • For each item in each CSV line, push the value to as a new model on the VM array.

I broke the code below, out a bit, to make it easier to read. Note that if there is no value in the CSV line, I added a blank string to save problems later.

  1. function Load_CSVData(e) {  
  2.     userVM.userModel.removeAll();  
  3.     CSVLines = e.target.result.split(/\r\n|\n/);  
  4.     $.each(CSVLines, function (i, item) {  
  5.   
  6.         var element = item.split(","); // builds an array from comma delimited items  
  7.         var LUserName = (element[0] == undefined) ? "" : element[0].trim();  
  8.         var LPassword = (element[1] == undefined) ? "" : element[1].trim();  
  9.         var LFirstName = (element[2] == undefined) ? "" : element[2].trim();  
  10.         var LLastName = (element[3] == undefined) ? "" : element[3].trim();  
  11.         var LEmailAddress = (element[4] == undefined) ? "" : element[4].trim();  
  12.   
  13.         userVM.userModel.push(new userModel()  
  14.             .UserName(LUserName)  
  15.             .Password(LPassword)  
  16.             .FirstName(LFirstName)  
  17.             .LastName(LLastName)  
  18.             .Email(LEmailAddress)  
  19.   
  20.         )  
  21.     });   
  22. }  
That all works fine. The data is showing perfectly.
 


The next step is to use the power of the observable pattern to adjust the UI as data is received and changed...

There are a number of improvements to be made:

  1. Highlight rows that have a problem in red.
  2. Make it obvious when data is required (we will give the input a yellow color).
  3. Show rows that are OK or almost OK, in green.
  4. Show a message if the email address provided is not valid.

On each table row, I added in a CSS data-bind that called a computed function. This function checked if the required fields (Username, Password, Email) had values and if not, set the CSS background color for the row to Red.

  1. .Red {  
  2. borderthin dotted #FF6600;  
  3.     background-colorred;   
  4. }  
  1. <tr data-bind="css: ValidCredentials">  
  2.     <td class="Pad"><input...  
ValidCredentials is a computed function, 
  1. this.ValidCredentials = ko.computed(function () {  
  2.     var ValidCreds = (this.UserName() != "" &&   
  3.          this.Password() != "" && this.Email() != "");  
  4.     return !ValidCreds ? "Red" : "Green";  
  5. }, this);  
This worked fine. So, I extended the logic a bit further, so that not only would the problem rows appear in red, but the input fields that needed extra data would appear in yellow. There were three fields, so I created a separate definition for each (CSS: USR_Required, PWD_Required, EML_Required). 
  1. <td class="Pad"><input data-bind="value: UserName, css: USR_Required" class="Standard"/></td>  
  2. <td class="Pad"><input data-bind="value: Password, css: PWD_Required" class="Standard" /></td>  
  3. <td class="Pad"><input data-bind="value: FirstName" class="Standard"/></td>  
  4. <td class="Pad"><input data-bind="value: LastName"  class="Standard"/></td>  
  5. <td class="Pad"><input data-bind="value: Email, css: EML_Required"  class="Wide"/>  
I created three almost identical methods to compute (worth revisiting to refactor!), 
  1. this.USR_Required = ko.computed(function () {  
  2.     var rslt = (this.UserName() != "")  
  3.     return rslt == true ? "NR" : "Required";  
  4. }, this);  
  5. this.PWD_Required = ko.computed(function () {  
  6.     var rslt = (this.Password() != "")  
  7.     return rslt == true ? "NR" : "Required";  
  8. }, this);  
  9. this.EML_Required = ko.computed(function () {  
  10.     var rslt = (this.Email() != "")  
  11.     return rslt == true ? "NR" : "Required";  
  12. }, this);  
and added corresponding CSS into my KnockoutStyles.css file, 
  1. .Required {  
  2.     background-color#FFFF00;  
  3. }  
  4.    
  5. .NR {  
  6.     background-color#FFFFFF;  
  7. }  
So far so good. The next step was to add code that runs a simple check on the email address. I borrowed some code from stack for this.
  1. this.InValidEmail = ko.computed(function () {  
  2.         if (this.Email() == ""// dont test if no value present  
  3.         {  
  4.             return false;  
  5.         }  
  6.         var rslt = !validateEmail(this.Email());  
  7.         return rslt;  
  8.     }, this);   
  9.   
  10. function validateEmail(email) {   
  11.     var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)  
  12.       *)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.  
  13.       [0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;  
  14.     return re.test(email);  
  15. }  

Almost there. The final thing was to give the user a means of removing a complete line of data if it was not relevant (think about all of those accounts in Active Directory of people who have left an organization over the years....).

To do this, I added a "Click" data-bind to the end of each row that called a removeUser method and added code to the ViewModel to drop the item from the KO array.

  1. <a href="#" data-bind="click: $parent.removeUser"><span class="White">Remove user</span></a>  
  1. function RemoveUserFromList(data) {  
  2.         userVM.userModel.remove(data);  
  3. }    
  4.  var userVM = {  
  5.     userModel: ko.observableArray([]),  
  6.     removeUser: function (data) {  
  7.             RemoveUserFromList(data)  
  8.     }  
  9. };  

That's it.

There is enough there to give a head-start to anyone looking for a similar solution. Possible improvements would be a button that is only enabled when data is corrected, another button to convert the data to JSON and send to the server, etc.
 
So, there we go, another installment in the KnockoutJS series... Until next time, Happy Front-end Coding! 


Similar Articles