Click here to Skip to main content
14,330,663 members

Reactive Web Applications with Tridash

Rate this:
4.00 (1 vote)
Please Sign up or sign in to vote.
4.00 (1 vote)
27 Sep 2019CPOL
Implementation of a simple reactive web application with Tridash - a very simple specification and gradually adding features to improve its functionality

In this post, we'll be implementing a simple reactive web application with Tridash. We'll start off with a very simple specification and gradually add features to improve its functionality.

This post is not a full in-depth introduction to the Tridash language. For a detailed introduction to its syntax and core concepts, check out the Tutorials.

Visit the homepage for download links and installation instructions.

Tridash: A Brief Introduction

In Tridash, an application is composed of a set of computational components, called nodes. Each node has a value which is a function of the values of one or more dependency nodes. Whenever the value of at least one of the dependency nodes changes, the node's value is recomputed. This relation between a node and its dependency nodes is referred to as a binding.

Bindings can be established explicitly, with the -> operator, in which the left hand side specifies the expression, of the dependency nodes, for computing the value of the node on the right hand side. The left hand side is referred to as the source of the binding and the right-hand side as the target of the binding.

Examples

The following establishes a simple binding in which node b is set to the value of node a:

a -> b

The following establishes a binding in which node c is set to the sum of the values of nodes a and b:

a + b -> c

Whenever a or b changes, a + b is reevaluated and the value of c is recomputed.

Budgeting Application

We'll implement a simple budgeting application. In the first iteration, we'd like the functionality to enter an amount of money allocated to a number of predefined expense categories. The total should be computed after the amounts are entered and recomputed whenever the amounts are changed. This will allows us to see whether the total amount allocated exceeds the budget.

Let's start off with the user-interface. We have three predefined expense categories:

  • Food
  • Electricity
  • Water

Create a simple HTML file app-ui.html containing the usual HTML boilerplate. Add three input elements, where the amounts allocated to the three expense categories are entered and a fourth readonly input element where the total is displayed.

<!doctype html>
<html>
  <head>
    <title>Budget App</title>
  </head>

  <body>
    <div>
      <label for="food">Food:</label>
      <div><input id="food"/></div>
    </div>
    <div>
      <label for="water">Water:</label>
      <div><input id="water"/></div>
    </div>
    <div>
      <label for="electricity">Electricity:</label>
      <div><input id="electricity"/></div>
    </div>
    <hr>
    <div>
      <label>Total:</label>
      <div><input id="total" readonly/></div>
    </div>
  </body>
</html>

Now we have a nice interface which doesn't actually do anything. This is where Tridash comes into the picture.

What we require is that the sum of the values, entered in the input elements, is computed and displayed in the total field. Additionally, we require that the sum is recomputed and that the new total is displayed.

If we were to implement this behaviour in raw JavaScript, we'd first have to write the initialization code which retrieves references to the input elements, making sure that it is run at the correct moment in time, that is after the document has been loaded. An event listener has to be attached to each input element, which is called whenever the element's value changes. In the event listener callback function, each input's value has to be converted to a number after which the total sum is computed. The value displayed in the total input element has to be manually updated to the new sum.

All in all, this would require something similar to the following:

var input_food = document.getElementById("food");
var input_water = document.getElementById("water");
var input_electricity = document.getElementById("electricity");
var input_total = document.getElementById("total");

function update_total() {
    var food = parseFloat(input_food.value);
    var water = parseFloat(input_water.value);
    var electricity = parseFloat(input_electricity.value);

    input_total.value = food + water + electricity;
}

input_food.addEventListener("change", update_total);
input_water.addEventListener("change", update_total);
input_electricity.addEventListener("change", update_total);

While this may not seem like much, the only part which actually implements our application logic is food + water + electricity. The rest is boilerplate which distracts from the core application logic. Event listeners for changes in the state of each of the UI elements have to be manually setup and the application state has to be manually updated (the computation of the new total) and manually synchronized across the remaining components of the application (the total input element which displays the output total). This leaves a lot of room for bugs. Furthermore, this code does not perform any error checking or handling of any sort. If you enter a non-numeric value in any of the input elements, NaN is displayed as the total. The bigger issue, however, is that this code is inflexible to changes in the application logic.

