Introduction to HotDrink
Table of Contents
1 Overview of basic concepts
HotDrink is a JavaScript library that assists with GUI implementation. HotDrink allows you to program GUIs declaratively—instead of writing event handlers that control what should happen in a GUI and when, the programmer specifies the relationships between various pieces of data, i.e., how changes to one piece of data affect other pieces of data. From this description, HotDrink decides how and when to enforce relationships, and also implements several GUI behaviors that can be reused with little effort.
Changes to GUI state are managed through commands written by the programmer. HotDrink ensures that (1) commands are initiated only when all relationships defined by the programmer have been enforced, and (2) commands will not interfere with one another, at least with respect to the GUI state. This means that your program will behave correctly even if a new command is issued before the previous command has finished.
1.1 Constraint systems
The heart of the HotDrink library is a constraint system. A constraint system manages variables and constraints among the variables. Each constraint represents something that should be true concerning some of the variables (i.e., a relation over those variables). For example, given three variables, \(x\), \(y\), and \(z\), you could create a constraint that \(x = y + z\), or that \(x \le y \le z\), or even that \(z\) is the result of a database query using \(x\) and \(y\). When the constraint is true, we say it is satisfied. HotDrink enforces a constraint by modifying some of its variables so that the constraint is satisfied.
In HotDrink's constraint system, constraints between variables are expressed as sets of functions. These functions are called methods (short for constraint satisfaction methods) and are written by the programmer. Each method is responsible for calculating new values for some of the constraint's variables so that the constraint will be satisfied. To enforce all constraints, HotDrink executes one method from each constraint. Together, the methods executed define a dataflow—a branching path showing which variables are used to update which other variables. HotDrink has a multi-way dataflow constraint system, meaning there are many possible dataflows depending on which methods are executed. In general, HotDrink selects a dataflow that preserves variables the user has edited more recently.
In a HotDrink “powered” user interface, GUI events are translated to changes to constraint system variables. These changes result in constraints which are no longer satisfied. HotDrink responds by solving the constraint system—that is, selecting which methods to execute, and then executing them so that all constraints are again satisfied. Solving can change the values of the system's variables; the changed values are then somehow displayed or visualized to the user, as specified by the programmer.
1.2 Commands
Whereas the constraint system runs in the background to automatically enforce constraints, a command is code that is run only when triggered by an event. A command may be as simple as assigning a new value to a variable, or as complex as making a server call using the value of some variables and updating the GUI with the result. After executing a command, HotDrink always solves the constraint system to ensure that all relations between variables are enforced.
HotDrink views the life of a GUI, not as a series of events, but as a sequence of commands. HotDrink manages these commands and coordinates their execution. Commands may execute asynchronously, meaning that a command does not have to wait for all previous commands to finish before it can begin. HotDrink manages the effects of all commands on GUI state so that the end result will be the same as if those commands had executed synchronously.
1.3 MVVM
Applications written with HotDrink follow the Model-View-ViewModel (a.k.a. MVVM) design pattern. This pattern divides the application as follows:
- The View is responsible for presentation and capturing interactions with the user. It arranges everything on the screen and provides events that indicate user actions. For our purposes, the view is specified in HTML, along with JavaScript that generates and modifies the HTML. This tutorial shows bits and snippets of HTML, with some commentary to help understand the examples, but in general we assume familiarity with HTML, JavaScript, and constructing web pages.
- The View-Model is responsible for managing the data presented in the view. This means supplying the data to be displayed by the view, as well as responding to user actions. The purpose of HotDrink is to help in implementing the view-model.
- Another important part of an MVVM application are the bindings. A binding connects elements in the view with variables in the view-model. The bindings are sometimes treated as part of the view—that is why there is no “B” in “MVVM”—but we treat them as a distinct component of the pattern. In HotDrink, bindings can be specified in JavaScript, or as annotations in HTML as part of the view specification.
- The Model is responsible for everything else—whatever the application is actually supposed to do. The model does not need to be aware of the user interface; therefore we will not discuss it further in this documentation.
2 Including HotDrink in your web page
To use HotDrink, your web page should include the file hotdrink.min.js
that
came with this tutorial. (Alternatively, if you downloaded the source tree,
follow the build instructions to compile your own hotdrink.min.js
.)
More specifically, add the following <script>
tag to the <head>
section of
your HTML, with the appropriate path in the src
attribute. In order for the
examples in this tutorial to work, the file hotdrink.min.js
must be located
in the same directory as this HTML file.
<script type="text/javascript" src="hotdrink.min.js"></script>
HotDrink does not rely on any other JavaScript frameworks, but neither should it
conflict with other frameworks (JQuery, Dojo, MooTools, etc.).
The only global symbol it exports is hd
, and all of its interactions
with the DOM are via cooperative API functions, e.g.,
addEventListener
, etc.
3 Important HotDrink objects
There are three important object types you will need to interact with when using HotDrink.
- The
PropertyModel
typeA property model is the View-Model of a HotDrink “powered” application. application. It contains variables for holding the data used by your GUI, and a constraint system for enforcing any constraints you define for those variables (as described in Section 1.1.) In general, your application will always have a single property model, customized to contain the variables and constraints needed for your GUI. Variables and constraints are created separately, then added to the property model.
- The
Component
typeA property model may be viewed as a type of container: it contains and manages a set of elements. The three types of property model elements we have discussed so far are variables, constraints, and commands. While it is possible to create these elements individually and add them directly to a property model, the recommended approach is to define a property model component. A component is also a collection of property model elements; however, a component does not manage its elements, it merely holds them.
You, the programmer, are responsible for making sure all the property model elements you are using are added to the property model. By placing related elements together in a component, it is easy to add them all to the property model at the same time—and to remove them later if they are no longer needed. You can also define your own component types so that you can reuse them.
- The
ComponentBuilder
typeThe task of building a component is somewhat complex. To help, HotDrink provides an object called the component builder. This factory object essentially defines its own language (i.e., an embedded domain-specific language) for constructing components. To use this language, create a new component builder instance, then call its member functions to add elements to the component.
4 An introductory example
We present here a simple example of using HotDrink. Note that we will not explain all the details of this example, only the most important points. The goal of this example is to define some basic concepts that you will need to understand the other HotDrink tutorials; the other tutorials will provide a more in-depth explanation of how HotDrink works.
4.1 The example
Below is an form from a hypothetical hotel reservation web page. The user may edit the check-in date, the check-out date, the number of nights to stay, and the number of beds desired. Notice that, as one text box is edited, other elements of the web page are updated to be consistent with the changes.
Check-In: | |
---|---|
Check-Out: | |
Nights: | |
Room Type: | |
Price: | $ |
Source Code
The source code for this form is shown in its entirety below. (Click the "show" links to view the code.) To clarify, we have broken the code into three sections: the HTML that defines the View, the JavaScript that defines the View-Model, and the JavaScript that performs binding. In the remainder of this section we will reproduce and examine snippets of this code.
1: <table> 2: <style type="text/css" scoped> 3: th { padding-right: 1ex; text-align: left; font-weight: bold; } 4: </style> 5: <tr> 6: <th>Check-In:</th> 7: <td><input type="text" data-bind="bd.date( checkin )"/></td> 8: </tr> 9: <tr> 10: <th>Check-Out:</th> 11: <td><input type="text" data-bind="bd.date( checkout )"/></td> 12: </tr> 13: <tr> 14: <th>Nights:</th> 15: <td><input type="text" data-bind="bd.num( nights, 0 )"/></td> 16: </tr> 17: <tr> 18: <th>Room Type:</th> 19: <td> 20: <select data-bind="bd.value( type )"> 21: <option value="1D">1 Double Bed</option> 22: <option value="1Q">1 Queen Bed</option> 23: <option value="1K">1 King Bed</option> 24: <option value="2Q">2 Queen Beds</option> 25: </select> 26: </td> 27: </tr> 28: <tr> 29: <th>Price:</th> 30: <td>$<span data-bind="bd.text( price, bd.fix( 2 ) )"></span></td> 31: </tr> 32: <tr> 33: <td colspan="2" align="right"> 34: <input type="button" value="Submit" data-bind="bd.click( reportPrice )"/> 35: </td> 36: </tr> 37: </table>
1: // Some constants to help us 2: var oneDayMs = 24*60*60*1000; 3: var today = new Date(); 4: today.setHours( 0, 0, 0, 0 ); 5: 6: // Define variables and constraints in a component 7: var model = new hd.ComponentBuilder() 8: 9: // Variables 10: .variable( "checkin", today) 11: .variable( "checkout" ) 12: .variable( "nights", 2 ) 13: .variable( "type", "1Q" ) 14: .variable( "price" ) 15: 16: // First constraint 17: .constraint( 'checkin, checkout, nights' ) 18: .method( 'checkin, checkout -> nights', function( checkin, checkout ) { 19: return (checkout.getTime() - checkin.getTime()) / oneDayMs; 20: } ) 21: .method( 'nights, checkin -> checkout', function( nights, checkin ) { 22: return new Date( checkin.getTime() + nights*oneDayMs ); 23: } ) 24: .method( 'nights, checkout -> checkin', function( nights, checkout ) { 25: return new Date( checkout.getTime() - nights*oneDayMs ); 26: } ) 27: 28: // Second constraint 29: .constraint( 'nights, type, price' ) 30: .method( 'nights, type -> price', function( nights, type ) { 31: var rate; 32: switch(type) { 33: case '1D': rate = 80; break; 34: case '1Q': rate = 95; break; 35: case '1K': rate = 115; break; 36: case '2!': rate = 140; break; 37: default: throw "Invalid room type"; 38: } 39: return nights * rate 40: } ) 41: 42: // Create a custom command named 'reportPrice' 43: .command( 'reportPrice', 'price ->', function( price ) { 44: alert( "The price of your room is $" + price.toFixed( 2 ) ); 45: } ) 46: 47: .component(); 48: 49: // Define the property model 50: var pm = new hd.PropertyModel(); 51: pm.addComponent( model );
1: window.addEventListener( 'load', function() { 2: hd.createDeclaredBindings( model, document.body ); 3: } );
4.2 The View
As mentioned above, the View is defined by the HTML used to construct the web page (and any JavaScript used to dynamically generate web page content). In this example, our View is an HTML table containing input elements and their labels. Each input element is bound to a variable in the view-model; the value of that variable is displayed by the element, and any changes made to the element are propagated to the value of the variable.
In this example we use HotDrink's declarative bindings, in which each tag is
annotated with JavaScript code indicating how it is to be bound. We call this
annotation a binding specification. Binding specifications are found in the
data-bind
attribute of an HTML tag. For example, the following text input
tag contains a binding specification indicating that it is to be bound to the
checkin
variable of the view-model, and that it is to be converted to a
JavaScript Date
object in the view-model. Thus, after each edit of the text
input, the string will be converted to a Date
object, and—if
successfully converted—will be stored in the checkin
variable.
<input type="text" data-bind="bd.date( checkin )"/>
Similarly, this text input tag contains a binding specification indicating
that it is to be bound to the nights
variable of the view-model, and that it
is to be converted to a number in the view-model. The second parameter to the
bd.num
function indicates that the number is to have zero digits after the
decimal point—i.e., it is to be an integer.
<input type="text" data-bind="bd.num( nights, 0 )"/>
The <select>
tag for the drop-down list element indicates that it is to use
the type
variable as the selected value.
<select data-bind="bd.value( type )">
The view contains one other element bound to a variable. The following
<span>
tag contains a binding specification indicating the price
variable
is to be the contents of the tag, and that it is to be interpreted simply as
text—i.e., no HTML tags, etc. The call to bd.fix( 2 )
indicates that the
variable is to be converted from a number to a string containing two digits
after the decimal point.
<span data-bind="bd.text( price, bd.fix( 2 ) )"></span>
These binding specifications have no effect until you tell HotDrink to automatically perform all bindings according to their specifications. We do that in the Binding section.
As a side-node, you may find this view to be very plain. Let us emphasize that the purpose of HotDrink is not to create a fancy View; it is to create a fancy View-Model. HotDrink focuses on the data underlying your web page, enforcing constraints and orchestrating commands. There are many existing JavaScript toolkits which, for example, will help you create calendar widgets to help the user select a date. HotDrink aims to cooperate with those toolkits, not replace them.
4.3 The View-Model
As mentioned above, a property model represents the view-model of our application. Your job as the programmer, then, is to construct a property model that correctly models your GUI.
The first thing we must do is create the variables and constraints that will
define our property model. We create these as members of a component using a
component builder. The general strategy for constructing a component is: (1.)
create a new builder object, (2.) use the builder member functions to create
variables, constraints, etc., and (3.) call the component
builder member
function to retrieve the completed component. The mock code below illustrates
how this might be done.
// Create new builder object var builder = new hd.ComponentBuilder(); // Use methods to construct the model builder.variable( ... ); builder.constraint( ... ); builder.method( ... ); // ...and so on... // Retrieve the completed model var component = builder.component();
However, most builder member functions return the same builder object with which they were invoked. This allows a succinct programming style known as “chaining” in which the return value of one member function is used to immediately invoke the next member function. You can see this style in the mock code below.
// Create, use, and discard builder object var component = new hd.ComponentBuilder() .variable( ... ) .constraint( ... ) .method( ... ) .component();
First we create a new ComponentBuilder
object. Rather than storing this
builder in a variable, we simply begin invoking member functions on it. Each
member function invoked returns the same anonymous builder. This return value
is used to invoke the next member function, and so on. This continues until
the end, where the component
member function is invoked on the still-anonymous
builder to return the component that was constructed. It is this component that
is assigned to the component
variable. This is the style we use in our
view-model code.
In general, you may have as many components as you like. However, for small
interfaces, like the one in this example, we can represent the entire GUI with
a single component. You can name this component whatever you would like, but
our convention is to name it model
, as it represents the entirety of the
property model. Once you have a component, you can add it to a property
model, like so:
var pm = new hd.PropertyModel(); pm.addComponent( model );
When you add a component to a property model, it automatically adds to the property model all the elements contained by the component. Later, you may remove those elements from the property model, like so:
pm.removeComponent( model );
As those function names suggest, the full property model for your application can actually consist of several different components, and new components can be added or removed at run-time. For now we assume a single component defines the entire property model.
4.4 The Binding
As mentioned above, the bindings are the connections between variables of the view-model and elements of the View. We can generate bindings programmatically with JavaScript code. However, in the example we have annotated elements of the View with binding specification, indicating how they are to be bound. We can perform all of these bindings in one fell swoop with the following line of code.
hd.createDeclaredBindings( model, document.body );
The createDeclaredBindings
function causes HotDrink to search a part of the
Document Object Model (DOM) for HTML elements which have a binding
specification. It then performs those bindings according to their
specification. The first parameter is a component used to look up any variable
names encountered in the binding specifications. The second parameter is a
DOM node indicating which part of the DOM should be searched for binding
specifications. HotDrink will only search the specified node and any of its
children when looking for binding specifications. In this case, however,
we've passed document.body
, indicating the entire body of the DOM is to be
searched.
There is a catch to performing binding—one web developers should be very
familiar with. Elements of the DOM are not available until the entire HTML
document has been parsed; thus, attempting to call performDeclaredBindings
in the header of the document will fail because the DOM is not yet ready.
In this example, we handle this by registering a callback function to be
executed when the entire document has finished loading; it is here that we
call performDeclaredBindings
. Many JavaScript toolkits and frameworks
provide alternative means of running code after the DOM is ready; any such
means would be suitable for this purpose.
4.5 Commands
In many ways, a command is similar to an event handler. However, whereas an event handler represents something that happened in the View, a command represents something that happened in the View-Model. For example, a key-press event is generated when the user presses a key. That event is translated by a binding into a variable-set command.
Most commands in this example are, in fact, variable-set commands issued by bindings. These commands are created automatically; no extra work is required. If you want to create your own custom commands, however, you may do so using the component builder.
// Create a custom command named 'reportPrice' .command( 'reportPrice', 'price ->', function( price ) { alert( "The price of your room is $" + price.toFixed( 2 ) ); } )
This command can be bound to events, like button presses. Commands can also be used to modify variables of the property model, or event modify the property model itself, adding or removing variables, constraints, etc.