Basic HotDrink Usage

Table of Contents

1 Introduction

This document describes the basics of how to create a property model, and how that property model operates and interacts with the rest of your web application. We feel that you should not have to use a library without understanding exactly how it is working and what it is doing for you. Therefore, in this document we initially set aside the shortcuts and convenience functions of HotDrink and work with it at the lowest level so that it will be clear exactly how a property model works. Then we slowly lay the groundwork for the shortcuts, making it clear what they are doing for you. After reading this document, you should have a foundation for understanding what HotDrink is doing without feeling like it is “magic”.

2 Creating a constraint system

The most fundamental part of a GUI written with HotDrink is the constraint system which represents data dependencies in your GUI. This first example shows how to create variables and constraints for that system, and how you can directly interact with variables.

We assume here that you have read the document Introduction to HotDrink and are therefore familiar with the basic concept of a constraint system.

2.1 The example

This example shows a single constraint of three variables: emails, filter, and result. Each of these variables holds a string. The value of emails is a comma-delimited list of email addresses. The value of filter is any string. The value of result is the comma-delimited list of just those addresses in emails which contain filter as a substring.

You can use the “Set” buttons to set the values of the emails and filter variables. Then click the “Get” button to see the value of the result variable.

Emails:
Filter:
Result:

Source Code

The entire source code for this example is shown in its entirety below. (Click the “show” links to view the code.) To clarify, we have broken code into three sections: the HTML that defines the View, the JavaScript that defines the View-Model, and the JavaScript that binds the View to the View-Model. In the remainder of this section we will reproduce and examine snippets of this code.

Note that, in this example, we deliberately avoid using any sort of binding mechanisms and interact directly with the property model. This makes the example a little awkward, but also makes clear how the property model works.

 1: <table>
 2:   <style type="text/css" scoped>
 3:     td { padding-right: 1ex; }
 4:     th { padding-right: 1ex; text-align: left; font-weight: bold; }
 5:   </style>
 6:   <tr>
 7:     <th>Emails:</th>
 8:     <td><input type="button" value="Set" onclick="setEmails()"/></td>
 9:   </tr>
10:   <tr>
11:     <th>Filter:</th>
12:     <td><input type="button" value="Set" onclick="setFilter()"/></td>
13:   </tr>
14:   <tr>
15:     <th>Result:</th>
16:     <td><input type="button" value="Get" onclick="getResult()"/></td>
17:   </tr>
18: </table>
 1: // Define root component
 2: var model = new hd.ComponentBuilder()
 3:     // Define variables
 4:     .variable( 'emails', 'joe@foo.com, sue@fum.edu, eve@foo.com, bob@baz.org' )
 5:     .variable( 'filter', 'foo.com' )
 6:     .variable( 'result' )
 7: 
 8:     // Define a constraint
 9:     .constraint( 'emails, filter, result' )
10:     .method( 'emails, filter -> result',
11:              function( emails, filter ) {
12:                var words = emails.trim().split( /\s*,\s*/ );
13:                var filteredWords = words.filter( function( word ) {
14:                  return word.indexOf( filter ) > -1;
15:                } );
16:                return filteredWords.join( ', ' );
17:              } )
18: 
19:     // Get resulting component
20:     .component();
21: 
22: // Create the property model
23: var pm = new hd.PropertyModel();
24: pm.addComponent( model );
25: pm.update();
 1: // Allow user to set emails variable
 2: setEmails = function setEmails() {
 3:   var emails = window.prompt( 'Emails:', model.emails.get() );
 4:   if (emails !== null) {
 5:     model.emails.set( emails );
 6:     pm.update();
 7:   }
 8: }
 9: 
10: // Allow user to set filter variable
11: setFilter = function setFilter() {
12:   var filter = window.prompt( 'Filter:', model.filter.get() );
13:   if (filter !== null) {
14:     model.filter.set( filter );
15:     pm.update();
16:   }
17: }
18: 
19: // Present result to user
20: getResult = function getResult() {
21:   alert( 'Result: ' + model.result.get() );
22: }

2.2 Working with the component builder

Recall from the introduction that the best way to create variables and constraints is in a component, and the best way to create a component is with the component builder. The general strategy for constructing a component is: (1.) create a new builder object, (2.) use the builder member functions to construct the component, (3.) call the component builder member function to retrieve the completed component.