With Tridash, this can be implemented in a single line of code:

food + electricity + water -> total

This Tridash declaration results in the creation of the nodes food, electricity and water which serve as the inputs. The node total is bound to the sum of the values of the nodes food, electricity and water. total is said to be bound to the sum, rather than simply being set to the sum, as its value is automatically recomputed whenever the value of one of food, electricity and water changes. It is this automatic recomputation of the values of nodes, which forms the core of the Tridash language.

Whilst that implements our application logic, we still need to bind the nodes food, electricity and water to the values entered in the input elements. It turns out, we can do that simply by adding a piece of Tridash code in each input element's value attribute, but first let's take care of converting the string values to numbers.

The to-real function, referred to as a meta-node in Tridash nomenclature, converts its argument to a real (floating point) number. For now, we can simply replace food with to-real(food), and do the same for electricity and water, in the expression, which computes the total sum.

to-real(food) + to-real(electricity) + to-real(water) -> total

Now to bind the node food to the value of the food input element, we can simply set its value attribute to the Tridash node food with the following:

<input id="food" value="<?@ food ?>"/>

The <?@ ... ?> tag establishes a binding between the value of an HTML element's attribute and a Tridash node.

We can repeat this procedure for the water, electricity and total fields.

To put it all together, we simply need to add the code, which implements our core application logic, to the HTML file within a Tridash code tag <? ... ?>.

The <? ... ?> tag allows Tridash code to be inserted directly in the HTML file.

The following is the complete application:

<?
:import(core)

to-real(food) + to-real(electricity) + to-real(water) -> total
?>
<!doctype html>
<html>
  <head>
    <title>Budget App</title>
  </head>

  <body>
    <div>
      <label>
      Food:
      <input id="food" value="<?@ food ?>"/>
      </label>
    </div>
    <div>
      <label>
      Water:
      <input id="water" value="<?@ water ?>"/>
      </label>
    </div>
    <div>
      <label>
      Electricity:
      <input id="electricity" value="<?@ electricity ?>"/>
      </label>
    </div>
    <hr>
    <div>
      <label>
      Total:
      <input id="total" value="<?@ total ?>" readonly/>
      </label>
    </div>
  </body>
</html>

The :import(core) line at the top of the file imports the core module which contains the to-real and + functions.

Build the application with the following command:

tridashc app-ui.html : node-name=ui -o app.html -p type=html -p main-ui=ui

See the Building section of Tutorial 1 for an explanation of the command.

Open the output app.html file in a web browser and enter some values in the input fields.

Snapshot of application with entered values: 10,20,30 and total 60.

Enter some values in the input fields. Notice how the total is automatically computed. Now try changing some of the values and you'll notice that the total is automatically recomputed.

This is an improvement over the JavaScript version in many ways:

  • We've eliminated the boilerplate in establishing event listeners and manually updating the application state, which involves recomputing the total and updating the value of the total field. Instead, we've specified our application state in a declarative manner, and let the language take care of updating the application state in response to user events.

  • You don't get NaN as a result if an invalid value is entered, though we'll add proper error handling later.

  • It's clear which part of the code is responsible for implementing the application logic as it is not buried in event listener callback functions. As a result, the application logic can be easily changed or extended when the specification changes.

In order to convince you of the last point, let's say we'd also like to display a message with the total underneath the total input element. To do so, we can simply add the following to the HTML file after the total input element:

You have a total of <?@ total ?>!

<?@ total ?> is automatically be replaced with the value of the total node and automatically updated whenever the node's value changes. The value of the total node is itself recomputed whenever the values, entered in the input elements, change.

Improvements

Believe it or not, this application can be made even more succinct. The to-real function is special in that if it occurs as the target of a binding, a number is parsed from the value of the source node, not the argument. The argument node is, instead, set to the parsed real value. This allows us to place the conversion logic directly in the value attributes of the input elements. As an example value="<?@ food ?>" can be replaced with value="<?@ to-real(food) ?>".

Thus the code implementing our main application logic can be simplified to:

food + electricity + water -> total

If you really want to be succinct, the total node can be removed entirely and value="<?@ total ?>" can be replaced with the sum expression directly:

<input id="total" value="<?@ food + electricity + water ?>"/>

