This article written by Carlo Russo, author of the book KnockoutJS Blueprints, describes that RequireJS gives us a simplified format to require many parameters and to avoid parameter mismatch using the CommonJS require format; for example, another way (use this or the other one) to write the previous code is:
define(function(require) {
var $ = require("jquery"),
ko = require("knockout"),
viewModel = {};
$(function() {
ko.applyBindings(viewModel);
});
});
(For more resources related to this topic, see here.)
In this way, we skip the dependencies definition, and RequireJS will add all the texts require('xxx') found in the function to the dependency list.
The second way is better because it is cleaner and you cannot mismatch dependency names with named function arguments.
For example, imagine you have a long list of dependencies; you add one or remove one, and you miss removing the relative function parameter. You now have a hard-to-find bug.
And, in case you think that r.js optimizer behaves differently, I just want to assure you that it's not so; you can use both ways without any concern regarding optimization.
Just to remind you, you cannot use this form if you want to load scripts dynamically or by depending on variable value; for example, this code will not work:
var mod = require(someCondition ? "a" : "b");
if (someCondition) {
var a = require('a');
} else {
var a = require('a1');
}
You can learn more about this compatibility problem at this URL: http://www.requirejs.org/docs/whyamd.html#commonjscompat.
You can see more about this sugar syntax at this URL: http://www.requirejs.org/docs/whyamd.html#sugar.
Now that you know the basic way to use RequireJS, let's look at the next concept.
The component binding handler is one of the new features introduced in Version 2.3 of KnockoutJS.
Inside the documentation of KnockoutJS, we find the following explanation:
Components are a powerful, clean way of organizing your UI code into self-contained, reusable chunks. They can represent individual controls/widgets, or entire sections of your application.
A component is a combination of HTML and JavaScript. The main idea behind their inclusion was to create full-featured, reusable components, with one or more points of extensibility.
A component is a combination of HTML and JavaScript.
There are cases where you can use just one of them, but normally you'll use both.
You can get a first simple example about this here: http://knockoutjs.com/documentation/component-binding.html.
The best way to create self-contained components is with the use of an AMD module loader, such as RequireJS; put the View Model and the template of the component inside two different files, and then you can use it from your code really easily.
Writing a custom module of KnockoutJS with RequireJS is a 4-step process:
We are going to build bases for the Search Form component, just to move forward with our project; anyway, this is the starting code we should use for each component that we write from scratch.
Let's cover all of these steps.
We start with the View Model of this component. Create a new empty file with the name BookingOnline/app/components/search.js and put this code inside it:
define(function(require) {
var ko = require("knockout"),
template = require("text!./search.html");
function Search() {}
return {
viewModel: Search,
template: template
};
});
Here, we are creating a constructor called Search that we will fill later.
We are also using the text plugin for RequireJS to get the template search.html from the current folder, into the argument template.
Then, we will return an object with the constructor and the template, using the format needed from KnockoutJS to use as a component.
In the View Model we required a View called search.html in the same folder. At the moment, we don't have any code to put inside the template of the View, because there is no boilerplate code needed; but we must create the file, otherwise RequireJS will break with an error.
Create a new file called BookingOnline/app/components/search.html with the following content:
<div>Hello Search</div>
When you use components, there are two different ways to give KnockoutJS a way to find your component:
The first way is the easiest one: using the default component loader of KnockoutJS.
To use it with our component you should just put the following row inside the BookingOnline/app/index.js file, just before the row $(function () {:
ko.components.register("search", {require: "components/search"});
Here, we are registering a module called search, and we are telling KnockoutJS that it will have to find all the information it needs using an AMD require for the path components/search (so it will load the file BookingOnline/app/components/search.js).
You can find more information and a really good example about a custom component loader at: http://knockoutjs.com/documentation/component-loaders.html#example-1-a-component-loader-that-sets-up-naming-conventions.
Now, we can simply use the new component inside our View; put the following code inside our Index View (BookingOnline/index.html), before the script tag:
<div data-bind="component: 'search'"></div>
Here, we are using the component binding handler to use the component; another commonly used way is with custom elements.
We can replace the previous row with the following one:
<search></search>
KnockoutJS will use our search component, but with a WebComponent-like code.
If you want to support IE6-8 you should register the WebComponents you are going to use before the HTML parser can find them. Normally, this job is done inside the ko.components.register function call, but, if you are putting your script tag at the end of body as we have done until now, your WebComponent will be discarded.
Follow the guidelines mentioned here when you want to support IE6-8: http://knockoutjs.com/documentation/component-custom-elements.html#note-custom-elements-and-internet-explorer-6-to-8
Now, you can open your web application and you should see the text,
Hello Search.
We put that markup only to check whether everything was working here, so you can remove it now.
Now that we know how to create a component, and we put the base of our Search Form component, we can try to look for the requirements for this component.
A designer will review the View later, so we need to keep it simple to avoid the need for multiple changes later.
From our analysis, we find that our competitors use these components:
This is a wireframe of what we should build (we got inspired by Trivago):
We could do everything by ourselves, but the easiest way to realize this component is with the help of a few external plugins; we are already using jQuery, so the most obvious idea is to use jQuery UI to get the Autocomplete Widget, the Date Picker Widget, and maybe even the Button Widget.
Let's start downloading the current version of jQuery UI (1.11.1); the best thing about this version is that it is one of the first versions that supports AMD natively.
After reading the documentation of jQuery UI for the AMD (URL: http://learn.jquery.com/jquery-ui/environments/amd/) you may think that you can get the AMD version using the download link from the home page. However, if you try that you will get just a package with only the concatenated source; for this reason, if you want the AMD source file, you will have to go directly to GitHub or use Bower.
Download the package from https://github.com/jquery/jquery-ui/archive/1.11.1.zip and extract it.
Every time you use an external library, remember to check the compatibility support. In jQuery UI 1.11.1, as you can see in the release notes, they removed the support for IE7; so we must decide whether we want to support IE6 and 7 by adding specific workarounds inside our code, or we want to remove the support for those two browsers.
For our project, we need to put the following folders into these destinations:
jquery-ui-1.11.1/ui -> BookingOnline/app/ui
jquery-ui-1.11.1/theme/base -> BookingOnline/css/ui
We are going to apply the widget by JavaScript, so the only remaining step to integrate jQuery UI is the insertion of the style sheet inside our application.
We do this by adding the following rows to the top of our custom style sheet file (BookingOnline/css/styles.css):
@import url("ui/core.css");
@import url("ui/menu.css");
@import url("ui/autocomplete.css");
@import url("ui/button.css");
@import url("ui/datepicker.css");
@import url("ui/theme.css")
Now, we are ready to add the widgets to our web application.
You can find more information about jQuery UI and AMD at: http://learn.jquery.com/jquery-ui/environments/amd/
We want to give to the user a really nice user experience, but as the first step we can use the wireframe we put before to create a skeleton of the Search Form.
Replace the entire content with a form inside the file BookingOnline/components/search.html:
<form data-bind="submit: execute"></form>
Then, we add the blocks inside the form, step by step, to realize the entire wireframe:
<div>
<input type="text" placeholder="Enter a destination" />
<label> Check In: <input type="text" /> </label>
<label> Check Out: <input type="text" /> </label>
<input type="submit" data-bind="enable: isValid" />
</div>
Here, we built the first row of the wireframe; we will bind data to each field later.
We bound the execute function to the submit event (submit: execute), and a validity check to the button (enable: isValid); for now we will create them empty.
Update the View Model (search.js) by adding this code inside the constructor:
this.isValid = ko.computed(function() {
return true;
}, this);
And add this function to the Search prototype:
Search.prototype.execute = function() { };
This is because the validity of the form will depend on the status of the destination field and of the check-in date and check-out date; we will update later, in the
next paragraphs.
Now, we can continue with the wireframe, with the second block. Here, we should have a field to select the number of rooms, and a block for each room.
Add the following markup inside the form, after the previous one, for the second row to the View (search.html):
<div>
<fieldset>
<legend>Rooms</legend>
<label>
Number of Room
<select data-bind="options: rangeOfRooms,
value: numberOfRooms">
</select>
</label>
<!-- ko foreach: rooms -->
<fieldset>
<legend>
Room <span data-bind="text: roomNumber"></span>
</legend>
</fieldset>
<!-- /ko -->
</fieldset>
</div>
In this markup we are asking the user to choose between the values found inside the array rangeOfRooms, to save the selection inside a property called numberOfRooms, and to show a frame for each room of the array rooms with the room number, roomNumber.
When developing and we want to check the status of the system, the easiest way to do it is with a simple item inside a View bound to the JSON of a View Model.
Put the following code inside the View (search.html):
<pre data-bind="text: ko.toJSON($data, null, 2)"></pre>
With this code, you can check the status of the system with any change directly in the printed JSON.
You can find more information about ko.toJSON at http://knockoutjs.com/documentation/json-data.html
Update the View Model (search.js) by adding this code inside the constructor:
this.rooms = ko.observableArray([]);
this.numberOfRooms = ko.computed({
read: function() {
return this.rooms().length;
},
write: function(value) {
var previousValue = this.rooms().length;
if (value > previousValue) {
for (var i = previousValue; i < value; i++) {
this.rooms.push(new Room(i + 1));
}
} else {
this.rooms().splice(value);
this.rooms.valueHasMutated();
}
},
owner: this
});
Here, we are creating the array of rooms, and a property to update the array properly. If the new value is bigger than the previous value it adds to the array the missing item using the constructor Room; otherwise, it removes the exceeding items from the array.
To get this code working we have to create a module, Room, and we have to require it here; update the require block in this way:
var ko = require("knockout"),
template = require("text!./search.html"),
Room = require("room");
Also, add this property to the Search prototype:
Search.prototype.rangeOfRooms = ko.utils.range(1, 10);
Here, we are asking KnockoutJS for an array with the values from the given range.
ko.utils.range is a useful method to get an array of integers. Internally, it simply makes an array from the first parameter to the second one; but if you use it inside a computed field and the parameters are observable, it re-evaluates and updates the returning array.
Now, we have to create the View Model of the Room module. Create a new file BookingOnline/app/room.js with the following starting code:
define(function(require) {
var ko = require("knockout");
function Room(roomNumber) {
this.roomNumber = roomNumber;
}
return Room;
});
Now, our web application should appear like so:
As you can see, we now have a fieldset for each room, so we can work on the template of the single room.
Here, you can also see in action the previous tip about the pre field with the JSON data.
With KnockoutJS 3.2 it is harder to decide when it's better to use a normal template or a component.
The rule of thumb is to identify the degree of encapsulation you want to manage:
- Use the component when you want a self-enclosed black box, or the template if you want to manage the View Model directly.
What we want to show for each room is:
We can update the Room View Model (room.js) by adding this code into the constructor:
this.numberOfAdults = ko.observable(2);
this.ageOfChildren = ko.observableArray([]);
this.numberOfChildren = ko.computed({
read: function() {
return this.ageOfChildren().length;
},
write: function(value) {
var previousValue = this.ageOfChildren().length;
if (value > previousValue) {
for (var i = previousValue; i < value; i++) {
this.ageOfChildren.push(ko.observable(0));
}
} else {
this.ageOfChildren().splice(value); this.ageOfChildren.valueHasMutated();
}
},
owner: this
});
this.hasChildren = ko.computed(function() {
return this.numberOfChildren() > 0;
}, this);
We used the same logic we have used before for the mapping between the count of the room and the count property, to have an array of age of children.
We also created a hasChildren property to know whether we have to show the box for the age of children inside the View.
We have to add—as we have done before for the Search View Model—a few properties to the Room prototype:
Room.prototype.rangeOfAdults = ko.utils.range(1, 10);
Room.prototype.rangeOfChildren = ko.utils.range(0, 10);
Room.prototype.rangeOfAge = ko.utils.range(0, 17);
These are the ranges we show inside the relative select.
Now, as the last step, we have to put the template for the room in search.html; add this code inside the fieldset tag, after the legend tag (as you can see here, with the external markup):
<fieldset>
<legend>
Room <span data-bind="text: roomNumber"></span>
</legend>
<label> Number of adults
<select data-bind="options: rangeOfAdults,
value: numberOfAdults"></select>
</label>
<label> Number of children
<select data-bind="options: rangeOfChildren,
value: numberOfChildren"></select>
</label>
<fieldset data-bind="visible: hasChildren">
<legend>Age of children</legend>
<!-- ko foreach: ageOfChildren -->
<select data-bind="options: $parent.rangeOfAge,
value: $rawData"></select>
<!-- /ko -->
</fieldset>
</fieldset>
<!-- /ko -->
Here, we are using the properties we have just defined.
We are using rangeOfAge from $parent because inside foreach we changed context, and the property, rangeOfAge, is inside the Room context.
Why did I use $rawData to bind the value of the age of the children instead of $data?
The reason is that ageOfChildren is an array of observables without any container. If you use $data, KnockoutJS will unwrap the observable, making it one-way bound; but if you use $rawData, you will skip the unwrapping and get the two-way data binding we need here. In fact, if we use the one-way data binding our model won't get updated at all.
If you really don't like that the fieldset for children goes to the next row when it appears, you can change the fieldset by adding a class, like this:
<fieldset class="inline" data-bind="visible: hasChildren">
Now, your application should appear as follows:
Now that we have a really nice starting form, we can update the three main fields to use the jQuery UI Widgets.
As soon as we start to write the code for this field we face the first problem: how can we get the data from the backend?
Our team told us that we don't have to care about the backend, so we speak to the backend team to know how to get the data.
After ten minutes we get three files with the code for all the calls to the backend; all we have to do is to download these files (we already got them with the Starting Package, to avoid another download), and use the function getDestinationByTerm inside the module, services/rest.
Before writing the code for the field let's think about which behavior we want for it:
The documentation of KnockoutJS also explains how to create custom binding handlers in the context of RequireJS.
All the bindings we use inside our View are based on the KnockoutJS default binding handler.
The idea behind a binding handler is that you should put all the code to manage the DOM inside a component different from the View Model. Other than this, the binding handler should be realized with reusability in mind, so it's always better not to hard-code application logic inside.
The KnockoutJS documentation about standard binding is already really good, and you can find many explanations about its inner working in the Appendix, Binding Handler.
When you make a custom binding handler it is important to remember that: it is your job to clean after; you should register event handling inside the init function; and you should use the update function to update the DOM depending on the change of the observables.
This is the standard boilerplate code when you use RequireJS:
define(function(require) {
var ko = require("knockout"),
$ = require("jquery");
ko.bindingHandlers.customBindingHandler = {
init: function(element, valueAccessor,
allBindingsAccessor, data, context) {
/* Code for the initialization… */
ko.utils.domNodeDisposal.addDisposeCallback(element,
function () { /* Cleaning code … */ });
},
update: function (element, valueAccessor) {
/* Code for the update of the DOM… */
}
};
});
And inside the View Model module you should require this module, as follows:
require('binding-handlers/customBindingHandler');
ko.utils.domNodeDisposal is a list of callbacks to be executed when the element is removed from the DOM; it's necessary because it's where you have to put the code to destroy the widgets, or remove the event handlers.
So, now we can write our binding handler.
We will define a binding handler named autocomplete, which takes the observable to put the found value.
We will also define two custom bindings, without any logic, to work as placeholders for the parameters we will send to the main binding handler.
Our binding handler should:
We also should ensure that if the observable gets cleared, the input field gets cleared too.
So, this is the code of the binding handler to put inside BookingOnline/app/binding-handlers/autocomplete.js (I put comments between the code to make it easier to understand):
define(function(require) {
var ko = require("knockout"),
$ = require("jquery"),
autocomplete = require("ui/autocomplete");
ko.bindingHandlers.autoComplete = {
init: function(element, valueAccessor, allBindingsAccessor, data, context) {
Here, we are giving the name autoComplete to the new binding handler, and we are also loading the Autocomplete Widget of jQuery UI:
var value = ko.utils.unwrapObservable(valueAccessor()),
allBindings = ko.utils.unwrapObservable(allBindingsAccessor()),
options = allBindings.autoCompleteOptions || {},
events = allBindings.autoCompleteEvents || {},
$element = $(element);
Then, we take the data from the binding for the main parameter, and for the optional binding handler; we also put the current element into a jQuery container:
autocomplete(options, $element);
if (options._renderItem) {
var widget = $element.autocomplete("instance");
widget._renderItem = options._renderItem;
}
for (var event in events) {
ko.utils.registerEventHandler(element, event, events[event]);
}
Now we can apply the Autocomplete Widget to the field.
If you are questioning why we used ko.utils.registerEventHandler here, the answer is: to show you this function. If you look at the source, you can see that under the wood it uses $.bind if jQuery is registered; so in our case we could simply use $.bind or $.on without any problem.
But I wanted to show you this function because sometimes you use KnockoutJS without jQuery, and you can use it to support event handling of every supported browser.
The source code of the function _renderItem is (looking at the file ui/autocomplete.js):
_renderItem: function( ul, item ) {
return $( "<li>" ).text( item.label ).appendTo( ul );
},
As you can see, for security reasons, it uses the function text to avoid any possible code injection.
It is important that you know that you should do data validation each time you get data from an external source and put it in the page.
In this case, the source of data is already secured (because we manage it), so we override the normal behavior, to also show the HTML tag for the bold part of
the text.
In the last three rows we put a cycle to check for events and we register them.
The standard way to register for events is with the event binding handler. The only reason you should use a custom helper is to give to the developer of the View a way to register events more than once.
Then, we add to the init function the disposal code:
// handle disposal
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
$element.autocomplete("destroy");
});
Here, we use the destroy function of the widget.
It's really important to clean up after the use of any jQuery UI Widget or you'll create a really bad memory leak; it's not a big problem with simple applications, but it will be a really big problem if you realize an SPA.
Now, we can add the update function:
},
update: function(element, valueAccessor) {
var value = valueAccessor(),
$element = $(element),
data = value();
if (!data)
$element.val("");
}
};
});
Here, we read the value of the observable, and clean the field if the observable is empty.
The update function is executed as a computed observable, so we must be sure that we subscribe to the observables required inside. So, pay attention if you put conditional code before the subscription, because your update function could be not called anymore.
Now that the binding is ready, we should require it inside our form; update the
View search.html by modifying the following row:
<input type="text" placeholder="Enter a destination" />
Into this:
<input type="text" placeholder="Enter a destination"
data-bind="autoComplete: destination,
autoCompleteEvents: destination.events,
autoCompleteOptions: destination.options" />
If you try the application you will not see any error; the reason is that KnockoutJS ignores any data binding not registered inside the ko.bindingHandlers object, and we didn't require the binding handler autocomplete module.
So, the last step to get everything working is the update of the View Model of the component; add these rows at the top of the search.js, with the other require(…) rows:
Room = require("room"),
rest = require("services/rest");
require("binding-handlers/autocomplete");
We need a reference to our new binding handler, and a reference to the rest object to use it as source of data.
Now, we must declare the properties we used inside our data binding; add all these properties to the constructor as shown in the following code:
this.destination = ko.observable();
this.destination.options = {
minLength: 3,
source: rest.getDestinationByTerm,
select: function(event, data) {
this.destination(data.item);
}.bind(this),
_renderItem: function(ul, item) {
return $("<li>").append(item.label).appendTo(ul);
}
};
this.destination.events = {
blur: function(event) {
if (this.destination() && (event.currentTarget.value !==
this.destination().value)) {
this.destination(undefined);
}
}.bind(this)
};
Here, we are defining the container (destination) for the data selected inside the field, an object (destination.options) with any property we want to pass to the Autocomplete Widget (you can check all the documentation at: http://api.jqueryui.com/autocomplete/), and an object (destination.events) with any event we want to apply to the field.
Here, we are clearing the field if the text inside the field and the content of the saved data (inside destination) are different.
Have you noticed .bind(this) in the previous code? You can check by yourself that the value of this inside these functions is the input field.
As you can see, in our code we put references to the destination property of this, so we have to update the context to be the object itself; the easiest way to do this is with a simple call to the bind function.
In this article, we have seen all some functionalities of KnockoutJS (core).
The application we realized was simple enough, but we used it to learn better how to use components and custom binding handlers.
If you think we put too much code for such a small project, try to think what differences you have seen between the first and the second component: the more component and binding handler code you write, the lesser you will have to write in the future.
The most important point about components and custom binding handlers is that you have to realize them looking at future reuse; more good code you write, the better it will be for you later.
The core point of this article was AMD and RequireJS; how to use them inside a KnockoutJS project, and why you should do it.
Further resources on this subject:
e to add—as we have done before for the Search View Model— |