Now Available React Programming: The Big Nerd Ranch Guide
Front-End ReactBased on our React Essentials course, this book uses hands-on examples to guide you step by step through building a starter app and a complete,...
We all know that the larger a project gets, the harder it is to maintain and organize UI dependencies. “What if the user does this, or this? That needs to change that and that.” This can be tough to deal with, especially when a different developer comes onto the project and doesn’t quite understand how you manually set up these dependencies.
Among the plethora of different Javascript libraries that help UI developers create responsive UIs, Knockout.JS has been a pleasure to work with. It knows what it needs to do to help you but also when to get out of the developer’s way.
Knockout aims to help with this by providing the developer with a way to “make elegant dependency tracking, declarative bindings, and be trivially extensible.” Knockout uses the MVVM (model-view-viewmodel) pattern, so if you’re familiar with any other MV* pattern, it should be easy to pick up on. The pattern allows developers to keep the logic in the Javascript and the HTML5 view is left solely to render the logic. What I’ve enjoyed the most about using Knockout.JS is that it makes the web page reactive to the user, like a native desktop app, easily.
The heart and soul of Knockout are what’s called “observables.” The best description comes from Knockout’s documentation: “Observables are special JavaScript objects that can notify subscribers about changes, and can automatically detect dependencies.” What this means is that if, for example, a user changes his/her age on the UI, this change will automatically update that property on the ViewModel and automatically update the View and whatever dependencies may be on Age, without having to save or post back to the server. Let’s look at the ViewModel code:
function courseViewModel() {
this.studentsName = ko.observable('');
this.studentsAge = ko.observable();
};
ko.applyBindings(new courseViewModel());
The ko.applyBindings(new courseViewModel());
kicks everything off and binds your ViewModel to the View. Let’s bind the student’s age to the View.
<div data-bind='text: studentsAge'></div>
As you can assume, this binds the value of student’s age to the text value of the div element. To change this value live, add an input element: <input data-bind="value: studentsAge" />
Since this is an observable, it will notify all subscribers that there has been a change in value of studentsAge. Right now, the only subscriber is the div element that we created before and its text will be updated whenever you change the value of the input box.
Knockout also handles collections gracefully, allowing you to add, remove and manipulate lists on the fly. Say Big Nerd Ranch instructors want to be able to add notes to an individual student, maybe describing a certain course concept he/she needs to reiterate to that student the next day. We can do that easily:
//JavaScript
function courseViewModel() {
this.studentsName = ko.observable('');
this.studentsAge = ko.observable();
this.notes = ko.observableArray([
{ dateTime: new Date(), text: ""}
]);
};
ko.applyBindings(new courseViewModel());
//HTML
<ul data-bind="foreach: notes">
<li>
<div data-bind="text: dateTime"></div> : <div data-bind="text: text"></div>
</li>
<ul>
The data-bind="foreach: notes"
loops through the notes array and renders list items for each item including the date/time it was added and its text.
Knockout also gives you the ability to create templates that can be used to render HTML for a single element or for each element in an array. Templates can help organize your code and can be pretty powerful. Let’s refactor the HTML above:
<h5>Notes:</h5>
//Where the template is actually rendered
<div data-bind="template: { name: 'notes-template', foreach: notes }"></div>
//What is rendered
<script type="text/html" id="notes-template">
<div class="well well-small">
<h5 data-bind="text: dateTime"></h5>
<div data-bind="text: text"></div>
</div>
</script>
The template script tag requires type="text/html"
so that the browser does not mistake it as JavaScript. Knockout also supports the Underscore template engine and the jquery.tmpl engine, each with its own syntax. Check out Knockout’s documentation for more information.
There are lots of different bindings to make your UI responsive to what data is being sent to the UI. If there are no notes for an individual student, you can bind to the “visible” property of an element such as the “Notes:” heading title like so: <h5 data-bind="visible: notes().length > 0">Notes:</h5>
This will hide the header if there are no notes. We will talk more about some of Knockout’s bindings later on.
Right now, we have no way of adding notes to the collection, so let’s remedy that. Add the following to your HTML View:
<form data-bind="submit: addNote">
<div class="form-group">
<label>Add Note:</label>
<textarea data-bind="value: noteToAdd" class="form-control"></textarea>
<br />
<button class="btn btn-primary" type="submit">Add</button>
</div>
</form>
We’re telling the View to monitor a “submit” event, triggered when the user clicks the submit button, and run the “addNote” procedure when it detects that event has occured. The ViewModel will grab the text value from the user’s input (noteToAdd) and push that to our notes array. This can be achieved easily by adding the following to our ViewModel:
this.noteToAdd = ko.observable();
this.addNote = function () {
var note = this.noteToAdd().trim();
if (note) {
this.notes.push({ dateTime: new Date(), text: note });
this.noteToAdd("");
}
};
The if (note) {
is a simple validation, checking to see if the user had typed anything into the textbox. If so, it adds that note to the array. Knockout makes doing more advanced validation and reporting results to the user a fairly trivial task for developers. We will go over validation later on.
Now on to computed observables, the last observable type in your toolbox.
Computed Observables come in handy when tracking the dependencies on one or more observables. Whenever one of the dependent observables changes, it re-evaluates the computed observable’s value. The usual example is a “full name” situation, where a user changes the value of their first name or last name and the full name property combines the two values.
We’re going to do something different. Let’s make a feature that allows instructors to track how many assignments or challenges the student has completed and show a percentage of overall completion. First, create a “Challenges” model that sets up the structure for creating challenge objects. This is a best practice for fully utilizing the power of the MVVM (model-view-viewmodel) pattern. It separates your code into logical parts and makes handling and binding JSON data easier later on.
var Challenge = function (challengeText, challengeCompleted) {
this.text = ko.observable(challengeText);
this.completed = ko.observable(challengeCompleted);
}
And then add “challenges” to your ViewModel constructor: function courseViewModel(challenges) {
This will allow us to insert and parse a custom array or JSON data into the ViewModel.
Add the following to your ViewModel:
this.challenges = ko.observableArray(
ko.utils.arrayMap(challenges, function(challenge) {
return new Challenge(challenge.challengeText, challenge.completed);
})
);
What this is doing is mapping data from the array that we inserted through the ViewModel’s constructor to an observableArray of “Challenge” objects (with their respective observable properties). Knockout comes with numerous helper functions like the ko.utils.arrayMap
that make situations like these easier. You can always still use jQuery or UnderscoreJS to do the the same stuff, it’s up to you.
Add some default data to your ViewModel like so:
var challenges = [
{"challengeText": "Chapter 1 Challenge", "completed": false},
{"challengeText": "Chapter 2 Challenge", "completed": false},
{"challengeText": "Chapter 3 Challenge", "completed": false},
{"challengeText": "Chapter 4 Challenge", "completed": false},
{"challengeText": "Chapter 5 Challenge", "completed": false},
{"challengeText": "Chapter 6 Challenge", "completed": false}
];
var viewModel = new courseViewModel(challenges);
ko.applyBindings(viewModel);
This data would normally come from an Ajax request, but for demo purposes, let’s hardcode it for now. Now on to the computed observable:
//JavaScript -- Add to ViewModel
this.challengeCompletedCount = ko.computed(function () {
return ((ko.utils.arrayFilter(this.challenges(), function (challenge) {
return challenge.completed();
}).length / this.challenges().length) * 100).toFixed(2) + ' %';
}.bind(this));
While this is technically only dependent on one observable, it is computing two different inputs: the number of completed assignments and the number of assignments, and computing that into one output.
Finally, bind it to the HTML view:
//HTML
<h5 data-bind="visible: challenges().length > 0">Challenges:</h5>
<div data-bind="template: { name: 'challenges-template', foreach: challenges }"></div>
<script type="text/html" id="challenges-template">
<div class="checkbox">
<label>
<input type="checkbox" data-bind="checked: completed"> <div data-bind="text: text"></div>
</label>
</div>
</script>
<h5>Completed: <span data-bind="text: challengeCompletedCount"></span></h5>
We’re done! If you have spent any time with front-end development, you will appreciate how simple Knockout makes handling dependencies, with little regard to how or where your data is coming from.
I hope you learned something and enjoy working with KnockoutJS as much as I do. I strongly encourage you to check out the Knockout.JS documentation and go through their live tutorials.
You can see my full code and demo for this tutorial on jsFiddle.
Based on our React Essentials course, this book uses hands-on examples to guide you step by step through building a starter app and a complete,...
Svelte is a great front-end Javascript framework that offers a unique approach to the complexity of front-end systems. It claims to differentiate itself from...
Large organizations with multiple software development departments may find themselves supporting multiple web frameworks across the organization. This can make it challenging to keep...