Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / CSS

User credential onboarding with KnockoutJS

4.64/5 (7 votes)
7 Aug 2013CPOL5 min read 23K   293  
A cool way to batch upload new user credentials to a web-app using KnockoutJS

Introduction

I was reading an article recently on ways to add value to a business and one of the key things 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 to provide a simple onboarding validation mechanism, hopefully it can assist someone in a similar situation.

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

Image 1

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

Image 2

Background

The objective of the project is to allow a user to upload a CSV file, parse it 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 plaintext password, but used a hash (the sample data is different however for illustration purposes).

JavaScript
function userModel() {
    this.UserName = ko.observable();
    this.Password = ko.observable();
    this.FirstName = ko.observable();
    this.LastName = ko.observable();
    this.Email = ko.observable();

The next step was to define the ViewModel array, and link the binding:

JavaScript
// Define ViewModel which is an array of user details model
var userVM = {
    userModel: ko.observableArray([])
    }
};   

// Bind the VM to the UI
ko.applyBindings(userVM); 

Then adding the KO data-bind markup to the HTML

HTML
<table border="1">
    <thead>
    <tr>
        <th class="Pad">Username</th>
        <th class="Pad">Password</th>
        <th class="Pad">First name</th>
        <th class="Pad">Last name</th>
        <th class="Pad">Email address</th>
        <th class="Pad">  </th>
    </tr>
    </thead>
    <tbody data-bind="foreach: userModel">
    <tr data-bind="css: ValidCredentials">
        <td class="Pad"><input data-bind="value: UserName" class="Standard"/></td>
        <td class="Pad"><input data-bind="value: Password" class="Standard" /></td>
        <td class="Pad"><input data-bind="value: FirstName" class="Standard"/></td>
        <td class="Pad"><input data-bind="value: LastName"  class="Standard"/></td>
        <td class="Pad"><input data-bind="value: Email" class="Wide"/> <br /></td>
        <td class="Pad"></td>
    </tr>
    </tbody>
</table>

At the top of the file I added a FILE input control and an anchor link.

HTML
<form>
    <input type="file" id="UserFile" />
    <a href="#" id="lnkUpload">Upload CSV</a> 
</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.

JavaScript
$('#lnkUpload').click(function () {
    var FileToRead = document.getElementById('UserFile');
    if (FileToRead.files.length > 0) {
        var reader = new FileReader();
        // assign function to the OnLoad event of the FileReader
        // non synchronous event, therefore assign to other method
        reader.onload = Load_CSVData;
        // call the reader to capture the file
        reader.readAsText(FileToRead.files.item(0));
    }
}); 

There as one gotcha that had me head scratching for a while, I tried to use a jQuery selector to get the var FileToRead - 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.

JavaScript
function Load_CSVData(e) {
    userVM.userModel.removeAll();
    CSVLines = e.target.result.split(/\r\n|\n/);
    $.each(CSVLines, function (i, item) {

        var element = item.split(","); // builds an array from comma delimited items
        var LUserName = (element[0] == undefined) ? "" : element[0].trim();
        var LPassword = (element[1] == undefined) ? "" : element[1].trim();
        var LFirstName = (element[2] == undefined) ? "" : element[2].trim();
        var LLastName = (element[3] == undefined) ? "" : element[3].trim();
        var LEmailAddress = (element[4] == undefined) ? "" : element[4].trim();

        userVM.userModel.push(new userModel()
            .UserName(LUserName)
            .Password(LPassword)
            .FirstName(LFirstName)
            .LastName(LLastName)
            .Email(LEmailAddress)

        )
    }); 
} 

That all works fine, the data is showing perfectly...

Image 3

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.

CSS
.Red {
border: thin dotted #FF6600;
    background-color: red; 
}
XML
<tr data-bind="css: ValidCredentials">
    <td class="Pad"><input...

ValidCredentials is a computed function:

JavaScript
this.ValidCredentials = ko.computed(function () {
    var ValidCreds = (this.UserName() != "" && 
         this.Password() != "" && this.Email() != "");
    return !ValidCreds ? "Red" : "Green";
}, 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).

HTML
<td class="Pad"><input data-bind="value: UserName, css: USR_Required" class="Standard"/></td>
<td class="Pad"><input data-bind="value: Password, css: PWD_Required" class="Standard" /></td>
<td class="Pad"><input data-bind="value: FirstName" class="Standard"/></td>
<td class="Pad"><input data-bind="value: LastName"  class="Standard"/></td>
<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!)

JavaScript
this.USR_Required = ko.computed(function () {
    var rslt = (this.UserName() != "")
    return rslt == true ? "NR" : "Required";
}, this);
this.PWD_Required = ko.computed(function () {
    var rslt = (this.Password() != "")
    return rslt == true ? "NR" : "Required";
}, this);
this.EML_Required = ko.computed(function () {
    var rslt = (this.Email() != "")
    return rslt == true ? "NR" : "Required";
}, this);

and added corresponding CSS into my KnockoutStyles.css file:

CSS
.Required {
    background-color: #FFFF00;
}
 
.NR {
    background-color: #FFFFFF;
}

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:

JavaScript
this.InValidEmail = ko.computed(function () {
        if (this.Email() == "") // dont test if no value present
        {
            return false;
        }
        var rslt = !validateEmail(this.Email());
        return rslt;
    }, this); 

function validateEmail(email) { 
    var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)
      *)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.
      [0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
    return re.test(email);
}

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.

HTML
<a href="#" data-bind="click: $parent.removeUser"><span class="White">Remove user</span></a>
JavaScript
function RemoveUserFromList(data) {
        userVM.userModel.remove(data);
}  
 var userVM = {
    userModel: ko.observableArray([]),
    removeUser: function (data) {
            RemoveUserFromList(data)
    }
}; 

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.

(PS: if you liked reading this please let me know by rating the article at the top of the page!)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)