However, we'll keep the total node as it will simplify the implementation of the other features of our application.

Budget Application version 2.0

The application we've implemented is quite rudimentary. We have to keep the budget in mind and manually check whether the resulting total exceeds the budget. It would be nice if we could enter the budget and have the application tell us whether it was exceeded.

It was promised that Tridash allows the application logic to be easily modified and extended. Now we'll prove it.

As in the first version, let's begin with the UI. We need a new input element for entering the budget. Let's add it above the first expense field. While we're at it, let's bind its value, converted to a real number, to a node called budget.

<div>
  <label for="budget">Budget:</label>
  <div><input id="budget" value="<?@ to-real(budget) ?>"/></div>
</div>

Now we need to display a message which tells use whether the total is within the budget or the budget was exceeded. We can do this using the case macro.

The arguments to the case macro are of the form condition : value in which condition is the condition expression and value is the value expression. The case expression evaluates to the value of the first argument for which the corresponding condition evaluates to true. The last argument may simply be of the form value, in which case it becomes the default value, to which the case expression evaluates, if the conditions of the preceding arguments evaluate to false.

Example

case(
  a > b : 1,
  a = b : 0,
  -1
)

The above evaluates to 1 if a is greater than b, 0 if a is equal to b and -1 otherwise.

We need the message "Within Budget" to be displayed if total is less than budget and the message "Budget Exceeded!!!" to be displayed otherwise. This is implemented by the following case expression:

case(
  total < budget : "Within Budget",
  "Budget Exceeded!!!"
)

Now how do we actually display the message? Simple, we just place this declaration inline, within a <?@ ... ?> tag somewhere in the HTML file.

The following is the complete code for version 2.0 of the budgeting application:

<?
 :import(core)

 food + electricity + water -> total
?>
<!doctype html>
<html>
  <head>
    <title>Budget App</title>
  </head>

  <body>
    <div>
      <label for="budget">Budget:</label>
      <div><input id="budget" value="<?@ to-real(budget) ?>"/></div>
    </div>
    <hr>
    <div>
      <label for="food">Food:</label>
      <div><input id="food" value="<?@ to-real(food) ?>"/></div>
    </div>
    <div>
      <label for="water">Water:</label>
      <div><input id="water" value="<?@ to-real(water) ?>"/></div>
    </div>
    <div>
      <label for="electricity">Electricity:</label>
      <div><input id="electricity" value="<?@ to-real(electricity) ?>"/></div>
    </div>
    <hr>
    <div>
      <label>Total:</label>
      <div><input id="total" value="<?@ total ?>" readonly/></div>
    </div>

    <p>
      <?@
       case(
           total < budget : "Within Budget",
           "Budget Exceeded!!!"
       )
       ?>
    </p>
  </body>
</html>

Let's try it out entering some values such that the total is within the budget:

Snapshot with total within budget.

"Within Budget" is displayed as we wanted.

Now change the amounts such that the total exceeds the budget:

Snapshot with total exceeding budget.

The message automatically changes to "Budget Exceeded!!!". This demonstrates the reactive nature of Tridash.

That's all we had to do to implement the improvements to our application logic.

Note: The node total can be replaced directly with the expression food + electricity + water. It makes no difference in efficiency as the expression is only computed once when it is first used.

Adding Color

We have a basic working application, which informs us whether the total amount allocated exceeds the budget, however it can do with some improvements. It would be better if there is a visual indication, in the form of a color change, for whether the budget was exceeded or not rather than having to read out the message in full.

What we want is the background, of the total input field, to turn red when the budget is exceeded and turn green if the total amount is within the budget. In other words, the background color of the total input element should be bound to red if the budget is exceeded and bound to green if the total is within the budget.

HTML elements can be referenced from Tridash using self.<id>, where <id> is replaced with the HTML ID of the element. The . operator references a subnode of a node, where the node is on the left-hand side and the subnode identifier on the right hand side. Attributes of the element can be referenced as subnodes of the HTML element node, with the style attributes as subnodes of the style subnode.

Thus we can implement the desired behaviour with the following:

case(total < budget : "green", "red") -> self.total.style.backgroundColor

self.total references the total input element, which has its id set to total. style is the identifier of the style subnode under which all style attributes of the element are grouped. Finally, backgroundColor is the identifier of the style attribute which controls the element's background color.

