Programming Guide

Table of Contents

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

1 Utility: an HTML JavaScript Console

We wish to start by illustrating a few simple classes and interfaces used in HotDrink. Unfortunately, a web page does not offer a particularly good environment for demonstrating simple code. Thus, we compensate with a simple "HTML Console" which allows simple string I/O within a web page. This code is not relevant to HotDrink itself, only to these examples. Therefore, we present it once (in case your interested) and then assume it is available from here on.

The HtmlConsole class takes an empty HTMLDivElement (i.e., the DOM node for a <div> tag) and fills it with the HTML elements needed for the console. It has two public members. The first is the println function takes a string and prints it on a new line in the console. The second is the oninput property where one can assign a callback function to be called when input is generated.

HTML

 1: <style type="text/css">
 2:   div.console {
 3:     width: 600px;  height: 120px;  padding: 10px; overflow: auto;
 4:     background: #224; color: #FFF
 5:   }
 6:   div.console div.input {
 7:     color: #999;
 8:   }
 9:   input[type="text"].console {
10:     width: 554px; padding: 1px;
11:     background: #EEF;
12:   }
13:   input[type="submit"].console {
14:     width: 60px;
15:   }
16: </style>

JavaScript

 1: function HtmlConsole( div ) {
 2:   var form = document.createElement( 'form' );
 3:   div.appendChild( form );
 4: 
 5:   var console = document.createElement( 'div' );
 6:   console.className = "console";
 7:   form.appendChild( console );
 8: 
 9:   var input = document.createElement( 'input' );
10:   input.type = "text";
11:   input.className = "console";
12:   form.appendChild( input );
13: 
14:   var go = document.createElement( 'input' );
15:   go.type = "submit";
16:   go.value = "Go";
17:   go.className = "console";
18:   form.appendChild( go );
19: 
20:   form.onsubmit = function() {
21:     this.println( input.value, 'input' );
22:     if (this.oninput) {
23:       this.oninput( input.value );
24:     }
25:     input.select();
26:     return false;
27:   }.bind( this );
28: 
29:   this.console = console;
30: }
31: 
32: HtmlConsole.prototype.println = function( msg, className ) {
33:   var newdiv = document.createElement( 'div' );
34:   if (className) {
35:     newdiv.className = className;
36:   }
37:   newdiv.appendChild( document.createTextNode( msg ) );
38:   this.console.appendChild( newdiv );
39:   this.console.scrollTop = this.console.scrollHeight - this.console.clientHeight;
40: }

2 HotDrink Observable/Observer types

2.1 Observable and Observer interfaces

2.1.1 The example

HTML

1: <div id="console1"></div>

JavaScript

 1: // Create console
 2: var console = new HtmlConsole( document.getElementById( 'console1' ) );
 3: 
 4: // Observer object
 5: var observer = {
 6:   id: 'Observer',
 7:   onNext:    function( v ) { console.println( '[' + this.id + '] value: ' + v );    },
 8:   onError:   function( e ) { console.println( '[' + this.id + '] error: ' + e );    },
 9:   onCompleted:  function() { console.println( '[' + this.id + '] complete' );       }
10: };
11: 
12: // Observable object
13: var observable = new hd.BasicObservable();
14: 
15: // Subscribe
16: observable.addObserver( observer );
17: 
18: // Console input triggers observable
19: console.oninput = function( value ) {
20:   if (value == "END") {
21:     observable.sendCompleted();
22:   }
23:   else {
24:     var n = Number( value );
25:     if (isNaN( n )) {
26:       observable.sendError( 'Not a number' );
27:     }
28:     else {
29:       observable.sendNext( n );
30:     }
31:   }
32: }

Result

Notes

The Observable produces numbers as they are entered in the input box. The Observer receives these and prints them to the console. Values which are not valid numbers produce errors. The special value "END" completes the Observable.

2.1.2 Defining the interfaces

Observable and Observer are two interfaces that we use in HotDrink. An interface simply defines certain member variables or functions that an object should have. Note that, in JavaScript, you do not have to explicitly declare which interfaces your object implements; any object which has the specified members is said to implement the interface.

An Observable is largely equivalent to what is called an "event" in traditional event-driven programming: it represents something that may occur multiple times during the lifetime of the program, and allows you to register a callback function (in the form of an Observer) to be executed whenever it does. Each time the event occurs, a single value is passed to the callback function to represent the event. Thus, we may describe an Observable as something which produces values from time to time over its lifetime.

