Creating Interactive Web Interfaces with the help of Knockout.js

jeffcogswell 6 Tallied Votes 566 Views Share

As I spend time in the Daniweb forums, I see some questions that ultimately come down to the issue of manipulating JavaScript data and a user interface on a website. When you build a GUI that lets your user view, add, edit, and delete data, a common approach is to spend a lot of time copying data back and forth between the JavaScript variables and the user interface. Further, this often involves managing the user interface, such as adding new elements as new data is added.

However, often people perform extra work that isn't necessary. In addition to the extra time, such work usually means writing extra JavaScript code, which opens up room for more bugs. It turns out that there are several JavaScript libraries that are well written, heavily tested, and do a fine job of simplifying what I'm describing here.

One such library that I personally like is called Knockout, which you can find at knockoutjs.com. Other libraries work well, too; I just find this one to be more suited to my tastes. In this tutorial I'll show you how to get up and running with it.

A basic model

First let's build the basic data structure. I prefer to start there, with the data modeling. For this example we'll create a simple array that holds information about US presidents. Each item in the array will have a first name, a last name, and a number representing the order of the president, with George Washington being 1:

{
  firstName: 'George',
  lastName: 'Washington',
  number: 1
};

We'll create a holding object like so:

var MyData = {
  presidents: []
};

The idea here is that if this were a production app, we would later have more things to put in our model besides presidents, even though in this tutorial we won't actually have additional data.

Now let's build a web page to try it out. If we weren't using a JavaScript framework, we might write JavaScript code that loops through our array, and creates a string of HTML consisting of a TR element and three TD elements inside it. Then we might add that to an existing table. Later, if we want to add additional data, we would add the data to the array, and then create another TR and add it to the table. Throughout that process we're doing two things: Manipulating the array and separately manipulating the elements on the screen.

But instead we'll use knockout to see how easily we can have elements automatically created and changed as we modify our data. And we can go the other way too; if we have input fields on the screen, when we enter data into those fields, the data can automatically change. This greatly simplifies things for us.

To get started we'll use a CDN that hosts the external libraries we need, jQuery and knockout. (Life has gotten much easier in the last couple of years with cloud hosted libraries that have fast retrieval and don't require us to download them and install them on our server.) One CDN that I recently found by chance is called CloudFlare CDN. It has received decent reviews and seems safe to use. (I'm not affiliated with them.) In the code below you can see where I added three script lines accessing three libraries from CloudFlare, one for jQuery and two for knockout.

Here's the whole file, which I called ko.html. (This is just the first version, which only displays the data. We'll add onto it as we go along.)

<html>
<head>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js"></script>
</head>
<script>

var MyData = {
  presidents: []
};

var datum1 = {
  firstName: 'George',
  lastName: 'Washington',
  number: 1
};

var datum2 = {
  firstName: 'Thomas',
  lastName: 'Jefferson',
  number: 3
};

var datum3 = {
  firstName: 'Abe',
  lastName: 'Lincoln',
  number: 1
};

MyData.presidents.push(datum1);
MyData.presidents.push(datum2);
MyData.presidents.push(datum3);

</script>
<body>

<table>
<thead>
  <tr><th>First Name</th><th>Last Name</th><th>Number</th></tr>
</thead>
<tbody data-bind="foreach: presidents">
  <tr>
    <td data-bind="html: firstName"></td>
    <td data-bind="html: lastName"></td>
    <td data-bind="html: number"></td>
  </tr>
</tbody>
</table>

<script>
ko.applyBindings(MyData);
</script>
</body>
</html>

