Evaluator

The benefits:

An Example

Below is the input doubler, a very simple example. Enter a number in the input field and press return or tab. The result will appear instantly.

To create this calculator, follow these steps:

  1. Link to the evaluator stylesheet
  2. Create the HTML input and output elements
  3. Implement the functionality

Step 1: Link the evaluator stylesheet to your HTML document:

Linking the evaluator stylesheet
<link rel="stylesheet" href="path/to/evaluator.css" />

This helps to style the input and output elements.

Step 2: Create the GUI Elements.

<div id="calculator-1">
<label for="n">Enter a number:</label>
<input id="n" type="number" 
	data-set="n" 
	data-member="value" 
	data-exec="update"/>
<label>n doubled = </label>
<span 
	data-get="nDoubled"
	data-member="innerText">
	</span>
</div>

It is recommended to put the calculator elements in a container and give it a document-wide unique id. Inside, we have an input for data input, and a span element for showing the results. Both are decorated with some data- attributes that provide the information required for binding.

For example, data-set="n" means hat this is a read-write element, bound to the variable n. The data-member="innerText" attribute specifies which element property will be updated by the Evaluator when the value of n changes.

The data-exec="update" binds a function to the bound variable. This function will be called by the Evaluator when n changes.

Similarly, the attributes data-get and data-member attribute create a read-only binding between the variable n and the innerText property of the span element.

Step 3: Implement the functionality. The following listing demonstrates the doubling calculator script:

<script type="module" name="calculator-1">
const calculatorScope = {
	update : function ( ) {
		evaluator.set( "nDoubled", 2 * this.n );
		}
	}
import { Evaluator } from "./evaluator-1.js" ;
const evaluator = new Evaluator ( { 
	container: "calculator-1" , 
	scope: calculatorScope 
	} );
</script>

The script must be of type "module" because the Evaluator component is a JavaScript module. The name is optional, but helps a reader to connect the script to the related HTML elements container.

TIP: The script can be placed in the div container, perhaps this is the better way to tie the GUI to the script.

The first instruction creates a local scope object for the Evaluator. This object will hold the variables, and this is the place where you define the functions used by the calculator. Specifically, the update function, referenced by the input element above. The implementation uses the Evaluator.set() method to assign the doubled value of n to the variable nDoubled.

Now the span element above has been bound to that variable, so if nDoubled is modified, the span.innerText property will be updated with the new value.

To complete the code, you have to include two more instructions. The Evaluator class object must be imported from the evaluator.js module, and an Evaluator instance must be created. The constructor receives two arguments in the options parameter, the name of the HTML container that holds the input and output elements, and a reference to the local scope object, calculatorScope.

That's it. Notice that we did not define any variable. They are created implicitly with the first assignment in the Evaluator.set() method. But of course you add variables to the calculatorScope and preset them to a specific value.

NOTE: You don't need to pass any arguments to the Evaluator constructor. In this case, the evaluator searches for HTML decorated with data-get and data-set attributes in the entire document, and expects the functions defined on the globalThis object, which is window in the case of HTML documents. But it should be avoided to pollute the global namespace with all sorts of names, and a HTML container prevents name conflicts if you have multiple calculators in the same document.

Some Tricks

Nested Function Calls

The attribute data-exec="pythagoras,area,angles" should be rewritten to something like data-exec="updateResults". And the function implementation forwards to the original functions:

Input elements
globalThis.updateResults = function ( ) {
	pythagoras( );
	area( );
	angles( );
	}

The benefit is that we now have a single place to edit if something has to be changed.

One-to-Many Bindings

Multiple objects can be bound to a single variable:

Binding objects to the same variable
<span id="e1" data-get="someVariable" data-member="innerText"></span>
<span id="e2" data-get="someVariable" data-member="innerText"></span>
<span id="e3" data-get="someVariable" data-member="innerText"></span>

Working with Scope

Evaluator is a JavaScript module and can only be imported into another JavaScript module:

Standard evaluator import
<script type="module" id="container">
let firstName, lastName;
import * as evaluator from "/path/to/evaluator-1.js" ;
</script>

Evaluator cannot see the variables declared in the container module as shown in the example above. By default, Evaluator looks for members of globalThis, which is Window in the case of HTML documents. If you call evaluator.set on an undefined variable, the variable will be created on the globalThis object.

But polluting the global namespace with all sorts for names is considered bad practice. To address this conflict, you can put your variables and functions in an object and pass that object reference to evaluator.initPage() method:

Passing a "scope container" to evaluator
<script type="module" id="container">
const myScope = {
	firstName : "" , 
	lastName : "" , 
	update : function ( ) { 
		firstName="John" ; 
		lastName="Smith" ; 
		age = 42 ;
	} ;
import * as evaluator from "/path/to/evaluator-1.js" ;
evaluator.initPage( { scope : myScope } );
</script>

Evaluator stores the scope reference and uses it to access variables and functions.

Initializing Scoped to a Container

By default, evaluator.initPage() uses document.querySelectorAll to find elements subject to processing. If only a subset of a document must be processed, use a container around the subset and pass a container references in the options argument to initPage():

Limiting element search to a container
evaluator.initPage( { container : document.getElementById( "container-id" ) } ) ;

Distribution and Integration

The module can be found in the files evaluator.js and evaluator.css.