To create variables and constraints, you will use the following builder member functions.

  1. Creating variables

    Just as in a programming language, values in HotDrink are stored in variables. Note, however, that HotDrink variables are not the same as JavaScript variables; HotDrink variables are actually JavaScript objects. Variables are created with the model builder using the variable member function and then stored in the model. To create a variable you must specify a name; you may optionally specify an initial value. As with JavaScript variables, uninitialized variables are given the value undefined. For example, the line below creates a variable named emails which is initialized with a list of email addresses.

        .variable( 'emails', 'joe@foo.com, sue@fum.edu, eve@foo.com, bob@baz.org' )
    

    As mentioned previously, the return value of the variable function is the builder object itself in order to facilitate chaining.

    Builder member functions such as variable are used frequently when declaring a property model. To make them easier to use, the model builder provides abbreviated names for many of them. For example, you may refer to the variable member function simply as v. This is perhaps slightly less readable, but much easier to type; we will use the abbreviated name v in the remainder of the examples.

  2. Creating constraints

    Recall from the introduction that a constraint is something that should always be true concerning some of your variables—i.e., a relation over the variables. In the example above, the relation is that result should contain just those addresses in emails for which filter is a substring.

    To define a constraint, you must specify the variables involved. To make this simple, the constraint member function of the model builder takes a string containing all of the variables' names in a comma delimited list. In this example we define a single constraint for the variables emails, filter, and result, as shown on the line below.

        .constraint( 'emails, filter, result' )
    

    The return value of the constraint member function is the builder. The constraint member function may be abbreviated as c; we will use this abbreviation in the remainder of the examples.

  3. Creating methods

    Notice that, when you create a constraint, you do not tell HotDrink what relation the constraint represents. Instead, you define a constraint by providing a set of constraint satisfaction methods, or just methods for short. A method is a function whose parameters are some variables of the constraint, and which returns new values for other variables of the constraint. The purpose of a method is to provide new values for its output variables that will satisfy the constraint.

    The method creation function of the model builder, named method, takes a signature and a function. The signature is a string defining which variables are inputs and which are outputs; it has the form “/inputs/ -> outputs”, where both inputs and outputs are comma-delimited lists of variables. For this example we provide only a single method, shown below. This method takes emails and filter and uses them to calculate result. You can have more than one method per constraint; we'll discuss this further in later examples.

        .method( 'emails, filter -> result',
                 function( emails, filter ) {
                   var words = emails.trim().split( /\s*,\s*/ );
                   var filteredWords = words.filter( function( word ) {
                     return word.indexOf( filter ) > -1;
                   } );
                   return filteredWords.join( ', ' );
                 } )
    

    Note that in this example we provide an anonymous function as our method, but this is not a requirement; methods can be named functions defined elsewhere. Also note that it is not a requirement for function parameter names to match the variables which should be passed to them; in fact, HotDrink has no way of knowing what you named your parameters. HotDrink will use the signature you provided to decide what variables to pass to the function.

    Whenever you call method, the builder assumes you are adding a method to the most recently defined constraint. As with the other builder member functions, the return value of method is the builder to facilitate chaining, and it may be abbreviated as m; we will use this abbreviation in the remainder of the examples.

2.3 Interacting with variables

Variables are stored as fields in the component to which they belong. For example, in the example we store our component in a variable named model; thus, the emails variable of that component is accessed as model.emails. The value of variables can be retrieved using the get member function and modified using the set member function. If a variable is registered with a property model (e.g., the variable is in a component that has been added to the constraint system), then changes to the variable's value will be noticed by the property model.

In the example above, the following function is called every time the user clicks the “Get” button. The function calls model.result.get to retrieve the current value of the result variable; it then displays this value using the standard JavaScript alert function.

getResult = function getResult() {
  alert( 'Result: ' + model.result.get() );
}

The following function is called every time the user clicks the “Set” button for the email address list. The function calls model.emails.get() to retrieve the current value of the emails variable; this is used to initialize the prompt. Once the user enters a new value, it is stored in the variable using model.emails.set().

setEmails = function setEmails() {
  var emails = window.prompt( 'Emails:', model.emails.get() );
  if (emails !== null) {
    model.emails.set( emails );
    pm.update();
  }
}

Note that in the next section we will introduce binding, which is a better way to set and get variable values.

2.4 Updating the property model

To update the property model means to respond to changes that have been made—such as new constraints or modified variables—by enforcing any constraints which may no longer be satisfied. The constraint system notices, and remembers, when changes occur. However, the constraint system does not actually update until its update member function is called, as shown below. This allows you to package several changes into a single update. In the example above, we call update after adding our component to the property model, as well as after setting a variable.

pm.update();