Our implementation of observable and observer is largely inspired by Microsoft's reactive extensions. Unlike a traditional event, Observables actually have three associated callbacks: one for normal event values, one for error values indicating something has gone wrong, and one without a value indicating no more values will be produced. An Observer is any object which provides these three callback functions; thus, an Observer must have 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

Here is the JavaScript excerpt which creates an object implementing the Observer interface. For this simple example, the three callbacks do nothing more than print the argument to the HTML console. Again, notice that the object may contain fields beyond those required by the Observer interface.

// Observer object
var observer = {
  id: 'Observer',
  onNext:    function( v ) { console.println( '[' + this.id + '] value: ' + v );    },
  onError:   function( e ) { console.println( '[' + this.id + '] error: ' + e );    },
  onCompleted:  function() { console.println( '[' + this.id + '] complete' );       }
};

An Observer must be registered with an Observable in order for its callbacks to be invoked by that Observer. We say that the Observer subscribes to the Observable. An Observable is any object which allows observers to subscribe and unsubscribe; thus, an Observable must have the following two properties.

addObserver( observer )
function which subscribes the observer to this observable
removeObserver( observer )
function which unsubscribes the observer from this observable

2.1.3 The BasicObservable type

As we said before, any JavaScript object can be Observable as long as it contains the two functions defined in the interface. However, in order to simplify the process of making Observable types, we have created a simple, straightforward implementation named hd.BasicObservable. In the following excerpt we create a new instance and subscribe the observer to it.

// Observable object
var observable = new hd.BasicObservable();

// Subscribe
observable.addObserver( observer );

The BasicObservable type keeps an array of Observers which have subscribed to the object in a property named observers; it is recommended that you not modify this field directly, but use addObserver and removeObserver instead.

In addition, the BasicObservable type has member functions sendNext, sendError, and sendCompleted which can be used to invoke the corresponding callbacks of all subscribed Observers.

sendNext( value )
call the onNext function of all subscribers with the given event value
sendError( error )
call the onError function of all subscribers with the given error value
sendCompleted()
call the onCompleted function of all subscribers; also unsubscribes all observers so that they will not receive any further messages

The following excerpt uses these functions to invoke callbacks when it receives input from the console.

// Console input triggers observable
console.oninput = function( value ) {
  if (value == "END") {
    observable.sendCompleted();
  }
  else {
    var n = Number( value );
    if (isNaN( n )) {
      observable.sendError( 'Not a number' );
    }
    else {
      observable.sendNext( n );
    }
  }
}

2.2 Custom Observable and observer types

2.2.1 The example

HTML

1: <div id="console2"></div>

JavaScript

 1: var console = new HtmlConsole( document.getElementById( 'console2' ) );
 2: 
 3: /*--------------------------------------------------------------------
 4:  * An observable type
 5:  */
 6: 
 7: // Constructor
 8: NumberObservable = function NumberObservable() { }
 9: 
10: // Inheritance
11: NumberObservable.prototype = Object.create( hd.BasicObservable.prototype );
12: 
13: // Convert string to number
14: NumberObservable.prototype.processInput = function( value ) {
15:   if (value === "END") {
16:     this.sendCompleted();
17:   }
18:   else {
19:     var n = Number( value );
20:     if (isNaN( n )) {
21:       this.sendError( 'Not a number' );
22:     }
23:     else {
24:       this.sendNext( n );
25:     }
26:   }
27: }
28: 
29: /*--------------------------------------------------------------------
30:  * Observable instance
31:  */
32: 
33: var observable = new NumberObservable();
34: 
35: console.oninput = observable.processInput.bind( observable );
36: 
37: /*--------------------------------------------------------------------
38:  * An observer type
39:  */
40: 
41: // Constructor
42: NumberObserver = function NumberObserver( id, console ) {
43:   this.id = id;
44:   this.console = console;
45: }
46: 
47: // onNext callback
48: NumberObserver.prototype.onNext = function( value ) {
49:   this.console.println( '[' + this.id + '] number: ' + value );
50: }
51: 
52: // onError callback
53: NumberObserver.prototype.onError = function( error ) {
54:   this.console.println( '[' + this.id + '] error: ' + error );
55: }
56: 
57: // onCompleted callback
58: NumberObserver.prototype.onCompleted = function() {
59:   this.console.println( '[' + this.id + '] complete' );
60: }
61: 
62: /*--------------------------------------------------------------------
63:  * Observer instances
64:  */
65: 
66: var observerA = new NumberObserver( 'Observer-A', console );
67: observable.addObserver( observerA );
68: 
69: var observerB = new NumberObserver( 'Observer-B', console );
70: observable.addObserver( observerB );

