Throughout these tutorials, we’ve glossed over two-way bindings without going into much detail of how they work, yet they were a vital component of every application as the bindings to the UI elements have all been two-way bindings.
Each node has a number of contexts which store information about how to compute the node’s value, i.e. what function to use and what dependencies are operands to the function. The active context of a node, at a given moment in time, is the context which is used to compute the node’s value. In general, a context is activated when the value of an operand node of the context changes. By default, a node context is created for each dependency of a node which was added by an explicit binding.
Example.
a -> x # Context created for dependency `a` b -> x # Context created for dependency `b` c -> x # Context created for dependency `c`
In this example node x
has three contexts one for each of its
dependency nodes, a
, b
and c
, to which it is bound explicitly.
An implicit binding between a meta-node instance and the meta-node arguments does not result in the creation of a context for each operand.
a + b
Nodes a
and b
are implicitly added as dependencies of a + b
however they are added as operands to the same context with the +
function.
The following application demonstrates how different contexts are activated, when the values of their operand nodes change.
ui.html
.
<? x -> node y -> node z -> node ?> <!doctype html> <html> <head> <title>Node Contexts</title> </head> <body> <div><label>X: <input value="<?@ x ?>"/></label></div> <div><label>Y: <input id="b" value="<?@ y ?>"/></label></div> <div><label>Z: <input value="<?@ z ?>"/></label></div> <hr/> <div><strong>Last value entered: <?@ node ?></strong></div> </body> </html>
This is a simple application consisting of three text input fields
bound to nodes x
, y
and z
. Nodes x
, y
and z
are each
explicitly bound to node
, the value of which is displayed below the
fields.
Let’s enter a value in each field and see what happens. Observe the value displayed below the fields after each change:
Notice that after each change, the value that was just entered is displayed.
Now let’s try changing the values of the fields which were edited previously:
In this case the value of the second field, Y, was changed to 10 and that value was immediately displayed below the fields.
The value of the field that was changed last is displayed. To understand why this is so, let’s examine the sequence of steps taken when a value is entered in the X field.
x
, which is bound to the value in the X field, is
updated.
x -> node
is activated
due to the value of x
being updated.
node
is updated to the value of x
.
Contexts make two-way bindings possible:
Example.
input1 -> a # Two-way binding a -> b; a -> b input2 -> b
![]() | |
The |
a
has two contexts corresponding to dependency nodes input1
and
b
(which is also an observer). b
has two contexts corresponding to
dependency nodes input2
and a
.
When input1
is changed, the contexts corresponding to the bindings
in the following direction are activated:
input1 -> a
a -> b
When input2
is changed, the contexts corresponding to the bindings
in the following direction are activated:
input2 -> b
b -> a
The context of a binding can be set explicitly to a named context,
using the @
operator from the core
module.
a -> b @ context-id
The binding a -> b
is established in the context, of b
, with
identifier context-id
.
When multiple bindings are established to the same explicit context, the observer node takes on the value of the first operand which does not evaluate to a failure. The operands are ordered by the order in which the explicit bindings are declared in the source file. If all the operands evaluate to failures, the node evaluates to the failure value of the last operand.
This is better explained with an example application:
ui.html
.
<? /import(core) x -> node @ context y -> node @ context z -> node @ context ?> <!doctype html> <html> <head> <title>Explicit Contexts</title> </head> <body> <div><label>X: <input value="<?@ to-int(x) ?>"/></label></div> <div><label>Y: <input value="<?@ to-int(y) ?>"/></label></div> <div><label>Z: <input value="<?@ to-int(z) ?>"/></label></div> <hr/> <div><strong>Value: <?@ node ?></strong></div> </body> </html>
This application is similar to the previous application except the
bindings from nodes x
, y
and z
, to node
are established in an
explicit context with identifier context
. Additionally the input
fields are bound to to-int
instances, of x
, y
and z
which
results in x
, y
and z
being bound to the values entered in the
fields converted to integers. If a non-integer value is entered in a
field, the corresponding node is bound to a failure value.
Let’s try it out. Enter some integer values in each of the fields:
The value entered in the first field, X, was displayed. Since a
valid integer was entered, node x
evaluates to the integer value
1. The binding x -> node
was established first, as the declaration
occurs first in the source file, and since x
does not evaluate to a
failure, node
takes on the value of x
. The values of y
and z
are ignored.
Now let’s change x
to a non-integer value:
The value entered in the second field, 2, is displayed. Since a
non-integer value was entered in the first field, x
evaluates to a
failure. node
thus takes on the value of the next dependency, bound
to the explicit context, which does not evaluate to a failure. The
dependency is y
which evaluates to the integer entered in the second
field, 2.
Let’s see what happens if we enter a non-integer value in the third field:
The displayed value is unchanged since the second dependency, node
y
, already evaluates to a value which is not a failure value. The
value of the third dependency z
, corresponding to the value entered
in the third field, is ignored, regardless of whether it evaluates to
a failure or not.
Explicit contexts are a useful tool for handling failures. In the previous application a failure originating from the first input field, was handled by taking the value of the node bound to the second field. Similarly a failure originating from the second input field is handled by taking the value entered in the third field.
The @
operator also allows a binding to be activated only if the
result of the previous binding(s), in the same context, is a failure
value with a given type. When the context identifier is of the form
when(context, type)
the binding is only activated if the result of
the previous binding(s) is a failure of type type
.
Example.
x -> node @ context y -> node @ when(context, Invalid-Integer) z -> node @ when(context, Negative-Number)
Three bindings to node
are established in the explicit context
context
.
node
is primarily bound to the value of x
if it does not evaluate
to a failure. If x
evaluates to a failure of type Invalid-Integer
,
node
is bound to the value of y
. If x
, or y
evaluate to a
failure of type Negative-Number
, then node
is bound to the value
of z
.
To try this out replace the binding declarations, in the application
from the previous section, with the declarations in the example
above. Also copy over the definition of the meta-nodes valid-int
,
validate
and the Negative-Number
failure type from
Section 8.3, “Target-Node for own Meta-Nodes”, into the Tridash code tag. Replace
to-int
with valid-int
in the inline node declarations within the
input field values.
Enter a non-integer value in the first field, and an integer value in the second and third fields:
The value of the second field is displayed, since node
is bound to
it when the value in the first field is not an integer.
Now change the value of the second field to a negative integer, or alternatively enter a negative integer value in the first field:
The value of the third field is displayed in both cases, even when the
value of the second field is a valid positive integer. This is due to
node
being bound to the value of the third field when either the
value of the first field or second field is a negative number.
![]() | |
a -> b @ when(context, type) can be rewritten as: a -> b @ context when type |
Whilst the error handling logic in the Adding Numbers application,
from Section 8, “Target Node Transforms”, is adequate and correct, the
definition of the error-message
meta-node, responsible for selecting
an appropriate error message, can be improved using explicit
contexts. The current definition repeatedly checks whether the failure
type of the value
argument is of a given type using the fail-type?
meta-node. This is repetitive and does not convey the intent that this
is error handling/reporting logic.
The error-message
meta-node returns:
value
argument does not evaluate to a
failure.
value
evaluates to a
failure of type Invalid-Integer
.
value
evaluates to a failure of type Negative-Number
.
We can re-implement this logic using bindings to the self
node with
explicit contexts.
The self
node should primarily be bound to the empty string, if the
value
argument does not evaluate to a failure. There is a handy
utility meta-node, !-
, in the core
module, which returns the value
of its second argument if the first argument does not evaluate to a
failure. If the first argument evaluates to a failure value, it is
returned. This meta-node is registered as an infix operator thus can
be placed between its arguments.
The primary binding can thus be written as follows:
value !- "" -> self @ context
If this binding results in a failure of type Invalid-Integer
, self
should be bound to the constant string “Not a valid number!”. This
is achieved with the following:
"Not a valid number!" -> self @ context when Invalid-Integer
Finally self
should be bound to “Number must be greater than or
equal to 0!”, if the previous bindings resulted in a failure of type
Negative-Number
.
"Number must be greater than or equal to 0!" -> self @ context when Negative-Number
Putting it all together we have the following definition of
error-message
re-implemented using explicit contexts:
New implementation of error-message
.
error-message(value) : { value !- "" -> self @ context "Not a valid number!" -> self @ context when Invalid-Integer "Number must be greater than or equal to 0!" -> self @ context when Negative-Number }
The advantage of this implementation is that it more explicitly
conveys the intent that this is error handling logic. As such it can
be optimized more effectively, e.g. if self
evaluates to a failure
of type Negative-Number
, the check for whether the failure type is
Invalid-Integer
can be skipped altogether.
An additional advantage of this implementation is that the third
binding is activated on failures of type Negative-Number
originating
both from the first and second bindings whereas the previous
implementation only handled failures originating in value
. In this
case it doesn’t make a difference as the second binding cannot result
in a failure of type Negative-Number
. However this does make a
difference, in more complex error handling logic, where the handling
of an error may itself result in a new error.
This implementation does, however, have a difference from the previous
implementation in that if value
evaluates to a failure of a type
other than Invalid-Integer
or Negative-Number
it returns a
failure, whereas the previous implementation returned the empty
string. In this application it doesn’t make a difference as the
arguments passed to error-message
do not evaluate to failures of
other types.
Coming up with a context identifier and typing it out repeatedly can become tiresome. The original reason for having an identifier for explicit contexts is to distinguish them from the remaining contexts which are implicitly created and to allow for multiple explicit contexts. However, there is usually only a single explicit context used for handling failures.
To shorten the syntax for binding to an explicit context, a default
identifier, such as _
can be given to all explicit contexts which
are used only for handling failures. Alternatively, the @
operator
can take a single argument, the node, in which case it is a shorthand
for the explicit context with identifier default
.
# The following: x -> y @ default # Is equivalent to: x -> @(y)
When the context identifier is of the form when(type)
, that is
omitting the context identifier and leaving only the failure type, the
explicit context with identifier default
is, once again, assumed.
# The following: x -> y @ default when type # Is equivalent to: x -> y @ when(type)