A more common way of updating the property model is by executing a command. We will discuss commands a little later.

3 Simplified binding

The previous example illustrates the simplest possible interaction with the property model: getting and setting a variable's value, and updating the property model. However, more commonly we do not get and set variable value's directly; instead we rely on bindings to get and set them for us. In this example, we'll write our own very simple bindings to illustrate how this process works.

3.1 The example

This example is very similar to the previous: a list of email addresses which is filtered by a string. The difference is in the way we interact with the property model. In this example, the emails and filter variables are represented by text boxes: editing the text boxes changes the variables. Also, the value of the result variable is shown in the web page and updated automatically.

Emails:
Filter:
Result:

Source Code

Again, in this example we forgo using HotDrink's binding mechanisms and write our own binding mechanisms instead. The purpose of this is to illustrate the work involved in binding.

Note that the View-Model of this example is identical to the previous example; only the View and Binding have changed.

 1: <table>
 2:   <style type="text/css" scoped>
 3:     th { padding-right: 1ex; text-align: left; font-weight: bold; }
 4:     input.long { width: 100ex; }
 5:   </style>
 6:   <tr>
 7:     <th>Emails:</th>
 8:     <td><input type="text" id="emailsEdit" class="long"/></td>
 9:   </tr>
10:   <tr>
11:     <th>Filter:</th>
12:     <td><input type="text" id="filterEdit" class="long"/></td>
13:   </tr>
14:   <tr>
15:     <th>Result:</th>
16:     <td><span id="resultSpan"></span></td>
17:   </tr>
18: </table>
 1: // Define root component
 2: var model = new hd.ComponentBuilder()
 3:     .v( 'emails', 'joe@foo.com, sue@fum.edu, eve@foo.com, bob@baz.org' )
 4:     .v( 'filter', 'foo.com' )
 5:     .v( 'result' )
 6: 
 7:     .c( 'emails, filter, result' )
 8:     .m( 'emails, filter -> result',
 9:         function( emails, filter ) {
10:           var words = emails.trim().split( /\s*,\s*/ );
11:           var filteredWords = words.filter( function( word ) {
12:             return word.indexOf( filter ) > -1;
13:           } );
14:           return filteredWords.join( ', ' );
15:         } )
16: 
17:     .component();
18: 
19: // Create the property model
20: var pm = new hd.PropertyModel();
21: pm.addComponent( model );
 1: // To be done when the document has loaded...
 2: window.addEventListener( 'load', function() {
 3: 
 4:   // When emails edit-box changed, update variable
 5:   var emailsEdit = document.getElementById( 'emailsEdit' );
 6:   emailsEdit.value = model.emails.get();
 7:   emailsEdit.addEventListener( 'input', function() {
 8:     model.emails.set( emailsEdit.value );
 9:     pm.update()
10:   } );
11: 
12:   // When filter edit-box changed, update variable
13:   var filterEdit = document.getElementById( 'filterEdit' );
14:   filterEdit.value = model.filter.get();
15:   filterEdit.addEventListener( 'input', function() {
16:     model.filter.set( filterEdit.value );
17:     pm.update()
18:   } );
19: 
20:   // When result variable changed, update span
21:   var resultSpan = document.getElementById( 'resultSpan' );
22:   resultSpan.appendChild( document.createTextNode( model.result.get() ) );
23:   model.result.addObserver( { onNext: function() {
24:     resultSpan.removeChild( resultSpan.lastChild );
25:     resultSpan.appendChild( document.createTextNode( model.result.get() ) );
26:   } } );
27: 
28: } )

3.2 Introduction to binding

The purpose of binding is to keep a value used by the View and a value used by the View-Model “in sync.” Thus, every time one value is changed, the change is propagated by the binding to the other value. Generally speaking, this simply requires registering a callback function to be called any time one changes so that it can update the other. We can classify bindings based on the direction this update occurs.

  1. A model-to-view binding

    A model-to-view binding requires registering a callback function with a variable so that every time the variable is changed by the View-Model the View will be updated. The interface for this callback is discussed in depth in the Advanced Binding Concepts how-to; the brief version, however, is that we must create an object whose onNext property is a callback function, then pass that object to the addObserver function of the variable.

      model.result.addObserver( { onNext: function() {
        resultSpan.removeChild( resultSpan.lastChild );
        resultSpan.appendChild( document.createTextNode( model.result.get() ) );
      } } );
    

    The callback function we register here simply deletes the old contents of the <span> tag and then adds the value of the variable as the new contents. Now, every time the variable is modified, the span tag will be updated to reflect the same value.

  2. A view-to-model binding

    A view-to-model requires registering a callback function with the View so that every time it changes we can update the View-Model. The specific event for which the callback is registered varies depending on what type of View element it is; in some cases, there may be multiple events that must be subscribed to in order to ensure that every time the value changes the View-Model is updated as well. In the example, we register for the input event of text-edit box, as follows. Here, we make sure every time the value of the text-edit box is changed by the user, the value is used to update the corresponding View-Model.

      emailsEdit.addEventListener( 'input', function() {
        model.emails.set( emailsEdit.value );
        pm.update()
      } );
    
  3. A bi-directional binding

    This example does not use any bi-directional bindings, but it should be clear what such a binding would look like. A bi-directional binding is simply a pair of bindings: one from view-to-model, and one from model-to-view. Together, these bindings ensure that any changes in the View are propagated to the property model, and any changes in the property model are propagated to the view.

