Programming Guide
Table of Contents
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
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:
- The user moves the slider to a new position — say, 25.
- The slider reports that its position has been changed.
- The view adapter uses
sendNext
to report the new position. - The variable is updated to 25.
- The variable calls the adapter's
onNext
to report that the variable has, in fact, been set to 25. - The adapter sets the slider's position to 25.
- The slider reports that its position has been changed.
- The adapter uses
sendNext
to report the new position. - 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.