Result

Notes

This example is the same as the first (albeit with two observers instead of one). The difference in the code is that we have created our own types to encapsulate details and improve abstraction.

2.2.2 Creating BasicObservable subtypes

The recommended way to create your own observable type is to make a subtype of BasicObservable. This will give you all the BasicObservable methods we've discussed so far: addObserver, removeObserver, sendNext, sendError, and sendCompleted. The only member of BasicObservable we haven't discussed is the observers property, which used to store all subscribed observers. It is recommended that you not access this field directly, but use addObserver and removeObserver instead.

The following excerpt creates a type named NumberObservable which triggers observers' callbacks when its processInput method is called. Note that the BasicObservable constructor does nothing, so there is no need to call it in your own constructor.

/*--------------------------------------------------------------------
 * An observable type
 */

// Constructor
NumberObservable = function NumberObservable() { }

// Inheritance
NumberObservable.prototype = Object.create( hd.BasicObservable.prototype );

// Convert string to number
NumberObservable.prototype.processInput = function( value ) {
  if (value === "END") {
    this.sendCompleted();
  }
  else {
    var n = Number( value );
    if (isNaN( n )) {
      this.sendError( 'Not a number' );
    }
    else {
      this.sendNext( n );
    }
  }
}

2.2.3 Creating Observer types

HotDrink does not provide any base implementation of the Observer interface because there is no prescribed behavior for an observer. Simply write your type to include onNext, onError, and onCompleted. (They can be empty functions if needed.)

2.3 Proxy observers

2.3.1 The example

HTML

1: <div id="console3"></div>

JavaScript

 1: /*--------------------------------------------------------------------
 2:  * Observable instance and console
 3:  */
 4: 
 5: var observable = new NumberObservable();
 6: 
 7: var console = new HtmlConsole( document.getElementById( 'console3' ) );
 8: console.oninput = observable.processInput.bind( observable );
 9: 
10: /*--------------------------------------------------------------------
11:  * Proxy observers
12:  */
13: 
14: // Just an object; not an observer
15: var object = {
16:   id: 'Object',
17:   good: function( v ) { console.println( '[' + this.id + '] good: ' + v ); },
18:   bad:  function( e ) { console.println( '[' + this.id + '] bad:  ' + e ); },
19: };
20: 
21: // Create proxy observer for object
22: var proxy = new hd.ProxyObserver( object, object.good, object.bad, null );
23: 
24: // Subscribe proxy
25: observable.addObserver( proxy );
26: 
27: function warn() { console.println( "No more input!" ); }
28: 
29: // Create proxy for global function
30: observable.addObserver( new hd.ProxyObserver( null, null, null, warn ) );

Result

Notes

This example has the same behavior as past examples. What is interesting about this example is that it uses an object that does not implement the Observer interface as well as a global function by wrapping them in proxy observers.

2.3.2 Using ProxyObserver wrappers

Sometimes it may not be convenient to force your object to meet the exact interface of an observer. For such cases, HotDrink provides a special wrapper: the hd.ProxyObserver type. The constructor for ProxyObserver looks like so:

ProxyObserver( object, next_cb, error_cb, completed_cb )
initialize an observer that will invoke the given callback functions on the given object

A ProxyObserver is initialized by providing another object, along with the callback functions that should be invoked on the object when events occur. The ProxyObserver will implement the Observer interface by invoking the corresponding callback function with which you initialized it. In this excerpt, we create a proxy observer for an object by using its good method as onNext and is bad method as onError.

// Just an object; not an observer
var object = {
  id: 'Object',
  good: function( v ) { console.println( '[' + this.id + '] good: ' + v ); },
  bad:  function( e ) { console.println( '[' + this.id + '] bad:  ' + e ); },
};

// Create proxy observer for object
var proxy = new hd.ProxyObserver( object, object.good, object.bad, null );

// Subscribe proxy
observable.addObserver( proxy );

