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.
- 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 valueundefined
. For example, the line below creates a variable namedemails
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 thevariable
member function simply asv
. This is perhaps slightly less readable, but much easier to type; we will use the abbreviated namev
in the remainder of the examples. - 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 inemails
for whichfilter
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 variablesemails
,filter
, andresult
, as shown on the line below..constraint( 'emails, filter, result' )
The return value of the
constraint
member function is the builder. Theconstraint
member function may be abbreviated asc
; we will use this abbreviation in the remainder of the examples. - 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 takesemails
andfilter
and uses them to calculateresult
. 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 ofmethod
is the builder to facilitate chaining, and it may be abbreviated asm
; 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.
- 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 theaddObserver
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. - 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() } );
- 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.
- What element of the view are we binding to?
- 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 itsappendChild
function. - What element of the property model are we binding to?
- 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.
- 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"/>
- 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 )"/>
- 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.
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.
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
.