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.

  1. The PropertyModel type

    A 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.

  2. The Component type

    A 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.

  3. The ComponentBuilder type

    The 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.

Created: 2015-10-29 Thu 10:02