Note that any of the callback function parameters may be null, indicating that no function should be called. In the above excerpt, we use null to indicate that there is no function to be called for onCompleted. Furthermore, the object itself may be null, indicating that the callback functions should be executed as global functions instead of being invoked on an object. This can be seen in the next excerpt in which we use a proxy to call a global function as onCompleted.

function warn() { console.println( "No more input!" ); }

// Create proxy for global function
observable.addObserver( new hd.ProxyObserver( null, null, null, warn ) );

2.4 Observing multiple Observables

2.4.1 The example

HTML

1: <div>
2:   First console:
3:   <button id="happysad">Remove happy/sad</button>
4:   <button id="all">Remove all</button>
5: </div>
6: <div id="console4"></div>
7: <div style="margin-top:1em">Second console:</div>
8: <div id="console5"></div>

JavaScript

 1: /*--------------------------------------------------------------------
 2:  * Observables and consoles
 3:  */
 4: observableA = new NumberObservable();
 5: consoleA = new HtmlConsole( document.getElementById( 'console4' ) );
 6: consoleA.oninput = observableA.processInput.bind( observableA );
 7: 
 8: observableB = new NumberObservable();
 9: consoleB = new HtmlConsole( document.getElementById( 'console5' ) );
10: consoleB.oninput = observableB.processInput.bind( observableB );
11: 
12: /*--------------------------------------------------------------------
13:  * An observer with extra methods
14:  */
15: var observer = new NumberObserver( 'Multi-Observer', consoleA );
16: 
17: observer.good = function( v, console ) {
18:   console.println( '[' + this.id + '] good:  ' + v );
19: }
20: 
21: observer.bad = function( e, console ) {
22:   console.println( '[' + this.id + '] bad:   ' + e );
23: }
24: 
25: observer.done = function( console ) {
26:   console.println( '[' + this.id + '] done   ' );
27: }
28: 
29: observer.happy = function( v, console ) {
30:   console.println( '[' + this.id + '] happy: ' + v );
31: }
32: 
33: observer.sad = function( e, console ) {
34:   console.println( '[' + this.id + '] sad:   ' + e );
35: }
36: 
37: /*--------------------------------------------------------------------
38:  * Observer subscriptions
39:  */
40: 
41: // A regular (no-proxy) subscription to observableA
42: observableA.addObserver( observer );
43: 
44: // Subscribe proxy to obeservableA
45: observableA.addObserver(
46:   new hd.ProxyObserver( observer, observer.good, observer.bad, observer.done, consoleA )
47: );
48: 
49: // Subscribe proxy to observableB
50: observableB.addObserver(
51:   new hd.ProxyObserver( observer, observer.good, observer.bad, observer.done, consoleB )
52: );
53: 
54: // Subscribe another proxy to observableA -- and keep the proxy
55: var happysadproxy =
56:   observableA.addObserver( observer, observer.happy, observer.sad, null, consoleA );
57: 
58: // Remove just the one proxy
59: document.getElementById( 'happysad' ).onclick = function() {
60:   observableA.removeObserver( happysadproxy );
61: }
62: 
63: // Remove all subscriptions for the observer
64: document.getElementById( 'all' ).onclick = function() {
65:   observableA.removeObserver( observer );
66: }

Result

First console:
Second console:

2.4.2 Extra parameters for callbacks

What if your Observer needs a little extra information about the Observable it is responding to? Of course, every JavaScript programmer should know how to create closures — functions that capture elements of the environment in which they were defined. However, it can be more convenient to simply pass any extra information needed as a parameter to the function: then you can reuse the same function many times instead of creating distinct closures for each one.

The ProxyObserver type supports this approach through an overloaded constructor.

ProxyObserver( object, next_cb, error_cb, completed_cb, tag )
initialize an observer that will invoke the given callback functions on the given object, also passing the given tag

Whatever value you provide as the tag will be passed to all three callbacks as an additional parameter. Notice how, in the example, we wrote callbacks that took the console to which output was to be written as a parameter.

observer.good = function( v, console ) {
  console.println( '[' + this.id + '] good:  ' + v );
}

observer.bad = function( e, console ) {
  console.println( '[' + this.id + '] bad:   ' + e );
}

observer.done = function( console ) {
  console.println( '[' + this.id + '] done   ' );
}

Then, when we created our proxy observers, we passed the console to be used as the tag.

// Subscribe proxy to obeservableA
observableA.addObserver(
  new hd.ProxyObserver( observer, observer.good, observer.bad, observer.done, consoleA )
);