3.3 The importance of binding

Binding code tends to be highly reusable. For example, we might write code to bind a variable to a text-edit input or a span tag, then reuse that code any time we wish to bind to such elements. This can often make binding a simple matter of a few lines of code.

There is another important reason why we should use binding instead of simply using the get and set methods of a variable directly. Property models update asynchronously. This is discussed in depth in the Asynchronous Methods, but the important detail for now is that calling update on the property model does not execute any methods of the property model. Instead, it schedules them to be run as soon as their inputs are available.

For example, consider the following code segment.

model.filter.set( '.edu' );
pm.update();
alert( model.result.get() );

In this segment we set the value of a variable, update the property model, then read the value of a different variable. If you were to execute this code, you would find the value of result that you read is not the updated value of result; it is still the old value. The new value won't be ready until your method has a chance to execute.

This is why it is so important that we register a callback to run when the variable changes. As a general rule, we cannot be sure when the new value of a variable will be ready. By registering a callback, we allow the property model to tell us when a new value of the variable is available.

4 HotDrink binding

The previous example illustrated the basic principle of binding: register a callback function to be called when the View (or View-Model) changes so that we can update the View-Model (or View). Now that you understand the basic principles, we would like to shift focus of this document away from binding. To that end, we introduce here a few of HotDrink's built-in bindings.

HotDrink includes bindings for a few common HTML elements, such as edit boxes and tags which can contain text. These bindings are slightly more complex than the bindings of the previous example, but that's primarily because we establish some conventions to promote ease-of-use, interoperability, and reuse; the bindings themselves follow the same basic principle.

4.1 The example

This example is similar to the previous two, but with one change: instead of filtering the email addresses in the list, we simply return one—specifically, we return the one indicated by the index variable. Thus, if index is 1, we return the first email address, and so on.

Emails:
Index:
Result:

Source Code

 1: <table id="ex4">
 2:   <style type="text/css" scoped>
 3:     th { padding-right: 1ex; text-align: left; font-weight: bold; }
 4:     input.long { width: 100ex; }
 5:   </style>
 6:   <tr>
 7:     <th>Emails:</th>
 8:     <td><input type="text" data-bind="bd.edit( emails )" class="long"/></td>
 9:   </tr>
10:   <tr>
11:     <th>Index:</th>
12:     <td><input type="text" data-bind="bd.num( index, 0 )"/></td>
13:   </tr>
14:   <tr>
15:     <th>Result:</th>
16:     <td><span data-bind="bd.text( result )"></span></td>
17:   </tr>
18: </table>
 1: // Define root component
 2: var model = new hd.ComponentBuilder()
 3:     .v( 'emails', 'joe@foo.com, sue@fum.edu, eve@foo.com, bob@baz.org' )
 4:     .v( 'index', 2 )
 5:     .v( 'result' )
 6: 
 7:     .c( 'emails, index, result' )
 8:     .m( 'emails, index -> result',
 9:         function( emails, index ) {
10:           var words = emails.trim().split( /\s*,\s*/ );
11:           return words[index - 1];
12:         } )
13: 
14:     .component();
15: 
16: // Create the property model
17: var pm = new hd.PropertyModel();
18: pm.addComponent( model );
1: window.addEventListener( 'load', function() {
2:   hd.createDeclaredBindings( model, document.getElementById( 'ex4' ) );
3: } );

4.2 Embedding binding instructions within the View

HotDrink bindings can be created using only JavaScript code, much as we did in the previous example. However, HotDrink also allows you to embed JavaScript code in the data-bind attribute of an HTML tag; it uses this JavaScript code to create a binding. This has the advantage of placing binding code as close as possible to the View element which is being bound.