Additionally, the text should be changed to white in order for it to be legible. We can do using an inline style attribute in the HTML element or using CSS classes, however we can also bind the element's color style attribute to the constant "white", in Tridash. This is useful if later on, we'd like the text color to also change dynamically.

"white" -> self.total.style.color

These two Tridash declarations can be added to the <? ... ?> tag at the top of the file.

Snapshot: within budget, green total.

The background color of the total input element is green when the total is within the budget.

Budget exceeded, red total.

When the amounts are changed such that the budget is exceeded, the background color changes to red, which provides an immediate visual cue that the budget has been exceeded.

Error Handling

It was mentioned at the beginning of the article that you get a form of error handling for free. If a non-numeric value is entered in one of the input elements or some of the elements are left empty, the total remains unchanged. This is a step up from getting a total of NaN as the result, however there is no indication to the user that something went wrong.

An error message needs to be printed, next to the field containing the invalid numeric value. The message should prompt the user to correct the value. Corrected.

Failures

Failures are a special type of value that, when evaluated by a function, the evaluation of the function is aborted and the failure value is returned. They can be used to represent the absence of a value or the failure of an operation. In this case the to-realmeta-node returns a failure if a number could not be parsed from the string value. When the failure value is evaluated, by the + function in the computation of the total, the evaluation of the expression is aborted and the total node is set to the failure value.

We can check whether a node evaluates to a failure value using the fails? meta-node. Thus we can, for example, determine whether an invalid numeric value was entered in the food input element, with the expression fails?(food).

With the case macro, we can construct a simple expression which evaluates to the error message, if a non-numeric value was entered, and evaluates to the empty string otherwise:

case(
    fails?(food) : "Please enter a valid number!",
    ""
)

Since we'll be repeating the same expression for the budget, food, waterand electricity nodes, we can extract the expression in a function of our own.

error-prompt(value) : {
    case(
        fails?(value) : "Please enter a valid number!",
        ""
    )
}

Functions are defined with the : operator. The left-hand side consists of the function name followed by the list of argument nodes, to which the function arguments are bound, in parenthesis. The right-hand side contains the declarations forming the body of the function. The value of the last expression in the body becomes the return value of the function.

With this function, we can get the error prompt for a particular field, say food, with the expression: error-prompt(food). Thus to add the error messages to our interface, we simply add an error-prompt expression next to each input field.

<div>
  <label for="budget">Budget:</label>
  <div>
    <input id="budget" value="<?@ to-real(budget) ?>"/>
    <?@ error-prompt(budget) ?>
  </div>
</div>
<hr>
<div>
  <label for="food">Food:</label>
  <div>
    <input id="food" value="<?@ to-real(food) ?>"/>
    <?@ error-prompt(food) ?>
  </div>
</div>
<div>
  <label for="water">Water:</label>
  <div>
    <input id="water" value="<?@ to-real(water) ?>"/>
    <?@ error-prompt(water) ?>
  </div>
</div>
<div>
  <label for="electricity">Electricity:</label>
  <div>
    <input id="electricity" value="<?@ to-real(electricity) ?>"/>
    <?@ error-prompt(electricity) ?>
  </div>
</div>
Invalid value entered for water, prompt displayed next to it.

The error message is displayed next to the field with the invalid value, and the total is not computed.

With this small addition, the user is now clearly informed of when an invalid number has been entered in the text input fields. However, this does not stop the user from entering valid numbers which are do not make sense in the context of the application, such as negative numbers.

We'll create a new function which parses a real number from a string value and ensures that the real number is greater than or equal to 0. To keep the current code, for displaying the error messages, unchanged, we require this function to return the number if it is greater than or equal to 0, and return a failure if the number could not be parsed or is less than 0. This can be achieved using conditional bindings.

A conditional binding is a binding which is only active if the value of another node, the condition node, is true. If the value of the condition node is false, the target node of the binding evaluates to a failure value rather than evaluating to the value of the source node.

Example:

A conditional binding in which a node y evaluates to the value of x only if x >= 0, is established with the following:

x >= 0 -> (x -> y)

Notice, in this expression, that the binding x -> y is itself treated as a node which is the target of a binding. A binding becomes a conditional binding whenever the binding, itself, is a target of a binding, with the source node becoming the condition node.