// Subscribe proxy to observableB
observableB.addObserver(
  new hd.ProxyObserver( observer, observer.good, observer.bad, observer.done, consoleB )
);

In this way, we were able to subscribe our observer to two different Observables, and have it react differently to each one.

2.4.3 BasicObservable shortcuts for ProxyObserver

The BasicObservable type was written with the ProxyObserver type in mind. This can be seen in two ways. First, the addObserver function has an overloaded version which will create a ProxyObserver for you.

addObserver( object, next_cb, error_cb, completed_cb, tag )
creates a ProxyObserver using the given object and callback functions, then subscribes that proxy to this observer

The addObserver function in a BasicObservable has a return value: the object which was subscribed. When you pass an Observer, you simply get the same Observer back; but, when you pass the parameters to a ProxyObserver, you get back the ProxyObserver it created for you. You may use this later if you want to unsubscribe. The following excerpt uses addObserver to implicitly create a ProxyObserver, and stores the observer in a variable to use later.

// Subscribe another proxy to observableA -- and keep the proxy
var happysadproxy =
  observableA.addObserver( observer, observer.happy, observer.sad, null, consoleA );

Note that you may subscribe the same object to the same observable multiple times by creating a separate proxy for each subscription. This is not ambiguous: each subscription is represented by a distinct object. You can cancel a single subscription by passing removeObserver the proxy for the subscription. You can see this with the button that removes just the proxy that uses happy and sad functions as callbacks.

// Remove just the one proxy
document.getElementById( 'happysad' ).onclick = function() {
  observableA.removeObserver( happysadproxy );
}

However, the second way in which BasicObservable reacts specially to ProxyObserver objects is this: you may pass any object to the removeObserver function, and it will unsubscribe any ~ProxyObserver~s which target that object. Note that if you have multiple proxies targeting the same object, this will unsubscribe all of them. You can see this with the button that removes all callbacks for the observer.

// Remove all subscriptions for the observer
document.getElementById( 'all' ).onclick = function() {
  observableA.removeObserver( observer );
}

Note that clicking this button removes every possible subscription the object has to the observable: any subscriptions represented by proxies, as well as the subscription in which the object itself is the observer.

3 Extensions

3.1 Basic extensions

3.1.1 The example

View

1: <div id="extension-example">
2:   <input type="text" data-bind="hd.edit( x, null, digits() )"/>
3:   <span data-bind="hd.text( x )"></span>
4: </div>

View-Model

1: var model = new hd.ModelBuilder().v( 'x', '0' ).end();

Binding

 1: // Constructor: nothing to do
 2: function DigitsOnly() { }
 3: 
 4: // Use Extension as prototype
 5: DigitsOnly.prototype = Object.create( hd.Extension.prototype );
 6: 
 7: // Override onNext to remove non-digit characters
 8: DigitsOnly.prototype.onNext = function( value ) {
 9:   this.sendNext( value.replace( /\D/g, '' ) );
10: }
11: 
12: // Factory function for DigitsOnly extension
13: digits = function digits() { return new DigitsOnly(); }
14: 
15: window.addEventListener( 'load', function() {
16:   hd.performDeclaredBindings( model, document.getElementById( 'extension-example' ) );
17: } )

Result

Notes

The extension we use for this edit box strips out any characters which are not digits from the input. Try entering "1a2b3c" and see the result.

3.1.2 Writing extensions

We described the basic idea behind extensions in the Advanced Binding Concepts document. Now it should be clear that an extension is simply an object which is both an Observer and an Observable: it takes a value from the previous observable, and passes it on to the next observer.

In order to make it easier to write your own extensions, we have provided the hd.Extension type. Extension is simply a BasicObservable which implements onNext using sendNext, onError using sendError, and onCompleted using sendCompleted. Thus, an Extension simply passes on the value it receives with no alteration. You can make your own extension by overriding the desired functions.

Here's an example of an extension that removes any non-digit characters from the string it receives.

// Constructor: nothing to do
function DigitsOnly() { }

// Use Extension as prototype
DigitsOnly.prototype = Object.create( hd.Extension.prototype );

// Override onNext to remove non-digit characters
DigitsOnly.prototype.onNext = function( value ) {
  this.sendNext( value.replace( /\D/g, '' ) );
}

Notice that we did not implement onError or onCompleted; the implementation we inherited from Extension (simply pass along the value received) was all we needed for those.

