Advanced Binding Concepts

Table of Contents

!!! This tutorial is still being updated for version 2.1. Please check back later.

1 Declarative binding

We return to the topic of binding, this time to see how we may embed binding specifications into tags. Doing so allows us to iterate over the DOM and automatically perform any bindings we encounter.

1.1 The example

This example uses no constraints; it simply illustrates a new binding technique. Editing the text boxes modifies the value displayed to the right.

X:
Y:

Source code

1: <div id="ex8">
2:   X: <input type="text" data-bind="{mkview: hd.Edit, model: x}"/>
3:            &rArr; <span data-bind="{mkview: hd.Text, model: x}"></span><br/>
4:   Y: <input type="text" data-bind="{mkview: hd.Edit, model: y, toModel: hd.toNum()}"/>
5:            &rArr; <span data-bind="{mkview: hd.Text, model: y}"></span>
6: </div>
1: var component = new hd.ComponentBuilder()
2:     .v( 'x', 'Hello, again!' ).v( 'y', 3 ).component();
3: 
4: var pm = new hd.PropertyModel();
5: pm.addComponent( component );
1: window.addEventListener( 'load', function() {
2:   hd.createDeclaredBindings( component, document.getElementById( 'ex8' ) );
3: } );

1.2 Automatic binding function

By now you've probably noticed that the binding section tends to be repetitive and boring. HotDrink offers two shortcuts to help alleviate this. The first, shown here, is allowing binding specifications to be embedded in the HTML. There are two steps required for this.

  1. Add binding specifications

    The first step is to add markup in the HTML to describe how elements should be bound. To indicate that you wish to bind a variable to the node for an HTML tag, simply add a data-bind attribute to the tag with the code for the binding specification. At run-time, HotDrink will evaluate this attribute string as JavaScript code to take the result as the binding specification object.

      X: <input type="text" data-bind="{mkview: hd.Edit, model: x}"/>
    

    Note, however, one slight change to this binding specification. In previous examples our view object was a view adapter which encapsulated some DOM node in the tree. When you embed a binding specification in the HTML, HotDrink assumes that a view adapter needs to be created for the DOM node. Thus, it looks for an attribute named "mkview". This should be a constructor or factory function which takes a single DOM node parameter. HotDrink will use this constructor or factory to create the view object, like so.

    specification.view = new specification.mkview( domNode );
    

    Also notice that, in the binding specification, you may refer to the variable by it's name in the model. For example, above we simply wrote model: x instead of model: component.x. When evaluating your binding specification, HotDrink will first try to find any names in the component you provided to the binding function (see below); if it cannot find it in the component, it will treat the name as a global identifier.

  2. Call the binding function

    The second step is to call the HotDrink function to performs the binding. You can do this as follows.

      hd.createDeclaredBindings( component, document.getElementById( 'ex8' ) );
    

    This function takes two parameters: a model to use, and a DOM node at which to start searching. This function will search the DOM node passed to it and any DOM nodes under it for elements with a data-bind attribute. When it finds one, it evaluate the binding specification as described above and then pass it to hd.createDeclaredBindings.

    In the example above, HotDrink would examine all tags contained by the <div> tag which has the id ="ex8"=. It would evaluate the binding specifications for each of the four tags with data-bind attributes. Then it will attempt to bind according to each of those specifications.

    The second parameter is optional; it defaults to document.body so that the entire document will be searched. Note, however, that this could cause problems if you are binding different models to different parts of the document (for example, as we are in this document).

    As with manual binding, you must ensure that the nodes involved have been added to the DOM before calling createDeclaredBindings; therefore, it is advised to use the window.onload event or some alternative method of executing code after the DOM is fully ready.

2 Shortcut: binding specification factories

The binding specifications in a data-bind attribute are evaluated as JavaScript code. This means it can contain any JavaScript code which produces a binding object. Thus, HotDrink provides several factory functions that create common binding specifications.

2.1 The example

This is the same as the previous example, except the binding specification objects are created using factory functions.

X:
Y:
1: <div id="ex7">
2:   X: <input type="text" data-bind="hd.edit( x )"/>
3:            &rArr; <span data-bind="hd.text( x )"></span><br/>
4:   Y: <input type="text" data-bind="hd.num( y )"/>
5:            &rArr; <span data-bind="hd.text( y )"></span>
6: </div>
1: var component = new hd.ComponentBuilder()
2:     .v( 'x', 'Hello, again!' ).v( 'y', 3 ).component();
3: 
4: var pm = new hd.PropertyModel();
5: pm.addComponent( component );
1: window.addEventListener( 'load', function() {
2:   hd.createDeclaredBindings( component, document.getElementById( 'ex7' ) );
3: } );

2.2 Factory functions

Binding specification objects tend to be lengthy and follow the same basic patterns. Since the data-bind attribute is simply evaluated as JavaScript code, we can use any JavaScript code we want to create these objects—e.g., call a factory function to create the object for us.

HotDrink provides several factory functions for creating these objects. There is nothing special about these factory functions, and you are encouraged to make your own. For example, the function hd.edit looks basically like the following:

hd.edit = function( model, toView, toModel ) {
  return {mkview:  hd.Edit,
          model:   model,
          toView:  toView,
          toModel: toModel
         };
}

A full list of the factory functions can be found in the Advanced Binding Concepts how-to, but for now we point out the following three:

  1. The hd.edit factory creates the binding specification for a text edit box.
  2. The hd.num factory also creates a specification for a text edit box, but the value is converted to a number before passing it to the model. Note that this factory takes a second optional argument which is used to specify the number of digits after the decimal point.
  3. The hd.text factory creates the binding specification for replacing the contents of a tag with the value of a variable.

3 Binding to variables

The previous example interacted with the property model using only the get and set functions for variables. In actuality, this is not the best way to interact with variables of the property model because the property model updates asynchronously. This means that calling pm.update() does not actually force the property model to update, but rather requests that it update itself at the next available time. Thus, you cannot be sure when a variable's value will be updated.

A better way to interact with variables of the property model is by letting them tell you when they have updated. A variable has an event that is triggered every time its value changes. We can register a callback function for this event so that we update the web page every time the variable changes. Similarly, most HTML input elements have an event that is triggered every time their value changes. We can register a callback function for this event so that every time its. Together, these callback functions represent a binding—a connection between an element of the View and an element of the View-Model.

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

In this example we use HotDrink's binding interfaces, but write our own binding objects. In fact, HotDrink comes with several built-in binding objects; however, writing our own makes it clear what is happening.

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 the context
 2: var context = new hd.ContextBuilder()
 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', function( emails, filter ) {
 9:       var words = emails.trim().split( /\s*,\s*/ );