To create the bindings you have specified in your HTML, call the hd.createDeclaredBindings function. This function takes two parameters. The first is a property model component; the second is a DOM node. We say this function binds the component to the DOM sub-tree rooted at the DOM node. In the binding section of this example, we do this as follows. Note that this function is called when the document loads so that we may be sure the entire DOM is available.

hd.createDeclaredBindings( model, document.getElementById( 'ex4' ) );

This function performs a recursive search of the DOM tree beginning at the specified DOM node—i.e., the node itself and everything contained by it. It attempts to create a binding for any tags it finds with a data-bind attribute.

When calling this function, the second parameter is optional; it defaults to document.body, meaning the entire document body is searched.

4.3 Binding specifications

The exact process through which the binding is created is described in detail in the Advanced Binding Concepts document. However, you should already have an idea of what that code does: create event handlers for either the View or View-Model (or both) that update one every time the other is changed.

To create a binding, we generally require the following information.

  1. What element of the view are we binding to?
  2. What kind of element is it? Or, more importantly, what is the interface for interacting with it? For example, you set the contents of a text-edit box by assigning to its value field; you set the contents of a container tag by using its appendChild function.
  3. What element of the property model are we binding to?
  4. Do we need to do any conversion between the View and the property model?

Taken together, we call this information a binding specification. HotDrink allows you to place binding specifications inside the data-bind attribute of HTML tags. This implies item #1 in the list above; it is the tag in which the specification is found. Generally, #2 is represented by a function written to create a certain kind of binding, and #3 is the parameter to that function. Item #4 may be implied by the function, or may be passed as an additional parameter to the function.

Generally speaking, the code inside the data-bind tag is just arbitrary JavaScript code. However, there are a couple of peculiarities. First, inside the data-bind attribute, the variable bd holds a special object called the binding environment. This object contains several member functions which can be used to create HotDrink's built-in bindings. We discuss three of these functions below.

The second peculiarity is that you may refer to properties of the component to which you are binding directly by name. Or, to put this another way, any unqualified names in the JavaScript code are first treated as properties of the component to which you are binding. If they are not found in that component, then they are treated as global names. This convenience feature means that we can simply write, e.g., filter instead of model.filter.

4.4 Three common bindings

Here we give three common bindings which we will use in the remainder of this document.

  1. The edit binding

    The edit binding applies specifically to a text-edit box. The value entered by the user is used to set the variable, and the value of the variable is used to set the value of the text box. This binding is specified using the bd.edit function. The required parameter of this function is the variable to bind to.

    <input type="text" data-bind="bd.edit( emails )" class="long"/>
    
  2. The number binding

    The number binding is a variant of the edit binding in which the value of the text box is converted to a number before being assigned to the variable. This binding is specified using the bd.edit function. The required parameter of this function is the variable to bind to. The first optional parameter is the number of digits to keep after decimal point. Note that numbers are always represented as floating point values, and that precision is achieved through rounding; we can not guarantee precision.p

    <input type="text" data-bind="bd.num( index, 0 )"/>
    
  3. The text binding

    The text binding is suitable for any tag that can contain text. Note that this binding replaces the entire contents of the tag with the value of the variable. This binding is specified using the bd.text function. The required parameter of this function is the variable to bind to.

    <span data-bind="bd.text( result )"></span>
    

5 Multi-way constraints

Constraints in HotDrink can be solved in multiple directions. For example, given the constraint \(width=right-left\), we could solve for any one of the three variables using the values of the other two. To create a multi-way constraint, we simply provide one method for each way the constraint can be solved.

In general, HotDrink tries to preserve the values the user has entered most recently. Thus, when given a choice of how to solve a constraint, HotDrink will look for the method which updates the least recently edited variable.

5.1 The example

This example defines a rectangular region of an image. There are several interrelated values reflected by this region, such as its boundaries, its dimensions, and its aspect ratio (ration of wight to height). Editing any of these values will update others so that they are all consistent.

Left:
Right:
Width:
Top:
Bottom:
Height:
Aspect Ratio:

Source Code

 1: <table id="ex5" style="text-align: right">
 2:   <tr>
 3:     <td>
 4:       Left:  <input type="text" data-bind="bd.num( left )"/><br/>
 5:       Right: <input type="text" data-bind="bd.num( right )"/><br/>
 6:       Width: <input type="text" data-bind="bd.num( width )"/><br/>
 7:     </td>
 8:     <td>
 9:       Top:    <input type="text" data-bind="bd.num( top )"/><br/>