Our function can thus be implemented with the following code:

check-valid(str) : {
    num <- to-real(str)
    num >= 0 -> (num -> self)
}

<- is the same as -> however the arguments are reversed, with the target on the left-hand side and the source on the right-hand side.

self is a special node, occurring inside functions. If a binding to self is established in the body of a function, the function returns the value of the self node rather than the value of the last expression in its body.

The only remaining task is to incorporate this function in our application. We can of course surround each of the nodes, bound to the values of the expense input elements in check-valid(...), in the computation of the total, however we can do better.

Target-Nodes

Remember to-real is special in that if an expression of to-real appears as the target of a binding, the value of the source node is converted to a real number and the argument node is bound to it. It turns out to-real is not that special since we can do the same for our own functions. This is done by means of a target-node function. The target-node function is the function which is used to compute the value of the argument node, given the value of the source node, whenever an expression of the function appears as the target of a binding. For to-real, this is simply to-real itself. That is why the following two declarations are equivalent:

to-real(x) -> z
x -> to-real(z)

This is achieved by setting the target-node attribute of to-real to to-real:

:attribute(to-real, target-node, to-real)

Attributes are key-value pairs which control various properties of nodes. Attributes are set with the :attribute special operator, in which the first argument is the node, the second argument is the attribute identifier and the third argument is the attribute value.

To obtain the same behaviour for our check-valid function, we simply set its target-node attribute to check-valid.

:attribute(check-valid, target-node, check-valid)

As a result, all that is necessary to implement this new error handling logic, is to replace to-real(...) with check-valid(...) in the value attributes of the input elements.

Example:

<input id="food" value="<?@ check-valid(food) ?>"/>

We should also change the message, to inform the user that negative valued inputs are invalid:

"Please enter a valid integer \u{2265} 0!"

\u{2665} represents the unicode character with code 2265, which is the character ≥:

Negative value entered for water, prompt displayed next to it.

Final Version Full Source Code

<?
 :import(core)

 error-prompt(value) : {
     case(
         fails?(value) : "Please enter a valid number \u{2265} 0!",
         ""
     )
 }

 check-valid(str) : {
     num <- to-real(str)
     num >= 0 -> (num -> self)
 }

 :attribute(check-valid, target-node, check-valid)

 food + electricity + water -> total

 case(total < budget : "green", "red") -> self.total.style.backgroundColor
 "white" -> self.total.style.color
?>
<!doctype html>
<html>
  <head>
    <title>Budget App</title>
  </head>

  <body>
    <div>
      <label for="budget">Budget:</label>
      <div>
        <input id="budget" value="<?@ check-valid(budget) ?>"/>
        <?@ error-prompt(budget) ?>
      </div>
    </div>
    <hr>
    <div>
      <label for="food">Food:</label>
      <div>
        <input id="food" value="<?@ check-valid(food) ?>"/>
        <?@ error-prompt(food) ?>
      </div>
    </div>
    <div>
      <label for="water">Water:</label>
      <div>
        <input id="water" value="<?@ check-valid(water) ?>"/>
        <?@ error-prompt(water) ?>
      </div>
    </div>
    <div>
      <label for="electricity">Electricity:</label>
      <div>
        <input id="electricity" value="<?@ check-valid(electricity) ?>"/>
        <?@ error-prompt(electricity) ?>
      </div>
    </div>
    <hr>
    <div>
      <label>Total:</label>
      <div><input id="total" value="<?@ total ?>" readonly/></div>
    </div>

    <p>
      <?@
       case(
           total < budget : "Within Budget",
           "Budget Exceeded!!!"
       )
       ?>
    </p>
  </body>
</html>

Conclusion

This post shows how easily an application, developed with Tridash, can be amended to changes in the specification. The state management boilerplate has been eliminated allowing you to focus on the core logic of your application. The creation of a new application component, which depends on the state of an existing application component, such as the total sum in this application, is simply a matter of declaring the component and declaring its dependency on the previous component.

Specifying the application state in a declarative can lead to fewer bugs related to state management and more maintainable code.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --
Technical Blog
Posted 27 Sep 2019

Tagged as

Stats

1.7K views