Wow, we had to make so many fundamental changes to our code just to implement a minor change in the input accepted by the application. We had to:
input-a
and input-b
, for which we had to come up
with meaningful identifiers.
input-a
and input-b
rather than a
and b
.
input-a
and input-b
rather than a
and b
.
a
to validate(input-a)
and b
to validate(input-b)
.
This is contrary to “simply adding new UI elements” which was the case when we introduced error handling. We can do better.
Notice that a lot of the code we added was simply repetitive binding
boilerplate code, which is the same for both a
and b
. It would be
nice if we could somehow abstract it away and not have to write the
same code for both nodes. Luckily, there is a way.
Remember, from the second tutorial, that some meta-nodes, such as
to-int
, are special in that a two-way binding is established between
the meta-node instance and the argument node. This allows instances of
the meta-node to also appear as targets of bindings.
Refresher Example.
# The following a -> to-int(b) # Is equivalent to to-int(a) -> b
It turns out to-int
is not so special as we can do the same for our
own meta-nodes by setting the target-node
attribute.
Node attributes are simply key-value pairs associated with a node,
which control various compilation options. Attributes are set using
the special /attribute
operator:
/attribute
Operator Syntax.
/attribute(node, key, value)
This sets the attribute of node
with key key
to the value value
.
Examples.
# Set value of attribute `my-attribute` to 1 /attribute(a, my-attribute, 1) # Set value of attribute `akey` to literal symbol `raw-id` /attribute(b, "akey", raw-id)
|
Node attributes do not form part of a runtime node’s state. |
The target-node
attribute determines, when set, the meta-node which
is used as the binding function of the binding in the reverse
direction, from a meta-node instance to the meta-node arguments.
As an example, a meta-node f
with its target-node
attribute set to
g
results in the following:
Example.
/attribute(f, target-node, g) # The following a -> f(b) # Is equivalent to g(a) -> b
In the example above the target-node
attribute of f
is set to
g
. Thus the declaration f(b)
also results in the binding g(f(b))
-> b
being created.
The meta-node to-int
simply has its target-node
attribute set to
itself, which is why it performs the same function, when it appears as
the target of a binding, as when it appears as the source of a
binding.
The |
Our code can be simplified considerably by allowing a meta-node, which
performs the additional input validation, to be bound (as the target)
to the values in the input field. Let’s first write that meta-node,
called valid-int
which is responsible for converting an input string
to an integer and ensuring that the resulting integer is greater than
or equal to zero. In essence this meta-node combines to-int
, we’ll
use int
this time, and validate
.
Meta-node valid-int
.
valid-int(value) : { x <- int(value) validate(x) }
In order to allow the node to appear as the target of a binding, and
still perform the same function, let’s set its target-node
attribute
to itself:
/attribute(valid-int, target-node, valid-int)
Now we can bind the contents of the input fields directly to an
instance of the valid-int
meta-node. In-fact, we can place the
valid-int
instance directly in an inline node declaration.
Replace to-int(input-a)
with valid-int(a)
, and the same for b
,
in the input fields as follows:
<label>A: <input value="<?@ valid-int(a) ?>"/></label> <label>B: <input value="<?@ valid-int(b) ?>"/></label>
The nodes input-a
and input-b
can be removed, as well as the
following declarations:
a <- validate(input-a) b <- validate(input-b)
The initial values of 0
can once again be given to the nodes a
and
b
rather than input-a
and input-b
.
0 -> a 0 -> b
The following is the full content of the Tridash code tag.
/import(core) # Error Reporting error-message(value) : case( fail-type?(value, Invalid-Integer) : "Not a valid number!", fail-type?(value, Negative-Number) : "Number must be greater than or equal to 0!", "" ) # Input Validation Negative-Number <- &(Negative-Number) Negative-Number! <- fail(Negative-Number) validate(x) : case( x >= 0 : x, Negative-Number! ) valid-int(value) : { x <- int(value) validate(x) } /attribute(valid-int, target-node, valid-int) # Initial Values 0 -> a 0 -> b
Compared to the previous version, the only modifications are in the
error-message
meta-node, the inline bindings in the input fields and
the addition of the validate
and valid-int
meta-nodes along with
the Negative-Number
failure type. This version, however, did not
require the addition of new nodes or modifying the bindings comprising
the core application logic. Changing the input validation logic was
simply a matter of substituting to-int
with valid-int
in the
bindings to the input field values.