10:       Bottom: <input type="text" data-bind="bd.num( bottom )"/><br/>
11:       Height: <input type="text" data-bind="bd.num( height )"/><br/>
12:     </td>
13:   </tr>
14:   <tr>
15:     <td colspan="2" style="text-align:center">
16:       Aspect Ratio: <input type="text" data-bind="bd.num( aspect )"/>
17:     </td>
18:   </tr>
19: </table>
 1: // Helper functions
 2: function sum ( x, y ) { return x + y; }
 3: function diff( x, y ) { return x - y; }
 4: function prod( x, y ) { return x * y; }
 5: function quot( x, y ) { return x / y; }
 6: 
 7: var model = new hd.ComponentBuilder()
 8:   .variables( 'left, right, width, top, bottom, height, aspect',
 9:               {left: 0, right: 100, top: 60, bottom: 140}
10:   )
11: 
12:   // Constraint: width == right - left
13:   .c( 'left, right, width' )
14:   .m( 'left, width -> right', sum  )
15:   .m( 'right, left -> width', diff )
16:   .m( 'right, width -> left', diff )
17: 
18:   // Constraint: height == bottom - top
19:   .c( 'top, bottom, height' )
20:   .m( 'top, height -> bottom', sum  )
21:   .m( 'bottom, top -> height', diff )
22:   .m( 'bottom, height -> top', diff )
23: 
24:   // Constraint: aspect == width / height
25:   .c( 'width, height, aspect' )
26:   .m( 'height, aspect -> width', prod )
27:   .m( 'width, height -> aspect', quot )
28:   .m( 'width, aspect -> height', quot )
29: 
30:   .component();
31: 
32: var pm = new hd.PropertyModel();
33: pm.addComponent( model );
1: window.addEventListener( 'load', function() {
2:   hd.createDeclaredBindings( model, document.getElementById( 'ex5' ) );
3: } );

5.2 Updating the constraint system

Whenever you modify any of the variables of the property model, HotDrink assumes any constraints which use those variables are no longer satisfied. It then performs two tasks. The first is to decide which variable (or variables) should be updated. The second is to use a method (or methods) you provided to calculate a new value for that variable (or variables).

Deciding which variables to update is not a trivial task. Consider the following scenario: the variable left is modified, so HotDrink decides to update the width variable so that width == right - left. But now the constraint aspect == width / height is no longer satisfied, so HotDrink must update one of those variables as well. Thus, the effects of a single edit can cascade to multiple variables. HotDrink must consider the entire constraint system and decide which variables will be used to update which other variables. This plan for updating is called a dataflow.

Often there is more than one possible dataflow which could work. In this case, HotDrink will generally select the dataflow which preserves variables that have been more recently edited by the user. (In certain unusual cases the behavior is slightly more complicated, but this rule of thumb is close enough for now.) We believe this is generally what the user wants, as the values more recently edited are more likely to reflect the user's current intent.

5.3 Declaring multiple variables at once

This example uses the variables function of the component builder to declare multiple variables at once. This function takes two parameters. The first is a string containing a comma-delimited list of variables to create. The second is an object used as a map to provide initial values for variables. If a variable does not contain an entry in the map, it is treated as uninitialized, and therefore given the value undefined.

  .variables( 'left, right, width, top, bottom, height, aspect',
              {left: 0, right: 100, top: 60, bottom: 140}
  )

Note that the variables are created in the order in which they appear in the string. This is significant as the order in which variables are created determines the initial editing order for the variables, and thus affects the initial dataflow selected by the system. Initializing a variable is treated as an edit; creating an uninitialized variable is not treated as an edit. Thus, the system will initially pick a dataflow that writes to uninitialized variables; If any initialized variables need to be overwritten, the system will choose to overwrite the variables created earlier, since those will be considered less-recently edited.

The variables member function returns the builder. The abbreviation for variables is vs; we will use this abbreviation in the remainder of the examples.

5.4 Reusing functions

Notice that we can reuse the same function for multiple methods. In this example, we defined four simple mathematical functions at the top, then simply used the appropriate function for each method. Again, the values passed as arguments will come from the variables specified in the signature.

In fact, if your constraints really are trivial equations such as this, there is an even easier way to specify them. We'll discuss that in the section Shortcut: equations.

6 Input/output variables

When solving the constraint system, HotDrink must ensure that any variables needing to be updated are given a new value before they are read by any methods. This creates a challenge for methods that want to look at the old value of a variable as they update it. Fortunately, HotDrink has a method of providing the previous value of a variable to a method.