To use this extension in your binding, simply create a new DigitsOnly object and provide it in the appropriate spot. Here we use the extension in an edit binding.

<input type="text" data-bind="hd.edit( x, null, new DigitsOnly() )"/>

This binding will take whatever value the user enters, remove any non-digit characters, then translate the resulting string into a number.

As an alternative approach, you might create a factory function for your extension type. (This is what HotDrink does for its extensions.)

// Factory function for DigitsOnly extension
digits = function digits() { return new DigitsOnly(); }

This simply makes the resulting binding expression a little bit easier to read.

<input type="text" data-bind="hd.edit( x, null, digits() )"/>

3.1.3 Review: the hd.fn extension

Remember that, for simple translations like this, really all you need is the hd.fn extension: just create the function that translates the value, and pass it to hd.fn to get an extension. You can still create factory functions to make your binding specifications short and easy to read. For example, here's how to rewrite the digits extension using the hd.fn extension.

function digitsOnly( value ) {
  return value.replace( /\D/g, '' );
}

function digits() {
  return hd.fn( digitsOnly );
}

3.2 Fancier extensions

3.2.1 The example

View

1: <div id="extension-example-2">
2:   <style type="text/css" scoped>
3:     .error { color: #900; }
4:   </style>
5:   <span class="error" data-bind="hd.text( x.error )"></span><br/>
6:   <input type="text" data-bind="hd.editVar( x, null, [hd.toNum(), new DelayErrors()] )"/>
7:   <span data-bind="hd.text( x )"></span>
8: </div>

View-Model

1: var model = new hd.ModelBuilder().v( 'x', '0' ).end();

Binding

 1: /*--------------------------------------------------------------------
 2:  * Extension type
 3:  */
 4: 
 5: // Constructor
 6: function DelayErrors() {
 7:   this.timeout = null;
 8: }
 9: 
10: // Inheritance
11: DelayErrors.prototype = Object.create( hd.Extension.prototype );
12: 
13: // Good value clears any delayed errors
14: DelayErrors.prototype.onNext = function( value ) {
15:   if (this.timeout) {
16:     window.clearTimeout( this.timeout );
17:     this.timeout = null;
18:   }
19:   this.sendNext( value );
20: }
21: 
22: // Delay error messages three seconds
23: DelayErrors.prototype.onError = function( error ) {
24:   if (this.timeout) {
25:     window.clearTimeout( this.timeout );
26:   }
27:   this.timeout = window.setTimeout( this.sendError.bind( this, error ), 3000 );
28: }
29: 
30: // Completed clears any delayed errors
31: DelayErrors.prototype.onCompleted = function() {
32:   if (this.timeout) {
33:     window.clearTimeout( this.timeout );
34:     this.timeout = null;
35:   }
36:   this.sendCompleted();
37: }
38: 
39: /*------------------------------------------------------------------*/
40: 
41: window.addEventListener( 'load', function() {
42:   hd.performDeclaredBindings( model, document.getElementById( 'extension-example-2' ) );
43: } )

Result


Notes

This extension delays any error messages for three seconds. Notice how long it takes to get error messages (for invalid numbers) compared to the propagation of good values.

3.2.2 Extension possibilities

By writing your own extension types you can implement functionality not possible with the hd.fn extension. For example, by overriding the onError function, you can write extensions that interact with errors. Or, by choosing when to call sendNext, you can make extensions which delay or even cancel certain values.

To illustrate this, the example above defines an extension which delays error values for two seconds. If the user produces another value within that time period, it never sends the error.

Note that it is important that your extension not block or take an excessively long time to complete. However extensions are well suited for asynchronous programming: you do not have to send a value right away; you may schedule an operation that will result in sending a value later. (In our example we simply use setTimeout to send the value later.) Whenever you do send the value, it will simply continue on its way.

4 View adapters

4.1 The example

View

 1: <div id="adapter-example">
 2: <div id="slider" data-bind="{mkview: Slider, model: x}"></div>
 3: <p>Value:
 4:   <input type="text" data-bind="hd.num( x, 0, null, hd.fn( restrict, 0, 100 ) )"/>
 5: </p>
 6: </div>
 7: <script type="text/javascript">
 8: // Using jQuery-UI to create a slider
 9: $(function() { $("#slider").slider( {min: 0, max: 100, step: 1} ); });
10: </script>

View-Model

1: var model = new hd.ModelBuilder().v( "x", 50 ).end();

Binding

 1: /*--------------------------------------------------------------------
 2:  * Define the object that represents the slider.  Must be Observable
 3:  * and Observer.
 4:  */
 5: 
 6: // Constructor - register callback for slider change
 7: function Slider( el ) {
 8:   this.el = el;
 9:   $(el).on( "slide", this.onSlide.bind( this ) );
10: }
11: 
12: // Inheritance
13: Slider.prototype = Object.create( hd.BasicObservable.prototype );
14: 
15: // When slider changes
16: Slider.prototype.onSlide = function( event, ui ) {
17:     if (ui.value != this.last) {
18:       this.sendNext( ui.value )
19:     }
20: }
21: 
22: // When variable changes
23: Slider.prototype.onNext = function( value ) {
24:   this.last = value;
25:     if ($(this.el).slider( "value" ) != value) {
26:       $(this.el).slider( "value", value );
27:     }
28: }
29: 
30: // Needed by Observer
31: Slider.prototype.onError = function( error ) { }
32: 
33: // Neede by Observer
34: Slider.prototype.onCompleted = function() { }
35: 
36: /*--------------------------------------------------------------------
37:  * Translation function for extension
38:  */
39: function restrict( from, to, n ) {
40:   if (n < from) { return from; }
41:   if (n > to) { return to; }
42:   return n;
43: }
44: 
45: // Schedule the binder
46: window.addEventListener( 'load', function() {
47:   hd.performDeclaredBindings( model, document.getElementById( 'adapter-example' ) )
48: } );

Result

Value:

Notes

Here, we bind a slider and a text input to the same variable. Notice that changing the slider changes the text box, and vice-versa.

4.2 Writing adapters

We first described view adapters in the Advanced Binding Concepts document. It should be clear now that an adapter is an object which implements the Observable and/or Observer interfaces. If your view element produces values which should be used to update the variable, it should be an Observable. If your view element accepts updates from the variable, it should be an Observer.

In the example above we define an adapter for a jQuery UI slider widget. We want the slider to be used to update the variable; therefore we make the adapter Observable by using the BasicObservable type as a prototype and calling sendNext every time the slider's value changes.

We also want the slider to change every time the variable changes; therefore we make the adapter an Observer by implementing onNext, onError, and onCompleted. The onNext function updates the slider with the new value; the other two functions have no effect.

4.3 Beware of cycles

Note that every time your view adapter produces a new value with which to update the variable, the variable will respond by producing a new value with which to update the view. This is an intentional design decision in order to allow the value to pass through the translators defined for the binding — it may very well be that the value your adapter produces is not the same value that the variable is updated with.

However, this design decision does present a possibility for an infinite loop in which the view and the view-model continually update one another. This is especially a danger if updating the view causes the view to generate update events, as is the case with the jQuery UI slider. In the case of the slider, a cycle would go like this:

  1. The user moves the slider to a new position — say, 25.
  2. The slider reports that its position has been changed.
  3. The view adapter uses sendNext to report the new position.
  4. The variable is updated to 25.
  5. The variable calls the adapter's onNext to report that the variable has, in fact, been set to 25.
  6. The adapter sets the slider's position to 25.
  7. The slider reports that its position has been changed.
  8. The adapter uses sendNext to report the new position.
  9. And so on, and so forth…

To avoid this cycle, we add a couple of safety checks. The first is a check in onNext to avoid setting the slider to the same value it currently has.

  if ($(this.el).slider( "value" ) != value) {
    $(this.el).slider( "value", value );
  }

The second is a check in onSlide to avoid setting the variable to the same value it currently has.

  if (ui.value != this.last) {
    this.sendNext( ui.value )
  }

Either one of these checks is sufficient to break the cycle; however, together they ensure that we will never do unnecessary work by setting something to a value which it already has.

4.4 Side note: a range enforcing translator

Finally, we draw attention to one other important detail in this example. We created a translator function to ensure that the value entered in the text box will always be betwen zero and one hundred: the valid range of the slider. If the value lies outside the valid range, it changes it either to the minimum of the range (if it is too low) or the maximum of the range (if it is too high). This enforces an invariant on the variable x: it is a number between zero and one hundred.

Note that we wrote this function so that the range it enforces is passed as parameters to the function, making it easy to reuse as needed.

Created: 2015-10-29 Thu 10:02