Notice the script section towards the top where I create the data model. I then fill it with three president structures. (And I intentionally gave Abe Lincoln the incorrect number of 1; we'll fix it after we add on the ability to modify the data through the GUI.)

The data structure is just several simple JavaScript objects stored in an array. Now look at the table element in the body section. The first part is just a simple THEAD element. But the body is where we make use of knockout.js. Knockout.js uses a custom HTML attribute called data-bind. (The HTML spec allows us to create custom attributes that start with data-. jQuery makes it easy to access these things through the data() method, although we're not using that here.) The TBODY tag has this data-bind:

data-bind="foreach: presidents"

This simply says we're going to loop through the items in the presidents member of the root object. And what do we do for each item? We will create a table row and fill it with TD elements that contain the first name, last name, and number. Here's how we do it:

<td data-bind="html: firstName"></td>

This says that we'll fill the TD with HTML taken from the firstName member of the current object.

You can see how we're creating three TD elements, filling each with named members from the current object: firstName, lastName, and number. The outer element, tbody, loops through the objects in the JavaScript array, and knockout will create the inner stuff, the TR and TDs , for each element in the array. Here's what the page looks like when we load it:

1a22ecc620ec31fd89ccdb35a3fcc0e6

But in order for all this to work, there's one key line of code that we put at the end of our file, after the elements:

ko.applyBindings(MyData);

(We could also put this inside a jQuery document load handler.) This calls into knockout, and tells it what data to use when going through the data-bind information. Here, then, are the steps it follows: Knockout loads up with MyData for the root object. Our TBODY element accesses the presidents member of MyData, and executes a loop on each item in the presidents array. For each item, knockout creates a table row consisting of the members of the item – first name, last name, and number. Knockout does the hard part of actually generating the elements for us. Think of what we provided as a template. Then when we open the page, knockout runs and creates the table for us from the existing data. Very nice! And very little coding is needed. In fact, our code consisted of filling the JavaScript data structures, and calling applyBindings. Knockout did the rest for us.

Modifying the Data

That last example just created a table based on the existing JavaScript data, which isn't too terribly sophisticated. But one nice thing about Knockout is that it provides a mechanism whereby if you modify the data, the screen elements will change automatically for us. That makes life easier, because after we have our template worked out, if we modify the data, we don't need to do anything at all to the user interface. All we have to do is modify our data. That is, we can focus on our data model and not worry about changing the user interface to accommodate the new data. However, to make this work, we have to first make some adjustments to our code. Knockout has a way of converting our data to a special type of member that the knockout library can subsequently “observe,” or keep an eye on for changes, updating the user interface when that data changes. This works by transforming our data members such as firstName into functions that we can call to set or retrieve the data. Let's try it out. This example will be a bit cumbersome at first, but in the next example we'll see how knockout provides a simpler way of doing this.

To convert our data, we make each member an instance of something called an observable. An observable can hold an object or a simple type such as a string or number. Here's the modified code. (Remember, this got a bit messy, but in a moment we'll undo these changes and see a simpler way to do this.)

<html>
<head>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js"></script>
</head>
<script>

var MyData = {
  presidents: []
};

var datum1 = {
  firstName: ko.observable('George'),
  lastName: ko.observable('Washington'),
  number: ko.observable(1)
};

var datum2 = {
  firstName: ko.observable('Thomas'),
  lastName: ko.observable('Jefferson'),
  number: ko.observable(3)
};

var datum3 = {
  firstName: ko.observable('Abe'),
  lastName: ko.observable('Lincoln'),
  number: ko.observable(1)
};

MyData.presidents.push(datum1);
MyData.presidents.push(datum2);
MyData.presidents.push(datum3);

</script>
<body>

<table>
<thead>
  <tr><th>First Name</th><th>Last Name</th><th>Number</th></tr>
</thead>
<tbody data-bind="foreach: presidents">
  <tr><td data-bind="html: firstName"></td>
  <td data-bind="html: lastName"></td>
  <td data-bind="html: number"></td></tr>
</tbody>
</table>

<script>
ko.applyBindings(MyData);
</script>
</body>
</html>

We modified the president data like this:

firstName: ko.observable('George'),

where it was previously just a simple assignment:

firstName: 'George'

Try it out. Save this as ko2.html. Open it up in Chrome or whatever browser you prefer for development work. Then open up Chrome's JavaScript developer tools by pressing F12. In the console, let's modify the data manually. First, let's fix Lincoln's number to 16. Type this into the JavaScript console and press Enter:

MyData.presidents[2].number()

This will retrieve the current value for the number in president index #2, which is the third in the list. Notice we called it as a function, since we're using an observable now. We'll see the number 1 appear. It should be 16, since Lincoln was the 16th president. Type the following into the console to change the data:

MyData.presidents[2].number(16)

When you type this in, notice that the UI immediately changes. The line showing Lincoln immediately changed to 16, again without us having to touch the user interface. And if we retrieve the data value again, we'll see it shows 16:

MyData.presidents[2].number()

In other words, we only changed the data. We didn't have to also manually change the UI. We didn't have to use jQuery to search for the table and then the row holding Lincoln and then the TD holding the number, and we didn't have to then change the data inside that TD to match the data model. The UI updated automatically with no coding on our part other than code that changed the data. Let's adjust this code a bit to see how we can do this without using the JavaScript console. We'll make a button labeled “Fix” that will change the item for us. First, add a button after the closing </table> tag:

<button>Fix</button>

Then find the script section at the bottom of ko2.html, and change it to look like this:

<script>
ko.applyBindings(MyData);
$('button').on('click', function() {
    MyData.presidents[2].number(16);
});
</script>

We're using jQuery to attach a handler to the button. The code is the same as what we typed into the console. Now reload the page. Look at the row for Lincoln, which says 1. Click the button, and the number will change to 16. It worked!

Let's take it to the next step. Not only do we want to change the data in an existing president, but let's include a way to add another president. We have a list of presidents, so adding another element to the list is easy. However, to make that work, we have to make a quick change. Find the line for the array of presidents in the main data model:

var MyData = {
  presidents: []
};

And let's change that into an observable as well. But this will be a special type that holds an array called an observable array. Change it to look like this, noting that the word Array is added after the word observable:

var MyData = {
  presidents: ko.observableArray([])
};

That's all it takes. Now we can add elements to the array and the interface will update automatically. Save this as ko3.html, and open it in the browser. Then from the JavaScript console, type the following:

MyData.presidents.push({firstName:'John', lastName:'Adams', number: 2})

This code starts with the presidents array, and then calls push to insert a new object into the array. When you press enter, you'll see the new item automatically added to the list, again without us having to manually change the UI. Again, we just changed the data. The UI updated automatically.

Let's build a final set of code that uses this approach without needing to open up the JavaScript console. We'll put three text input boxes on our page where we can enter the data for a new president. Then we'll include an add button that reads those text boxes, creates a data structure, and inserts that new data structure into the presidents list. But do we need to read those text boxes? Why not use knockout for those text boxes as well? So to do that we'll add another member to our MyData structure.

Now I need to mention something here. At this point we're starting to violate some of the good principles we learned in our computer science courses. I'm about to create a separate data structure that's just to help manage the user interface, but isn't the data that we would store in the database. So to prevent a mess, let's divide up our main data model into two parts: The main data, and the helper data. We'll put our presidents list in the main data, and the data that interacts with the user entry fields in the helper data. Then if we were to store this back to the server, we would only store the main data, not the helper data.

<html>
<head>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js"></script>
</head>
<script>

var MyData = {
  main: {
    presidents: ko.observableArray([])
  },
  helpers: {
    newitem: {
      firstName: ko.observable(),
      lastName: ko.observable(),
      number: ko.observable()
    }
  }
};

var datum1 = {
  firstName: 'George',
  lastName: 'Washington',
  number: 1
};

var datum2 = {
  firstName: 'Thomas',
  lastName: 'Jefferson',
  number: 3
};

var datum3 = {
  firstName: 'Abe',
  lastName: 'Lincoln',
  number: 16
};

MyData.main.presidents.push(ko.mapping.fromJS(datum1));
MyData.main.presidents.push(ko.mapping.fromJS(datum2));
MyData.main.presidents.push(ko.mapping.fromJS(datum3));

</script>
<body>

<table data-bind="with:main">
<thead>
  <tr><th>First Name</th><th>Last Name</th><th>Number</th></tr>
</thead>
<tbody data-bind="foreach: presidents">
  <tr>
    <td data-bind="html: firstName"></td>
    <td data-bind="html: lastName"></td>
    <td data-bind="html: number"></td>
  </tr>
</tbody>
</table>

<br />
<div data-bind="with:helpers.newitem">
    First: <input type="text" data-bind="value:firstName" /><br />
    Last: <input type="text" data-bind="value:lastName" /><br />
    Number: <input type="text" data-bind="value:number" />
</div>
<button>Add</button>

<script>
ko.applyBindings(MyData);
$('button').on('click', function() {
    MyData.main.presidents.push(ko.mapping.fromJS({
        firstName: MyData.helpers.newitem.firstName(),
        lastName: MyData.helpers.newitem.lastName(),
        number: MyData.helpers.newitem.number()
    }));
});

</script>
</body>
</html>

Notice how I divided MyData into two sections. But that means to access the presidents array, we have to qualify it as MyData.main.presidents, whereas previously it was just MyData.presidents. That means either modifying the foreach like so: foreach: main.presidents or using a slightly different technique, a knockout “with” statement, which is what I did. Look at the opening table tag. I added this data bind to it:

data-bind="with:main"

That means inside the table, knockout will operate within the context of the main data member. So when we access the presidents list here:

<tbody data-bind="foreach: presidents">

we will actually be accessing main.presidents.

Now look at the div section I added after the table. Again, I used a with, but this time I want to get right to the newitem member. So I used with: helpers.newitem. Also, look at the input boxes. To attach them to the data, I used a knockout command called value. This attches the data to the value attribute of the input elements. Since I'm using an observable, these will work both ways: if I change, for example, the MyData.helpers.newitem.firstName through code, the UI will immediately update with the textbox to show that value. You can try that out in the JavaScript console by typing this:

MyData.helpers.newitem.firstName('Dani')

Then you'll see the text box next to FirstName immediately get filed in with Dani. Again, that means to make changes to the UI, such as filling in a textbox, we don't need to drill down with jQuery and find the text box. We just change it by changing the data in our model. And the cool thing is that if we attach multiple elements on the screen to that data item, all the items will update. Very cool.

Similarly, to read the data, we just call the function without passing a parameter, like so:

MyData.helpers.newitem.firstName()

That means that when we want to add a new president, we don't have to go through and manually read the values of the inputs. We just grab them from the data, again allowing us to not touch the user interface.

But now for one tricky part. After the user enters in new information and clicks the Add button, we want to create a new president object. We don't want to just take the MyData.helpers.newitem object and add it directly. That's the data that gets filled in automatically. But if we add that object itself directly to the array, then we'll have multiple copies of that same object, which would be a mess. (As an exercise, I encourage you to think about the implications there, and maybe even try it out to see what happens.) Instead we want to get a full copy of the object, and add that copy to the array.

I have a couple tricks I use for copying JavaScript objects. One is to stringify using JSON2, and then parse the string. This works well if we have complex objects with lots of members that in turn are objects with members. It saves a lot of code, and it goes like this:

second = JSON.parse(JSON.stringify(first))

where first is the original object we're copying. That would partly work here, except for a slight problem: Our objects are filled with observables. The ko.mapping provides a handy function for retrieving a JavaScript object from one that has observables. In the code above, you can see I just did the copying manually. I accessed each member by calling the observable function to get the member's data, and stored it back into my new object:

firstName: MyData.helpers.newitem.firstName()

That works fine for small data structures like we have here. But a more elegant way is to make use of another function in knockout called fromJS. (Technically, toJS, which we used earlier, and fromJS are part of a separate library, the knockout mapping library. We included that library as our third script line from the CloudFlare CDN.)

Let's make this final change to our code and we'll be finished. Replace the button's handler with what I have in this code below, and we have our final app, (And yes, “app”. We're not just building web pages here; we're building interactive web applications!)

<html>
<head>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js"></script>
</head>
<script>

var MyData = {
  main: {
    presidents: ko.observableArray([])
  },
  helpers: {
    newitem: {
      firstName: ko.observable(),
      lastName: ko.observable(),
      number: ko.observable()
    }
  }
};

var datum1 = {
  firstName: 'George',
  lastName: 'Washington',
  number: 1
};

var datum2 = {
  firstName: 'Thomas',
  lastName: 'Jefferson',
  number: 3
};

var datum3 = {
  firstName: 'Abe',
  lastName: 'Lincoln',
  number: 16
};

MyData.main.presidents.push(ko.mapping.fromJS(datum1));
MyData.main.presidents.push(ko.mapping.fromJS(datum2));
MyData.main.presidents.push(ko.mapping.fromJS(datum3));

</script>
<body>

<table data-bind="with:main">
<thead>
  <tr><th>First Name</th><th>Last Name</th><th>Number</th></tr>
</thead>
<tbody data-bind="foreach: presidents">
  <tr>
    <td data-bind="html: firstName"></td>
    <td data-bind="html: lastName"></td>
    <td data-bind="html: number"></td>
  </tr>
</tbody>
</table>

<br />
<div data-bind="with:helpers.newitem">
    First: <input type="text" data-bind="value:firstName" /><br />
    Last: <input type="text" data-bind="value:lastName" /><br />
    Number: <input type="text" data-bind="value:number" />
</div>
<button>Add</button>

<script>
ko.applyBindings(MyData);
$('button').on('click', function() {
    MyData.main.presidents.push(
      ko.mapping.fromJS(ko.mapping.toJS(MyData.helpers.newitem))
    );
});

</script>
</body>
</html>

Conclusion

This was pretty cool, but there's more we can do. In the next tutorial I'll take it up yet one more notch and show how two more features:

  • When the user clicks on a row, the row becomes editable with text boxes. Then the user can save the changes.
  • The user can delete rows

You'll see that it's pretty easy to do. We'll make some minor adjustments to our HTML in the table, but very little. Knockout will handle the hard parts, letting us concentrate on our data structures and JavaScript code. Then we'll finally look at what we would need to do to save this data back to the server. See you then!

Member Avatar for iamthwee
iamthwee

Hmmm, I'm not convinced. Looks more intrusive to me.

To me I like to validate javascript using parsely.js, and if I have something very specific send it to a php file, do the validation then return using ajax/jquery.

Can you further elaborate why these frameworks are better? Why are people adopting MVC js frameworks? It just seems to be more learning for little benefit. We already have a huge array of PHP/.net/python/ruby frameworks to choose server side as it is.

Looking forward to your reply.

jeffcogswell 175 Light Poster Featured Poster

Well I'm not necessarily saying it's better. But I've found it has greatly sped up my development time. I'm creating data-bound user interfaces much more quickly than I used to for the web, more like in my days as a desktop software developer. And for me personally, the non-obtrusive JavaScript thing really isn't an issue. When desktop development started offering data-bound controls, life got much easier for many of us. And that's what I see here. I really don't have a problem if there's an intrusive or tightly-coupled aspect between the UI and the data structures. I'm not building my UIs to be reusable anyway. But that's just my own opinion. Others might differ. (And thanks for replying! I wasn't sure anyone was going to. :-) )

jeffcogswell 175 Light Poster Featured Poster

Also, here's another thought about why I like this library. I'm building a pretty big app right now that runs in the browser, with about ten thousand lines of JavaScript. Throughout the program, I have many dialog boxes. By using this library, I can bind every dialog box to a JavaScript object. When the user fills in a dialog box and clicks OK, I don't need to go through and gather up the values for all the text boxes and drop downs. The data is automatically in my objects, ready for me to process. And similarly, if the user needs to edit existing data, I don't need to write code that prepopulates the form fields. I just push the data into an observable with one line of code, and the fields get filled in automatically with no work from my end. Then when the user is finished editing the data, again I don't need to do anything. My data structure is automatically filled in for me. This has saved me a huge amount of coding.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.