6.1 The example

The constraint here is actually an inequality: \(begin\le{}end+1\). If you set one variable so that the inequality holds, then the other variable is not modified. However, if you make the inequality false, then the other variable is changed just enough to make it true.

Begin:
End:

Source Code

1: <div id="ex6" style="display:inline-block; text-align:right">
2:   Begin: <input type="text" data-bind="bd.num( begin )"/><br/>
3:   End:   <input type="text" data-bind="bd.num( end )"/>
4: </div>
 1: var model = new hd.ComponentBuilder()
 2:   .v( 'begin', 10 ).v( 'end', 20 )
 3: 
 4:    // Constraint: begin < end
 5:   .c( 'begin, end' )
 6:   .m( 'begin, !end -> end',
 7:       function( begin, end ) {
 8:         return end < begin + 1 ? begin + 1 : end;
 9:       } )
10:   .m( '!begin, end -> begin',
11:       function( begin, end ) {
12:         return begin > end - 1 ? end - 1 : begin;
13:       } )
14: 
15:   .component();
16: 
17: var pm = new hd.PropertyModel;
18: pm.addComponent( model );
1: window.addEventListener( 'load', function() {
2:   hd.createDeclaredBindings( model, document.getElementById( 'ex6' ) );
3: } );

6.2 Previous variable values

Technically a method cannot use the same variable as input and output: it would require the method to produce its output before it read its parameters. However, we allow a method to access the previous value of a variable—the value it had before we began solving the constraint system.

In fact, a method may access the previous value of any variable, not just its outputs. For this reason, when a method wants the previous value of a variable as an input, we require that it be indicated in the method's signature. This is done by putting an exclamation point (“!”) in front of the variable name. For example, the following method uses the previous value of begin and the updated value of end to calculate the updated value of begin.

  .m( 'begin, !end -> end',
      function( begin, end ) {
        return end < begin + 1 ? begin + 1 : end;
      } )

6.3 Caveat

In general, it is unwise to make assumptions about when HotDrink will run your method, including how often your method will be run. Thus, you should avoid methods like the following.

.m( "!n, x -> n",
    function( n ) {
      return n + x;
    } )

Such a method may lead to poorly defined behavior, since its effect depends on how frequently it is run. Furthermore, it is unclear what constraint such a method enforces; the relation \(n=n+x\) seems nonsensical.

If you need some sort of incremental behavior, it is better to use a separate variable to represent the increment since the previous iteration. For example, one might use a variable \(t\) to represent time. Then we could write the method as follows:

.method( "!t, t, !n, x -> n", function( t1, t2, n, x ) {
  return n + (t2 - t1)*x;
}

Such a method would define \(n\) as the integral of \(x\) over \(t\). Each change in \(t\) would cause the method to update the value of \(n\) accordingly.

7 Multiple output variables

In many cases it is desirable to have a method with multiple output variables. Of course, a function can only return a single value; however, we allow that value to be an array. The different elements of that array can then be assigned to variables by pattern matching against the signature.

7.1 The example

This is a straightforward constraint: the value for the year is the sum of the values for the four quarters. However, data can only flow two ways in this constraint. If any one of the quarters is modified, then the year is updated. If the year is modified, then we take the difference and distribute it evenly among the four quarters, thus satisfying the constraint.

First Quarter:
Second Quarter:
Third Quarter:
Fourth Quarter:

Full Year:

Source Code

1: <div id="ex7" style="display:inline-block; text-align:right">
2:   First Quarter:  <input type="text" data-bind="bd.num( q1 )"/><br/>
3:   Second Quarter: <input type="text" data-bind="bd.num( q2 )"/><br/>
4:   Third Quarter:  <input type="text" data-bind="bd.num( q3 )"/><br/>
5:   Fourth Quarter: <input type="text" data-bind="bd.num( q4 )"/><br/>
6:   <hr/>
7:   Full Year:      <input type="text" data-bind="bd.num( year )"/><br/>
8: </div>
 1: var model = new hd.ComponentBuilder()
 2:   .vs( 'q1, q2, q3, q4, year', {q1: 10, q2: 30, q3: 60, q4: 100} )
 3: 
 4:   .c( 'q1, q2, q3, q4, year' )
 5:   .m( 'q1, q2, q3, q4 -> year',
 6:       function( q1, q2, q3, q4 ) {
 7:         return q1 + q2 + q3 + q4;
 8:       } )
 9:   .m( 'year, !q1, !q2, !q3, !q4 -> q1, q2, q3, q4',
10:       function( year, q1, q2, q3, q4 ) {
11:         var diff = (year - q1 - q2 - q3 - q4) / 4;
12:         return [q1 + diff, q2 + diff, q3 + diff, q4 + diff ];
13:       }
14:   )
15: 
16:   .component();
17: 
18: var pm = new hd.PropertyModel();
19: pm.addComponent( model );
1: window.addEventListener( 'load', function() {
2:   hd.createDeclaredBindings( model, document.getElementById( "ex7" ) );
3: } );

7.2 Multiple return values

The signature of the following method designates that it will return an array, and that the elements of that array are the values of q1, q2, q3, and q4 respectively. When the value is returned, it is matched against the signature, and values are assigned to the corresponding variables.

  .m( 'year, !q1, !q2, !q3, !q4 -> q1, q2, q3, q4',
      function( year, q1, q2, q3, q4 ) {
        var diff = (year - q1 - q2 - q3 - q4) / 4;
        return [q1 + diff, q2 + diff, q3 + diff, q4 + diff ];
      }
  )

Note that this form of pattern matching works for inputs as well. For example, we could indicate that we wanted to receive the old values of p1, p2, p3, and p4 as an array, like so.

  .m( 'year, [!q1, !q2, !q3, !q4] -> [q1, q2, q3, q4]',
      function( year, qs ) {
        var diff = (year - qs[0] - qs[1] - qs[2] - qs[3]) / 4;
        return qs.map( function( qi ) { return qi + diff; }
      }
  )

8 Shortcut: equations

HotDrink does not inherently know how to solve any type of constraint; it relies on the programmer to provide constraint satisfaction methods. That being said, because many constraints can be represented as very simple mathematical equations, HotDrink includes a very basic equation parser which can translate simple equations into a set of constraint satisfaction methods. You are welcome to use this parser to avoid writing certain types of trivial methods.

8.1 The example

In this example we can calculate the minimum payment required on an account balance based on a given percentage of the balance. We can also specify the actual payment made, though it must be higher than the minimum.

Account Balance:
Minimum Percentage:
Minimum Payment:

Payment Made:
1: <table style="text-align: right" id="ex8">
2:   <tr><td>
3:     Account Balance: <input type="text" data-bind="bd.num( balance, 2 )"/><br/>
4:     Minimum Percentage: <input type="text" data-bind="bd.num( min_rate, 3 )"/><br/>
5:     Minimum Payment: <input type="text" data-bind="bd.num( min_pay, 2 )"/><br/>
6:     <hr/>
7:     Payment Made: <input type="text" data-bind="bd.num( pay, 2 )"/>
8:   </td></tr>
9: </table>
 1: var model = new hd.ComponentBuilder()
 2:   .vs( 'balance, min_rate, min_pay, pay', {balance: 4000, min_rate: 6.125} )
 3:   .equation( "min_pay == balance * min_rate / 100" )
 4:   .equation( "pay >= min_pay" )
 5:   .component();
 6: 
 7: var pm = new hd.PropertyModel();
 8: pm.addComponent( model );
 9: 
10: window.addEventListener( 'load', function() {
11:   hd.createDeclaredBindings( model, document.getElementById( 'ex8' ) );
12: } );

8.2 Allowable equations

HotDrink is not a program for solving mathematical equations. However, simple equations like the ones in this example are common and are not hard to solve. Therefore, as a convenience, HotDrink provides this shortcut method for creating constraints from simple equations.

The equations which HotDrink can enforce are those in which (1) no variable appears in the equation more than once, and (2) the only operations are add (+), subtract (-), multiply (*), and divide (/). This means, e.g., no exponents, no square roots, etc.

Note that, despite the name, the equation member function can also parse inequalities. Thus, the allowable comparison operators in an equation are ==, <=, and >=.

Examples of valid equations:

  • width == right - left
  • aspect == width / height
  • surface == (2*width + 2*length) * height

Examples of invalid equations:

  • perimeter = width + height + width + height
  • area == 3.14*radius^2
  • side == sqrt(area)

8.3 Implementation

HotDrink implements these equations by parsing them and then, for each variable, constructing a function which solves for that variable. Each of these functions then becomes a method. Thus, the constraint will have as many methods as there are variables, with each method updating exactly one variable.

To be clear, the equation function's only advantage is that it saves you some typing; in the end you still get a normal constraint, the same as if you had written the methods yourself.

As you probably expect by now, the equation member function returns the builder. It can be abbreviated eq.

Created: 2015-10-29 Thu 10:13