10:       var filteredWords = words.filter( function( word ) {
11:         return word.indexOf( filter ) > -1;
12:       } );
13:       return filteredWords.join( ', ' );
14:     } )
15: 
16:     .context();
17: 
18: // Create the property model
19: var pm = new hd.PropertyModel();
20: pm.addComponent( context );
 1: // To be done when the document has loaded...
 2: window.addEventListener( 'load', function() {
 3:   // Initialize email text box with variable value
 4:   var emailsEdit = document.getElementById( 'emailsEdit' );
 5:   emailsEdit.value = context.emails.get();
 6: 
 7:   // Make an observable for every time the text box is edited
 8:   var emailsObservable = new hd.BasicObservable();
 9:   emailsEdit.addEventListener( 'input', function() {
10:     emailsObservable.sendNext( emailsEdit.value );
11:   } );
12: 
13:   // Subscribe the variable to the observable
14:   emailsObservable.addObserver( context.emails );
15: 
16:   // Initialize filter text box with variable value
17:   var filterEdit = document.getElementById( 'filterEdit' );
18:   filterEdit.value = context.filter.get();
19: 
20:   // Make an observable for every time the text box is edited
21:   var filterObservable = new hd.BasicObservable();
22:   filterEdit.addEventListener( 'input', function() {
23:     filterObservable.sendNext( filterEdit.value );
24:   } );
25: 
26:   // Subscribe the variable to the observable
27:   filterObservable.addObserver( context.filter );
28: 
29:   // An observer for the result variable
30:   var resultObserver = {
31:     onNext: function( v ) { setSpan( 'resultSpan', v ) },
32:     onError: function( e ) { },
33:     onCompleted: function() { }
34:   };
35: 
36:   // Subscribe the observer to the variable
37:   context.result.addObserver( resultObserver );

3.2 The Observer pattern

You may have noticed that, in the previous example, we waited for the user to click the “Get Result” button before reading the value of the result variable. That was done intentionally. The reason is that property models execute asynchronously. This means that, when you update the property model by calling update, it does not immediately do all the work of solving the constraint system. Instead, it does some of the work right away, and then schedules the rest to be completed later. The ability of a property model to run asynchronously means it cooperates well with other asynchronous programming techniques, such as Ajax calls and web workers. However, it also means that, had we tried to read the value of result immediately after calling update, we would have read the old value of result.

Rather than asking the result variable for its value, the correct approach is to ask it to tell us whenever a new value becomes available. Typically we might do this by registering a callback function to be executed as soon as the value was ready. This technique is sometimes known as the observer pattern — the observer provides a function, and the object being observed calls the function whenever an event occurs. In HotDrink, we use a variation of the observer pattern inspired by Microsoft's reactive extensions. In this pattern, an observer has three associated callbacks: one to be called for normal event values, one for erroneous event values, and one without a value indicating that no more events will occur. Thus, in HotDrink, an observer is any object which has the following three properties:

onNext( value )
function called on normal event; takes value associated with the event
onError( error )
function called when error occurs; takes value associated with the error
onCompleted()
function called when no more events can occur

It does not matter what type of object it is, nor what other properties it has; only that it has these three properties. Here's an example of an observer for the result variable. Whenever the variable takes a new value, it will call onNext, which will update the web page with the new value.

  // An observer for the result variable
  var resultObserver = {
    onNext: function( v ) { setSpan( 'resultSpan', v ) },
    onError: function( e ) { },
    onCompleted: function() { }
  };

3.3 The Observable pattern

The counterpart to an observer is an observable. An observable is an object which publishes values an observer would want to know about. An observer may subscribe to an observable, in which case the observable will notify it of each new value by calling the observer's onNext callback. In HotDrink, an observable is any object which has the following two properties:

addObserver( observer )
Subscribe the observer to the observable so that it will receive notifications
removeObserver( observer )
Unsubsribe the observer so that it will no longer receive notifiactions.

As you have probably guessed, variables of the property model are observable. Thus, we may subscribe our observer to the result variable like so:

  // Subscribe the observer to the variable
  context.result.addObserver( resultObserver );

Now, as soon as the property model updates the result variable, our callback will be executed and the web page will be updated.

3.4 The BasicObservable type

Variables of the property model are not only observable, they are observers as well. When a variable observes some other observable, every value produced by that observable is treated as a set of the variable. (In fact, a variable's onNext and set functions do the exact same thing.) Thus, if we want changes to a text box to be reflected in a variable of the property model, we can create an observable that represents the value of the text box.

Creating an observable object is not hard, but it does tend to involve a lot of boiler-plate code. For this reason, HotDrink provides a simple implementation of an observable object called BasicObservable. You may create instances of this type, or use it as a prototype for your own JavaScript types. A BasicObservable keeps a list of observers in a property named observers. It also provides three functions, sendNext, sendError, and sendCompleted, for invoking callbacks of all subscribed observers.

The code below creates a BasicObservable to represent the text box for the email variable. An event listener is created for the text box so that every time it is edited, the observable will notify any subscribers of the new value. Then we subscribe the emails variable to this new observable. In this way, any changes to the text box will be relayed to the variable.

  // Initialize email text box with variable value
  var emailsEdit = document.getElementById( 'emailsEdit' );
  emailsEdit.value = context.emails.get();

  // Make an observable for every time the text box is edited
  var emailsObservable = new hd.BasicObservable();
  emailsEdit.addEventListener( 'input', function() {
    emailsObservable.sendNext( emailsEdit.value );
  } );

  // Subscribe the variable to the observable
  emailsObservable.addObserver( context.emails );

4 Adapters and translators

If we were to combine the observable and observer objects of the previous example, we would get a single object, both observable and an observer, which representing a text edit box of the View: when the object observes a value, it updates the text box to contain that value, and when the user edits the text box, it notifies any observers with the new value. This object is called a view adapter because it converts the interface of the View—i.e., an HTML DOM node—into the observable/observer interface used by HotDrink.

Often when a variable is bound to the view, the value contained in the View will be slightly different than the value contained in the property model. A good example of this is a variable whose value is a number bound to a text box whose value is a string. While it is possible for an adapter to make such data conversions, creating a separate object specifically to do this conversion, called a translator, allows more flexibility by making it easy to combine different adapters with different translators.

4.1 The example

This example also contains a single constraint. This time, the constraint represents the mathematical equation, \(income - expenses = profit\). As with the previous example, updating the text boxes will cause the result to update automatically.

This example utilizes some new binding techniques, including the ability to convert between string values in the View and numeric values in the View-Model.

Income:
Expenses:
Profit:

Source 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>Income:</th>
 7:     <td><input type="text" id="incomeEdit"/></td>
 8:   </tr>
 9:   <tr>
10:     <th>Expenses:</th>
11:     <td><input type="text" id="expensesEdit"/></td>
12:   </tr>
13:   <tr>
14:     <th>Profit:</th>
15:     <td><span id="profitSpan"></span></td>
16:   </tr>
17: </table>
 1: // Define the context
 2: var context = new hd.ContextBuilder()
 3:     .v( 'income', 2000 )
 4:     .v( 'expenses', 480 )
 5:     .v( 'profit' )
 6: 
 7:     .c( 'income, expenses, profit' )
 8:     .m( 'income, expenses -> profit', function( income, expenses ) {
 9:       return income - expenses;
10:     } )
11: 
12:     .context();
13: 
14: // Create the constraint system
15: var pm = new hd.PropertyModel();
16: pm.addComponent( context );
 1: // To be done when the document has loaded...
 2: window.addEventListener( 'load', function() {
 3: 
 4:   // An adapter for the <input> element for income
 5:   var incomeAdapter = new hd.Edit( document.getElementById( 'incomeEdit' ) );
 6:   // A translator from string to number
 7:   var incomeTranslator = new hd.Translator();
 8:   incomeTranslator.onNext = stringToNumber;
 9:   // Connect observables to observers
10:   incomeAdapter.addObserver( incomeTranslator );
11:   incomeTranslator.addObserver( context.income );
12:   context.income.addObserver( incomeAdapter );
13: 
14:   // An adapter for the <input> element for expenses
15:   var expensesAdapter = new hd.Edit( document.getElementById( 'expensesEdit' ) );
16:   // A translator from string to number
17:   var expensesTranslator = new hd.Translator();
18:   expensesTranslator.onNext = stringToNumber;
19:   // Connect observables to observers
20:   expensesTranslator.addObserver( context.expenses )
21:   expensesAdapter.addObserver( expensesTranslator );
22:   context.expenses.addObserver( expensesAdapter );
23: 
24:   // An adapter for the <span> element for profit
25:   var profitAdapter = new hd.Text( document.getElementById( 'profitSpan' ) );
26:   // A translator from number to string
27:   var profitTranslator = new hd.Translator();
28:   profitTranslator.onNext = fix2;
29:   // Connect observables to observers
30:   context.profit.addObserver( profitTranslator );
31:   profitTranslator.addObserver( profitAdapter );
32: } )
33: 
34: // Helper function for translator:  convert string to number
35: function stringToNumber( s ) {
36:   var n = Number( s );
37:   if (s == '' || isNaN( n )) {
38:     this.sendError( "Invalid number" );
39:   }
40:   else {
41:     this.sendNext( n );
42:   }
43: }
44: 
45: // Helper function for translator:  convert number to string
46: function fix2( n ) {
47:   this.sendNext( n.toFixed( 2 ) );
48: }

4.2 View adapters

A view adapter is an object which represents some element of the view which can be bound to a property model. It is called an adapter because it adapts the view element's interface to the observable/observer interface expected by HotDrink. A view adapter may be read-only (i.e., observable-only), read-write (i.e., observable-observer), or write-only (i.e., observer-only). For example, an adapter for the mouse cursor position would be read-only; an adapter for a text box would be read-write; and an adapter for a <span> tag would be write-only.

HotDrink provides several types of view adapters for common input elements. We describe them in detail in the Advanced Binding Concepts how-to. For now, however, we introduce two adapters for use in the remainder of this how-to.

The first adapter type is the Edit adapter: a read-write adapter that represents the value of a text box. As an observable, the Edit adapter reports every time the value of the text box changes. As an observer, the Edit adapter updates the value of the text box every time the value of the variable being observed changes. To create an Edit adapter, simply create a new hd.Edit object passing the DOM node for the text box to the constructor.

  // An adapter for the <input> element for income
  var incomeAdapter = new hd.Edit( document.getElementById( 'incomeEdit' ) );

Adapters can be creative in the way they choose to read or write values. For example, the Edit adapter does not report a new value with every keystroke; it waits for either a half-second pause in the typing, or for an event that indicates that the edit is complete, such as the user pressing the Enter key or the text box losing focus. Furthermore, if the adapter receives a new value while the user is editing, the adapter waits until the text box loses focus before updating its value.

The second adapter type is the Text adapter: a read-only adapter that represents the text contents of a tag. As an observer, the Text adapter update the contents of its tag every time the value of the variable being observed changes. To create a Text adapter, simply create a new hd.Text object passing the DOM node for the tag to the constructor.

  // An adapter for the <span> element for profit
  var profitAdapter = new hd.Text( document.getElementById( 'profitSpan' ) );

4.3 Translators

The purpose of a translator is to convert a value between the format expected by the View to the format expected by the View-Model. A translator is both an observable and an observer: it observes the value of the View (or View-Model) and produces a value for the View-Model (or the View).

Creating a translator is not hard, but it does tend to involve a lot of boiler-plate code. For this reason, HotDrink provides a simple implementation of a translator called Translator. You may create instances of this type, or use it as a prototype for your own JavaScript types. As it is written, this translator performs no translation; it simply takes the value it was given and passes it on. However, by assigning the translator a new onNext method, we can cause it to modify the values it is given.

  // A translator from string to number
  var incomeTranslator = new hd.Translator();
  incomeTranslator.onNext = stringToNumber;

The prototype for the Translator type is a BasicObservable. Thus, after you have translated your value, you can use sendNext to pass on the translated value. Alternatively, you can use sendError to signal an error.

// Helper function for translator:  convert string to number
function stringToNumber( s ) {
  var n = Number( s );
  if (s == '' || isNaN( n )) {
    this.sendError( "Invalid number" );
  }
  else {
    this.sendNext( n );
  }
}

Note that, while we could create an adapter to convert the number of the View-Model to a string for the View, it is not strictly necessary. JavaScript will automatically convert our number to a string using toString when it is assigned as the value of a text box. This is sufficient for our text boxes. However, in the case of our <span> tag, we would prefer to use toFixedString instead. Thus, we define a translator for this tag.

  // A translator from number to string
  var profitTranslator = new hd.Translator();
  profitTranslator.onNext = fix2;
// Helper function for translator:  convert number to string
function fix2( n ) {
  this.sendNext( n.toFixed( 2 ) );
}

4.4 Connections

Once we have created an adapter and translator, all that is left is to connect them by subscribing observers to observables. To support values going from the View to the View-Model, the translator subscribes to the adapter, and the variable subscribes to the translator. To support values going from the View-Model to the View, the adapter subscribes directly to the variable.

  // Connect observables to observers
  incomeAdapter.addObserver( incomeTranslator );
  incomeTranslator.addObserver( context.income );
  context.income.addObserver( incomeAdapter );

At this point, the variable is bound to the View. Any changes to the variable will go directly to the View. Any changes to the View will first go through the translator, then to the variable.

Note that it is not possible to reuse translators. If you were to try, you would have to subscribe the same translator to two view adapters, and two variables to the same translator. This would mean that every time either view adapter sent a value, both variables would receive the value. Thus, e.g., we must create a second string-to-number translator for the expenses variable; we cannot reuse the one for income.

5 The bind function and binding descriptions

The code required to connect adapters, translators, and variables tends to be somewhat repetitive. To help avoid boilerplate code, HotDrink provides the bind function which can make these connections for you.

5.1 The example

This example also contains a single constraint. This time the constraint represents the mathematical equation \(subtotal*(1+tax/100)=total\). As with previous examples, updating the text box will cause the result to update automatically.

This example performs all binding using the bind function with built-in adapters and translators.

Subtotal:
Tax: %
Total: $

Source 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>Subtotal:</th>
 7:     <td><input type="text" id="subtotalEdit"/></td>
 8:   </tr>
 9:   <tr>
10:     <th>Tax:</th>
11:     <td><input type="text" id="taxEdit"/>%</td>
12:   </tr>
13:   <tr>
14:     <th>Total:</th>
15:     <td>$<span id="totalSpan"></span></td>
16:   </tr>
17: </table>
 1: // Define the context
 2: var context = new hd.ContextBuilder()
 3:     .v( 'subtotal', 39.99 )
 4:     .v( 'tax', 8 )
 5:     .v( 'total' )
 6: 
 7:     .c( 'subtotal, tax, total' )
 8:     .m( 'subtotal, tax -> total', function( subtotal, tax ) {
 9:       return Math.round( subtotal * (100+tax) ) / 100;
10:     } )
11: 
12:     .context();
13: 
14: // Create the constraint system
15: var pm = new hd.PropertyModel();
16: pm.addComponent( context );
 1: window.addEventListener( 'load', function() {
 2:   hd.bind( {view:    new hd.Edit( document.getElementById( 'subtotalEdit' ) ),
 3:             model:   context.subtotal,
 4:             toModel: [hd.toNum(), hd.round( 2 )],
 5:             toView:  hd.fix( 2 )
 6:            }
 7:          );
 8:   hd.bind( {view:    new hd.Edit( document.getElementById( 'taxEdit' ) ),
 9:             model:   context.tax,
10:             toModel: [hd.toNum(), hd.round( 0 )],
11:             toView:  hd.fix( 0 )
12:            }
13:          );
14:   hd.bind( {view:    new hd.Text( document.getElementById( 'totalSpan' ) ),
15:             model:   context.total,
16:             toModel: hd.fix( 2 )
17:            }
18:          );
19: } );

5.2 Translator factories

Just as HotDrink provides several common adapter types, so too it provides several common translator types. While it is possible to create instances of these types yourself using the new operator, HotDrink provides factory functions for creating new instances of these translator types. We describe these functions in detail in the Advanced Binding Concepts how-to. For now, however, we introduce three translators for use in the remainder of this how-to.

The hd.toNum factory function creates a translator that converts a string to a number, just like the translators created in the previous example.

  // Using built-in translator
  var toNumTranslator = hd.toNum();

The hd.fix factory function creates a translator that converts a number to a string. This factory function takes a parameter indicating how many digits should be present after the decimal point.

  // Using built-in translator
  var fix2Translator = hd.fix( 2 );

Note that using a hd.fix translator only modifies how the value is seen by the View; it does not change the value in the View-Model. For example, if a variable was connected to an Edit adapter using the above translator, the user could enter as many digits of precision as he desired; the extra digits would be stored in the View-Model, but would not be displayed by the View.

We can improve this scheme by changing the value which is given to the View-Model using the hd.round translator. This translator takes a number and rounds it to a certain number of digits of precision.

  // A rounding translator
  var roundTranslator = hd.round( 2 );

Note that this translator does not change the data-type of a value; instead it modifies the value. If a change in data-type is required, this translator can be used in conjunction with another, such as hd.toNum. We demonstrate this below.

5.3 Binding descriptions

The function hd.bind takes a single parameter: an object referred to as the binding description. A binding description is any object which has the following four properties:

view
observable/observer representing a value of the View
model
observable/observer representing a value of the View-Model
toView
translator(s) for going from View-Model to View
toModel
translator(s) for going from View to View-Model

The toView and toModel properties are optional; the only required properties are view and model. If present, the toView and toModel properties should hold either a single translator, or an array of translators. Consider, for example, the binding description in the following call to hd.bind.

  hd.bind( {view:    new hd.Edit( document.getElementById( 'subtotalEdit' ) ),
            model:   context.subtotal,
            toModel: [hd.toNum(), hd.round( 2 )],
            toView:  hd.fix( 2 )
           }
         );

Here, the view object is an Edit view adapter, and the model object is the subtotal variable. When going from the View to the View-Model, we first translate the string to a number, then round the number to two digits. Going the other way, we convert the number to a fixed-point string with two digits of precision.

6 Translators

6.1 The example

This example uses several different translators to convert between the values edited by the user in the text box and the values stored in the view-model. The text next to the edit boxes is meant to give a better view of what is actually stored in the model.








Source Code

1: <div>
2:   <input type="text" id="a_edit"/> &rArr; <span id="a_view"></span><br/>
3:   <input type="text" id="b_edit"/> &rArr; <span id="b_view"></span><br/>
4:   <input type="text" id="c_edit"/> &rArr; <span id="c_view"></span><br/>
5:   <input type="text" id="d_edit"/> &rArr; <span id="d_view"></span><br/>
6:   <input type="text" id="e_edit"/> &rArr; <span id="e_view"></span><br/>
7:   <input type="text" id="f_edit"/> &rArr; <span id="f_view"></span><br/>
8:   <input type="text" id="g_edit"/> &rArr; <span id="g_err" style="color:red"></span><br/>
9: </div>
 1: var context = new hd.ContextBuilder()
 2:     .v( 'a', 3.14159 )
 3:     .v( 'b', 0.15 )
 4:     .v( 'c', new Date() )
 5:     .v( 'd', new Date().getTime() )
 6:     .v( 'e', 'John Doe' )
 7:     .v( 'f', 7 )
 8:     .v( 'g', 'delete me' )
 9:     .context();
10: 
11: var pm = new hd.PropertyModel();
12: pm.addComponent( context );
13: pm.update();
 1: window.addEventListener( 'load', function() {
 2:   hd.bind( {view: new hd.Edit( document.getElementById( 'a_edit' ) ),
 3:             model: context.a,
 4:             toModel: hd.toNum(),
 5:             toView: hd.fix( 2 )
 6:            }
 7:          );
 8: 
 9:   hd.bind( {view: new hd.Text( document.getElementById( 'a_view' ) ),
10:             model: context.a
11:            }
12:          );
13: 
14:   hd.bind( {view: new hd.Edit( document.getElementById( 'b_edit' ) ),
15:             model: context.b,
16:             toModel: hd.chain( hd.toNum(),
17:                                hd.round( 0 ),
18:                                hd.scale( 0.01 ) ),
19:             toView: hd.chain( hd.scale( 100 ),
20:                               hd.fix( 0 )      ),
21:            }
22:          );
23: 
24:   hd.bind( {view: new hd.Text( document.getElementById( 'b_view' ) ),
25:             model: context.b
26:            } );
27: 
28:   hd.bind( {view: new hd.Edit( document.getElementById( 'c_edit' ) ),
29:             model: context.c,
30:             toModel: hd.toDate(),
31:             toView: hd.dateToString()
32:            }
33:          );
34: 
35:   hd.bind( {view: new hd.Text( document.getElementById( 'c_view' ) ),
36:             model: context.c
37:            }
38:          );
39: 
40:   hd.bind( {view: new hd.Edit( document.getElementById( 'd_edit' ) ),
41:             model: context.d,
42:             toModel: [hd.toDate(), hd.dateToMilliseconds()],
43:             toView: [hd.toDate(), hd.dateToDateString()]
44:            }
45:          );
46: 
47:   hd.bind( {view: new hd.Text( document.getElementById( 'd_view' ) ),
48:             model: context.d
49:            }
50:          );
51: 
52:   hd.bind( {view: new hd.Edit( document.getElementById( 'e_edit' ) ),
53:             model: context.e,
54:             toModel: hd.def( 'Unknown' )
55:            }
56:          );
57: 
58:   hd.bind( {view: new hd.Text( document.getElementById( 'e_view' ) ),
59:             model: context.e
60:            }
61:          );
62: 
63:   hd.bind( {view: new hd.Edit( document.getElementById( 'f_edit' ) ),
64:             model: context.f,
65:             toModel: [hd.toNum(), hd.def( 1 )]
66:            }
67:          );
68: 
69:   hd.bind( {view: new hd.Text( document.getElementById( 'f_view' ) ),
70:             model: context.f
71:            }
72:          );
73: 
74:   hd.bind( {view: new hd.Edit( document.getElementById( 'g_edit' ) ),
75:             model: context.g,
76:             toModel: [hd.req(), hd.msg( "Don't forget me!" )]
77:            }
78:          );
79: 
80:   hd.bind( {view: new hd.Text( document.getElementById( 'g_err' ) ),
81:             model: context.g.error
82:            }
83:          );
84: } );

6.2 What translators do

As described in Basic HotDrink Usage, translators are objects which are both observable and observers. Translators are used when one wishes to bind a value in the View to a value in the View-Model, but each represents the value differently. A common example would be a text box in the View whose value is a string bound to a variable in the View-Model whose value is a number. A translator sits between the View and the View-Model (or vice-versa) and translates the values being sent from one to the other.

Every binding can potentially have two translators. The translator labeled toView in the binding specification is responsible for translating from the value used by the model to the value used by the view; the translator labeled toModel in the binding specification is responsible for translating from the value used by the view to the value used by the model.

6.3 Combining translators

In general, HotDrink assumes there will at most one translator from the view to the view-model, and at most one translator from the view-model to the view. However, you can easily combine multiple translators into one by wrapping them in a chain translator. A chain encapsulates a sequence of translators, making them look like a single translator. When it receives a value, it translates it by running it through all the translators it contains. The value produced by the final translator becomes the value produced by the chain itself.

You can see chains used in the following binding.

  hd.bind( {view: new hd.Edit( document.getElementById( 'b_edit' ) ),
            model: context.b,
            toModel: hd.chain( hd.toNum(),
                               hd.round( 0 ),
                               hd.scale( 0.01 ) ),
            toView: hd.chain( hd.scale( 100 ),
                              hd.fix( 0 )      ),
           }
         );

However, as a convenience to you, if the bind function finds that you have passed an array as either toView or toModel, it will use the contents of the array to create a chain. This is simply a shortcut to make binding specifications a bit shorter. You can see this shortcut is used in the following binding.

  hd.bind( {view: new hd.Edit( document.getElementById( 'd_edit' ) ),
            model: context.d,
            toModel: [hd.toDate(), hd.dateToMilliseconds()],
            toView: [hd.toDate(), hd.dateToDateString()]
           }
         );

6.4 When translators fail

Translators convert a value from one representation to another. It is possible, however, for that conversion to fail. For example, a translator which translates a string to a number will fail if the string does not parse as a valid number. If an translator fails, then it produces an error value which will be passed along in place of the value for the variable. For example, all translators provided by HotDrink produce a string containing an error message.

Most translators simply pass along error values without modifying them. It is possible, however, for an translator to modify the error value, or even convert the error value back into a variable value (see msg and def.) If an error value reaches the variable, it will be stored in the variable's error property, and the variable's value will not be changed. If a valid value reaches the variable, then the variable's error property will be set to null.

The error property can be bound to the view, allowing the error to be displayed. For more information, see the section "Binding to variable properties").

6.5 List of translators

Here are the translators provided by HotDrink.

  1. toNum

    Converts a value to number using JavaScript's Number constructor. Fails if the result is NaN.

  2. toDate

    Converts a value to a Date object using JavaScript's Date constructor. Fails if the resulting date's getTime method returns NaN.

  3. toStr

    Converts any value to string by invoking its toString method. Generally not needed, as JavaScript will automatically do this any time it needs to coerce a value to string.

  4. toJson

    Converts any value to string by invoking JSON.stringify.

  5. dateToString

    Converts a date object to a string using it's toLocaleString method.

  6. dateToDateString

    Converts a date object to a string using it's toLocaleDateString method.

  7. dateToTimeString

    Converts a date object to a string using it's toLocaleTimeString method.

  8. dateToMilliseconds

    Converts a date object to a number of milliseconds using its getTime method.

  9. fix( places )

    Converts a number to a string containing the specified number of places after the decimal point using JavaScript's Number.toFixed.

  10. prec( sigfigs )

    Converts a number to a string containing the specified number of significant figures using JavaScript's Number.toPrecision.

  11. exp( places )

    Converts a number to a string in scientific notation using the specified number of digits after the decimal point using JavaScript's Number.toExponential.

  12. round( places )

    Rounds a number to specified number of digits after the decimal point. Note that this is different from fix because it produces a number, not a string.

  13. scale( factor )

    Scales a number by a constant factor.

  14. stabilize( time_ms )

    Wait specified number of milliseconds before passing along value; only pass along if no other value arrives in that time period. Does not modify the value.

  15. req()

    Results in error if value is undefined, null, or empty string.

  16. def( value )

    Replaces undefined, null, empty string, or error with given value.

  17. msg( value )

    Replaces error with given value; can be used to customize error messages.

  18. chain( ext, ... )

    Creates a single translator from a list of translators.

  19. fn( f )

    Creates an translator which calls function f on the value, then produces the return value as a result. Discussed in next section.

7 Creating translators from functions

View

 1: <table id="ex2">
 2:   <tr>
 3:     <td></td>
 4:     <td>
 5:       <input type="text" data-bind="hd.num( x )"/> &gt; 10 &rArr;
 6:       <span data-bind="hd.text( x, hd.fn( gt10 ) )"></span>
 7:     </td>
 8:   </tr><tr>
 9:     <td>10 &le;&nbsp;</td>
10:     <td>
11:       <input type="text" data-bind="hd.num( y )"/> &le; 20 &rArr;
12:       <span id="y_view" data-bind="hd.text( y, hd.fn( range, 10, 20 ) )"><span>
13:     </td>
14:   </tr>
15: </table>

View-Model

1: var model = new hd.ModelBuilder()
2:     .v( 'x', 3 )
3:     .v( 'y', 13 )
4:     .end();

Binding

 1: // Global function
 2: gt10 = function gt10( x ) {
 3:   return x > 10 ? 'Yes!' : 'No!';
 4: }
 5: 
 6: // Global function
 7: range = function range( begin, end, x ) {
 8:   return (x >= begin && x <= end) ? 'Yes!' : 'No!';
 9: }
10: 
11: window.addEventListener( 'load', function() {
12:   hd.performDeclaredBindings( model, document.getElementById( 'ex2' ) );
13: } )

Results

##+HTML: <div class="results"> ##+INCLUDE: tangle/fnext.html html ##+HTML: </div> ##+HTML: <script type="text/javascript"> ##+HTML: (function() { ##+INCLUDE: tangle/fnext.js html ##+HTML: })(); ##+HTML: </script>

Notes

This example creates two translators from functions in order to translate the values displayed in the web page.

7.1 The fn translator

Translators are actually objects which conform to a certain interface. You can define your own translator types as JavaScript objects. However, in cases where you need to do a simple translation, there's an easier way:

  1. Write a translation function

    A translation function is a function which takes the incoming value as a parameter, then returns the translated value as the result. For example, the function gt10 on line 2 of the binding section takes a numeric value and translates it to the string "Yes!" if it is greater than 10, and the string "No!" otherwise.

    If your translation fails, your translation function should throw an error value as an exception. This will be caught and passed on as an error value.

    Note that, if you want to, you can write an anonymous translation function directly into the binding specification. The draft for EcmaScript 6 includes an arrow notation for anonymous functions, which would allow such inline functions to be even more convenient.

  2. Use it in a fn translator

    The hd.fn translator takes a translation function as a parameter. Then, when it receives a value, it invokes the translation function, and passes the result on. We used the gt10 function in the binding specification found in the view section on line 6. Thus, the value displayed in the <span> tag is not the numeric value of x, but the string returned by passing that to gt10.

7.2 Extra function parameters

You may want your translation function to take some extra parameters that configure its behavior. For example, the function range in line 7 of the binding section takes a begin and end parameter to specify the range to test. Any extra parameters passed to hd.fn (after the function itself) will be passed directly to the function when it is called. These extra parameters should be the first parameters of the function, with the value to be translated as the final parameter. You can see this in line 12 of the view section.

8 View adapters

8.1 The example

View

 1: <style type="text/css">
 2:   td.fancy { color: #009; background: #9f9; }
 3:   td.plain { color: #666; background: #eee; }
 4:   td.assertive { font-weight: bold; text-transform: uppercase; }
 5: </style>
 6: <table>
 7:   <tr>
 8:     <td>
 9:       <select id="edit_dropdown">
10:         <option>One</option><option>Two</option><option>Three</option>
11:       </select>
12:     </td>
13:     <td id="view_dropdown"></td>
14:   </tr><tr>
15:     <td><input id="edit_num1" type="input"/></td>
16:     <td id="view_num1"></td>
17:   </tr><tr>
18:     <td colspan="2">&nbsp;</td>
19:   </tr><tr>
20:     <td><input type="text" id="edit_num2"/></td>
21:     <td id="view_num2"></td>
22:   </tr><tr>
23:     <td>
24:       <input id="edit_isenabled" type="checkbox"/>
25:       <label for="edit_isenabled">Enabled</label>
26:     </td>
27:     <td id="view_isenabled"></td>
28:   </tr><tr>
29:     <td colspan="2">&nbsp;</td>
30:   </tr><tr>
31:     <td id="view_css" colspan="2">How do I look?</td>
32:   </tr><tr>
33:     <td>
34:       <input id="edit_isfancy" type="checkbox"/>
35:       <label for="edit_isfancy">Fancy</label>
36:     </td>
37:     <td id="view_isfancy"></td>
38:   </tr><tr>
39:     <td>
40:       <input id="edit_isassertive" type="checkbox"/>
41:       <label for="edit_isassertive">Assertive</label>
42:     </td>
43:     <td id="view_isassertive"></td>
44:   </tr><tr>
45:     <td colspan="2">&nbsp;</td>
46:   </tr><tr>
47:     <td>Time:</td>
48:     <td id="view_time"></td>
49:   </tr><tr>
50:     <td>Mouse:</td>
51:     <td id="view_mouse"></td>
52:   </tr>
53: </table>
54: </table>

View-Model

 1: var model = new hd.ModelBuilder()
 2:   .v( 'dropdown',    'Two'        )
 3:   .v( 'num1',        10           )
 4:   .v( 'num2',        20           )
 5:   .v( 'isenabled',   true         )
 6:   .v( 'isfancy',     true         )
 7:   .v( 'isassertive', false        )
 8:   .v( 'time',        new Date()   )
 9:   .v( 'mouse',       {x: 0, y: 0} )
10:   .end();

Binding

  1: window.addEventListener( 'load', function() {
  2:   hd.bind( {view:  new hd.Value( document.getElementById( 'edit_dropdown' ) ),
  3:             model: model.dropdown
  4:            }
  5:          );
  6: 
  7:   hd.bind( {view:    new hd.Value( document.getElementById( 'edit_num1' ) ),
  8:             model:   model.num1,
  9:             toModel: hd.toNum()
 10:            }
 11:          );
 12: 
 13:   hd.bind( {view:    new hd.Edit( document.getElementById( 'edit_num2' ) ),
 14:             model:   model.num2,
 15:             toModel: hd.toNum()
 16:            }
 17:          );
 18: 
 19:   hd.bind( {view:  new hd.Enabled( document.getElementById( 'edit_num2' ) ),
 20:             model: model.isenabled
 21:            }
 22:          );
 23: 
 24:   hd.bind( {view:  new hd.Checked( document.getElementById( 'edit_isenabled' ) ),
 25:             model: model.isenabled
 26:            }
 27:          );
 28: 
 29:   hd.bind( {view:  new hd.Checked( document.getElementById( 'edit_isfancy' ) ),
 30:             model: model.isfancy
 31:            }
 32:          );
 33: 
 34:   hd.bind( {view:  new hd.Checked( document.getElementById( 'edit_isassertive' ) ),
 35:             model: model.isassertive
 36:            }
 37:          );
 38: 
 39:   hd.bind( {view:  new hd.CssClass( 'fancy',
 40:                                     'plain',
 41:                                     document.getElementById( 'view_css' ) ),
 42:             model: model.isfancy
 43:            }
 44:          );
 45: 
 46:   hd.bind( {view:  new hd.CssClass( 'assertive',
 47:                                     null,
 48:                                     document.getElementById( 'view_css' ) ),
 49:             model: model.isassertive
 50:            }
 51:          );
 52: 
 53:   hd.bind( {view:  new hd.Time( 1000 ),
 54:             model: model.time
 55:            }
 56:          );
 57: 
 58:   hd.bind( {view:  new hd.MousePosition(),
 59:             model: model.mouse
 60:            }
 61:          );
 62: 
 63:   hd.bind( {view:  new hd.Text( document.getElementById( 'view_dropdown' ) ),
 64:             model: model.dropdown
 65:            }
 66:          );
 67: 
 68:   hd.bind( {view:  new hd.Text( document.getElementById( 'view_num1' ) ),
 69:             model: model.num1
 70:            }
 71:          );
 72: 
 73:   hd.bind( {view:  new hd.Text( document.getElementById( 'view_num2' ) ),
 74:             model: model.num2
 75:            }
 76:          );
 77: 
 78:   hd.bind( {view:  new hd.Text( document.getElementById( 'view_isenabled' ) ),
 79:             model: model.isenabled
 80:            }
 81:          );
 82: 
 83:   hd.bind( {view:  new hd.Text( document.getElementById( 'view_isfancy' ) ),
 84:             model: model.isfancy
 85:            }
 86:          );
 87: 
 88:   hd.bind( {view:  new hd.Text( document.getElementById( 'view_isassertive' ) ),
 89:             model: model.isassertive
 90:            }
 91:          );
 92: 
 93:   hd.bind( {view:   new hd.Text( document.getElementById( 'view_time' ) ),
 94:             model:  model.time,
 95:             toView: hd.dateToTimeString()
 96:            }
 97:          );
 98: 
 99:   hd.bind( {view:   new hd.Text( document.getElementById( 'view_mouse' ) ),
100:             model:  model.mouse,
101:             toView: hd.pointToString()
102:            }
103:          );
104: 
105: } );

Result

##+HTML: <div class="results"> ##+INCLUDE: tangle/allbind.html html ##+HTML: </div> ##+HTML: <script type="text/javascript"> ##+HTML: (function() { ##+INCLUDE: tangle/allbind.js html ##+HTML: })(); ##+HTML: </script>

Notes

Variable values are displayed in the page so that you can see their values. The "enabled" check-box controls whether the text box is enabled. The "fancy" and "assertive" check-boxes control the style of the preceding text.

8.2 What view adapters do

As described in the Basic HotDrink Usage document, a binding connects an element of the view with an element of the model so that changes in one will be reflected in the other. In order to do this, we need an object that represents the view and which supports the interface HotDrink expects. This object is called a view adapter. We will define the interfaces used in the Programming Guide, but for now we simply list the adapters that we provide with HotDrink.

There are two interfaces which a view adapter may use: one for sending values to the view-model, and one for receiving values from the model. We say that a adapter which only receives values is a read-only adapter, an adapter which only sends values is a write-only adapter, and an adapter which does both is a read-write adapter.

Most adapter types take a single parameter upon construction: an HTML DOM element node for the view element to be encapsulated. There is currently only one that takes additional parameters; there are two which don't require a DOM element because they do not encapsulate a specific DOM node.

This example showcases the adapters that are currently available in HotDrink. As the focus of HotDrink is on view-model behavior, the number of adapters we have developed is relatively small. We expect to add to this collection as time goes on. In the meantime, we have tried to make adding adapters to HotDrink as simple as possible so that, if we don't have the adapter you need, you can easily write it yourself. For more information, see the HotDrink Programming Guide.

8.3 List of adapters

Here are the adapters provided by HotDrink.

  1. Value( el )

    This adapter uses the value of the variable as the value property of the corresponding element. This adapter is well suited for <select> tags, like the one on line 9 of the view section. You may also use it for text-editing inputs, as on line 15 of the view section, though bindEdit is probably a better choice.

    This is a read-write adapter.

  2. Edit( el )

    This adapter also uses the value of the variable as the value property of the tag, but it has some additional behaviors to make it work well with text editing. In particular:

    • When the input is edited, the edit binding waits a fraction of a second before changing its variable in case any more changes are made. If so, the binding discards the previous edit. This is nice when the user is typing something — to update the GUI after every keystroke would be distracting.
    • If the value of the variable changes while the user is editing, the edit binding remembers the new value, but does not set it until the edit box loses focus. This might happen, for example, if the binding converts the value to some canonical form.

    We used the edit binding for the text box in line 20 of the view section. Compare it's behavior to the value binding. (Try typing "1e10" into both to see what happens.)

    This is a read-write adapter.

  3. Checked( el )

    This adapter uses a Boolean value as the checked property on a check-box.

    This is a read-write binding: checking or un-checking the check-box updates the variable.

  4. Enabled( el )

    This adapter uses a Boolean value to determine whether or not a widget is enabled — a true value means enabled, a false value means disabled. Later on we'll show you how this can be used to automatically disable variables which become irrelevant.

    This is a read-only adapter: explicitly enabling or disabling the widget will not affect the value of the variable.

  5. CssClass( ontrue, onfalse, el )

    This adapter uses a Boolean value to determine whether or not the given CSS class name should be applied to the view element. Notice that this constructor takes additional parameters. If the boolean value is true, then the ontrue class will be added to the element. If it is false, then the onfalse class will be added to the element.

    Both ontrue and onfalse may be null; if so, then the adapter simply will not add a class for the corresponding value. You can see, the binding on line 44 of the binding section specifies both, whereas the binding on line 51 of the binding section only specifies a class to add when the value is true.

    This is a read-only binding: explicitly setting the element's class name will not update the variable.

  6. Text( el )

    This adapter replaces the contents of the tag with the value of the variable. Note that the value is treated as text, not as HTML; any HTML tags or entities in the value will simply be displayed as text.

    This is a read-only binding: explicitly changing the contents of the tag will not update the variable.

  7. Time( update_ms )

    This adapter writes the current time to the variable every update_ms milliseconds. The time is given as a JavaScript Date object.

    This is a write-only binding: changing the contents of the variable will have no effect on the time.

  8. MousePosition()

    This adapter writes the current mouse position to the variable every time the mouse moves. The position is given as a JavaScript object with an x and y property.

    This is a write-only binding: changing the contents of the variable will have no effect on the mouse position.

  9. Position( el )

    This adapter takes a point (an object with an x and y property) and uses it to set the element's CSS left and top properties. The adapter will also set the element's CSS position property to absolute.

    This is a read-only binding: changing the position or the style of the element will not update the variable.

8.4 Factory functions

As described in the Basic HotDrink Usage document, factory functions are simply helpers which generate binding specifications for you. We have included factories for the specifications we think will be most common. However, you are encouraged to create and use your own if the provided factories do not meet your needs.

When using declarative binding, the format of the data-bind attribute is a comma-delimited list of binding specifications. In order to allow factory functions to generate multiple binding specifications, we make the following accommodation: the list of specifications will be flattened — that is, if an array is found in the list, the contents of the array will be interpolated into the list in place of the array. This flattening is repeated until there are no more arrays in the list; thus arrays can be arbitrarily nested.

8.5 List of factories

  1. value( model, toView?, toModel? )

    Use the Value adapter with optional toView and toModel arguments.

    hd.value( model, toView, toModel )
      ==
    {mkview:  hd.Value,
     model:   model,
     toView:  toView,
     toModel: toModel
    }
    
  2. edit( model, toView?, toModel? )

    Use the Edit adapter with optional toView and toModel arguments.

    hd.edit( model, toView, toModel )
      ==
    {mkview:  hd.Edit,
     model:   model,
     toView:  toView,
     toModel: toModel
    }
    
  3. number( model )

    Use the Edit adapter where the variable holds a number. Note that no toView is needed, as JavaScript will automatically convert a number to a string when needed.

    hd.num( model )
      ==
    {mkview:  hd.Edit,
     model:   model,
     toModel: hd.toNum()
    }
    
  4. number( model, places, toView?, toModel? )

    This overloaded form of number will round the number to a specified number of digits after the decimal point. Note that the toView and toModel specified here should treat the view as if it is a number, not a string. For example, the following specification will round to two places after the decimal point and the value showed in the view will be 100 times the value in the model.

    hd.num( model, 2, hd.scale( 100 ), hd.scale( 0.01 ) );
    
  5. date( model )

    Use the Edit adapter where the variable holds a Date object.

    hd.date( model )
      ==
    {mkview:  hd.Edit,
     model:   model,
     toView:  hd.dateToDateStr(),
     toModel: hd.toDate()
    }
    
  6. date( model, toView, toModel )

    This overloaded form of date allows an additional toView and toModel transformation. Note that these transformations hsould treat the view as if it is a date, not a string. For example, the following specification will let the user enter a date, but will store it in the model as the number of milliseconds after the epoch.

    hd.date( model, hd.dateToMilliseconds(), hd.toDate() );
    
  7. checked( model, toView?, toModel? )

    Use the Checked adapter with optional toView and toModel arguments.

    hd.checked( model, toView, toModel )
      ==
    {mkview:  hd.Checked,
     model:   model,
     toView:  toView,
     toModel: toModel
    }
    
  8. enabled( model, toView? )

    Use the Enabled adapter with optional toView argument.

    hd.enabled( model, toView )
      ==
    {mkview:  hd.Enabled,
     model:   model,
     toView:  toView
    }
    
  9. cssClass( model, ontrue, onfalse, toView? )

    Use the CssClass adapter with optional toView argument.

    Note that the ontrue and onfalse values are bound to the CssClass constructor to create a constructor requiring only a single argument. If you don't want to specify a class for ontrue (or onfalse) you may use the empty string or null.

    hd.cssClass( model, ontrue, onfalse, toView )
      ==
    {mkview:  hd.CssClass.bind( null, ontrue, onfalse ),
     model:   model,
     toView:  toView
    }
    
  10. cssClass( model )

    This overloaded form of cssClass takes a variable and generates multiple binding specifications which bind several CSS classes to various variable properties. For more information on binding to variable properties, see the section "Binding to variable properties".

    hd.cssClass( variable )
      ==
    [cssClass( variable.source,        'source',       'derived'         ),
     cssClass( variable.pending,       'pending',      'complete'        ),
     cssClass( variable.constributing, 'contributing', 'noncontributing' ),
     cssClass( variable.error,         'error',         null             )
    ];
    
  11. text( model, toView? )

    Use the Text adapter with optional toView argument.

    hd.text( model, toView )
      ==
    {mkview:  hd.Edit,
     model:   model,
     toView:  toView
    }
    
  12. editVar( model, toView?, toModel? )

    This single factory function is a shortcut for invoking three different factories on a variable.

    hd.editVar( model, toView, toModel )
     ==
    [hd.edit( model, toView, toModel ),
     hd.cssClass( model ),
     hd.enabled( model.relevant )
    ]
    
  13. numVar( model, places?, toview?, toModel? )

    This single factory function is a shortcut for invoking three different factories on a variable.

    hd.numVar( model, places, toView, toModel )
     ==
    [hd.num( model, places, toView, toModel ),
     hd.cssClass( model ),
     hd.enabled( model.relevant )
    ]
    
  14. dateVar( model, toView?, toModel? )

    This single factory function is a shortcut for invoking three different factories on a variable.

    hd.dateVar( model, toView, toModel )
     ==
    [hd.edit( model, toView, toModel ),
     hd.cssClass( model ),
     hd.enabled( model.relevant )
    ]
    

9 Binding to variable properties

View

 1: <style type="text/css">
 2:   .source { background-color: #ffe; }
 3:   .derived { background-color: #eee; }
 4:   .error { border: 2px solid #f00; }
 5: </style>
 6: <table id="ex4" style="text-align: right">
 7:   <tr>
 8:     <td>Number:</td>
 9:     <td><input type="text" data-bind="hd.num( x ),
10:            hd.cssClass( x.error, 'error' )"/></td>
11:     <td style="color: red" data-bind="hd.text( x.error )"></td>
12:   </tr><tr>
13:     <td>&nbsp;</td>
14:   </tr><tr>
15:     <td>Width:</td>
16:     <td><input type="text" data-bind="hd.num( width ),
17:            hd.cssClass( width.source, 'source', 'derived' )"/><td>
18:   </tr><tr>
19:     <td>Height:</td>
20:     <td><input type="text" data-bind="hd.num( height ),
21:            hd.cssClass( height.source, 'source', 'derived' )"/><td>
22:   </tr><tr>
23:     <td>Area:</td>
24:     <td><input type="text" data-bind="hd.num( area ),
25:            hd.cssClass( area.source, 'source', 'derived' )"/><td>
26:   </tr>
27: </table>

View-Model

1: var model = new hd.ModelBuilder()
2:   .v( 'x', 10 ).v( 'width', 8 ).v( 'height', 6 ).v( 'area' )
3:   .eq( 'area == width * height' )
4:   .end();
5: 
6: var system = new hd.ConstraintSystem();
7: system.addComponent( model );

Binding

1: window.addEventListener( 'load', function() {
2:   hd.performDeclaredBindings( model, document.getElementById( 'ex4' ) );
3: } )

Result

##+HTML: <div class="results"> ###+INCLUDE: tangle/prop.html html ##+HTML: </div> ##+HTML: <script type="text/javascript"> ##+INCLUDE: tangle/prop.js html ##+HTML: </script>

Notes

Typing an invalid number into the first box gives it a red border and produces an error message. The variables width, height, and area, are related by the constraint area == width * height. Thus, data will always flow from two of these variables into the third. The two source variables will be yellow, and the third — which is derived from the other two — will be gray.

9.1 Variable properties

You can bind to more than just a variable in the view-model; any object conforming to a certain interface can be bound to. (We describe this interface later in the Programming Guide). Each variable contains several properties which conform to this interface, and thus can be bound to by the view.

It is important to note that these properties are read-only. Thus, they should only be used with read-only adapters, such as text and cssClass, and never with read-write adapters, such as Edit.

9.2 List of variable properties

Although we list all properties here, some of them will not be fully explained here. They will be, however, in other documentation.

  1. error

    This property contains any error that was generated the last time the variable was set — whether by the view or by a method. In particular, note that any exceptions thrown by translators will be stored in this property.

    If no error was generated for the variable, the value of this property will be null. Note that, using normal JavaScript semantics, you may use this property as a Boolean value: an object or non-empty string will naturally evaluate to true, and null or the empty string will evaluate to false.

  2. source

    This Boolean property is true if the variable was a source (kept its value) during the last evaluation, and false if it was derived (received a new value from a method).

  3. pending

    This Boolean property is true if the variable is currently waiting for an update from an asynchronous method, and false if it is not. For more information, see Asynchronous Methods.

  4. contributing

    This Boolean property is true if the value of the variable was used to compute an output variable, and false if it was not. For more information, see Asynchronous Methods.

  5. relevant

    This Boolean property is true if editing the variable may cause a change to an output variable, and false if not. For more information, see Asynchronous Methods.

  6. value

    This property contains the current value of the variable. There should be no reason to bind to this property, as it is the same as binding to the variable itself, except that it is read-only. It is listed only for completeness.

Created: 2015-10-29 Thu 10:02