Advanced Binding Concepts
Table of Contents
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.
Y: ⇒
Source code
1: <div id="ex8"> 2: X: <input type="text" data-bind="{mkview: hd.Edit, model: x}"/> 3: ⇒ <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: ⇒ <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.
- 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 ofmodel: 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. - 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 tohd.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 withdata-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 thewindow.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.
Y: ⇒
1: <div id="ex7"> 2: X: <input type="text" data-bind="hd.edit( x )"/> 3: ⇒ <span data-bind="hd.text( x )"></span><br/> 4: Y: <input type="text" data-bind="hd.num( y )"/> 5: ⇒ <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:
- The
hd.edit
factory creates the binding specification for a text edit box. - 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. - 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"/> ⇒ <span id="a_view"></span><br/> 3: <input type="text" id="b_edit"/> ⇒ <span id="b_view"></span><br/> 4: <input type="text" id="c_edit"/> ⇒ <span id="c_view"></span><br/> 5: <input type="text" id="d_edit"/> ⇒ <span id="d_view"></span><br/> 6: <input type="text" id="e_edit"/> ⇒ <span id="e_view"></span><br/> 7: <input type="text" id="f_edit"/> ⇒ <span id="f_view"></span><br/> 8: <input type="text" id="g_edit"/> ⇒ <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.
toNum
Converts a value to number using JavaScript's
Number
constructor. Fails if the result isNaN
.toDate
Converts a value to a
Date
object using JavaScript'sDate
constructor. Fails if the resulting date'sgetTime
method returnsNaN
.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.toJson
Converts any value to string by invoking
JSON.stringify
.dateToString
Converts a date object to a string using it's
toLocaleString
method.dateToDateString
Converts a date object to a string using it's
toLocaleDateString
method.dateToTimeString
Converts a date object to a string using it's
toLocaleTimeString
method.dateToMilliseconds
Converts a date object to a number of milliseconds using its
getTime
method.fix( places )
Converts a number to a string containing the specified number of places after the decimal point using JavaScript's
Number.toFixed
.prec( sigfigs )
Converts a number to a string containing the specified number of significant figures using JavaScript's
Number.toPrecision
.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
.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.scale( factor )
Scales a number by a constant factor.
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.
req()
Results in error if value is
undefined
,null
, or empty string.-
def( value )
Replaces
undefined
,null
, empty string, or error with given value. -
msg( value )
Replaces error with given value; can be used to customize error messages.
chain( ext, ... )
Creates a single translator from a list of translators.
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 )"/> > 10 ⇒ 6: <span data-bind="hd.text( x, hd.fn( gt10 ) )"></span> 7: </td> 8: </tr><tr> 9: <td>10 ≤ </td> 10: <td> 11: <input type="text" data-bind="hd.num( y )"/> ≤ 20 ⇒ 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:
- 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.
- Use it in a
fn
translatorThe
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 thegt10
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 ofx
, but the string returned by passing that togt10
.
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"> </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"> </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"> </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.
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, thoughbindEdit
is probably a better choice.This is a read-write adapter.
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.
- When the input is edited, the
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.
Enabled( el )
This adapter uses a Boolean value to determine whether or not a widget is enabled — a
true
value means enabled, afalse
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.
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 theonfalse
class will be added to the element.Both
ontrue
andonfalse
may benull
; 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.
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.
Time( update_ms )
This adapter writes the current time to the variable every
update_ms
milliseconds. The time is given as a JavaScriptDate
object.This is a write-only binding: changing the contents of the variable will have no effect on the time.
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
andy
property.This is a write-only binding: changing the contents of the variable will have no effect on the mouse position.
Position( el )
This adapter takes a point (an object with an
x
andy
property) and uses it to set the element's CSSleft
andtop
properties. The adapter will also set the element's CSSposition
property toabsolute
.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
- value( model, toView?, toModel? )
Use the
Value
adapter with optionaltoView
andtoModel
arguments.hd.value( model, toView, toModel ) == {mkview: hd.Value, model: model, toView: toView, toModel: toModel }
- edit( model, toView?, toModel? )
Use the
Edit
adapter with optionaltoView
andtoModel
arguments.hd.edit( model, toView, toModel ) == {mkview: hd.Edit, model: model, toView: toView, toModel: toModel }
- number( model )
Use the
Edit
adapter where the variable holds a number. Note that notoView
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() }
- 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 thetoView
andtoModel
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 ) );
- date( model )
Use the
Edit
adapter where the variable holds aDate
object.hd.date( model ) == {mkview: hd.Edit, model: model, toView: hd.dateToDateStr(), toModel: hd.toDate() }
- date( model, toView, toModel )
This overloaded form of
date
allows an additionaltoView
andtoModel
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() );
- checked( model, toView?, toModel? )
Use the
Checked
adapter with optionaltoView
andtoModel
arguments.hd.checked( model, toView, toModel ) == {mkview: hd.Checked, model: model, toView: toView, toModel: toModel }
- enabled( model, toView? )
Use the
Enabled
adapter with optionaltoView
argument.hd.enabled( model, toView ) == {mkview: hd.Enabled, model: model, toView: toView }
- cssClass( model, ontrue, onfalse, toView? )
Use the
CssClass
adapter with optionaltoView
argument.Note that the
ontrue
andonfalse
values are bound to theCssClass
constructor to create a constructor requiring only a single argument. If you don't want to specify a class forontrue
(oronfalse
) you may use the empty string ornull
.hd.cssClass( model, ontrue, onfalse, toView ) == {mkview: hd.CssClass.bind( null, ontrue, onfalse ), model: model, toView: toView }
- 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 ) ];
- text( model, toView? )
Use the
Text
adapter with optionaltoView
argument.hd.text( model, toView ) == {mkview: hd.Edit, model: model, toView: toView }
- 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 ) ]
- 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 ) ]
- 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> </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.
- 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 totrue
, andnull
or the empty string will evaluate tofalse
. - 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).
- 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.
- 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.
- 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.
- 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.