Table Of Contents

JQuery Plugin - Calculator

Sometimes the fields of a form are part of a chain of a calculation. For example, when the price of an order position changes, its price including tax should be recalculated. Then the total price (including other additional costs) should also be updated. This means each input element (text, select box, ...) needs an onChange (or onBlur) function that recalculates the new results and triggers other (dependent) fields to update themselves.

Wouldn't it be better to write a formula and add it to a DOM element, and this one would be updated immediately when one of the formula's operands changes?

The Calculator plugin was designed to do that. A developer writes a formula and adds it to an input field or DOM element (say a table cell) and the plugin adds update trigger functions to each input field (or DOM element) that is used a an operand.

Example

var formulaString = '{{left}} * doSomethingMore({{right}})';
$('#result').addFormula(formulaString);
...

<input type="text" name="left">    {{left}}<br>
<input type="text" name="right">   {{right}}<br>
<hr>
<input type="text" id="result">

doSomethingMore(...) is a JavaScript function that returns a numeric value, like function doSomethingMore(v) { return 2 * v; }.

API

The methods .addFomula(), .aggregate() and .aggregateTo() return the this variable. Thus these methods are chainable.

.calculation([newCalcFunction])

This method adds a new calculation function to a (wrapped) DOM element or returns an array of calculations functions added to this DOM element.

The wrapper must contain only 1 (one) DOM element. If there are none or more than 1, no operation is performed.

If the method is called without a newCalcFunction parameter, the method returns an array of functions or undefined/null if no calculation function was set yet.

Normally this method is used internally by .addFomula() or .aggregate()/.aggregateTo() to add functions to an operand field.

The function passed as parameter normally takes all operands, calculates the result and sets it to the result field's value or result element's innerHTML. The function doesn't expect any parameter nor uses the this variable.

Properties of .calculation()

This function object is not just a method, it also holds the configuration of the plugin.

.addFormula(formulaString)

$('#result').addFormula('{{#a}} + {{#b}}')

This method generates a calculation function by interpreting the formulaString parameter and adds that function to each operand field.

Operands Within A Formula

The example above shows how a formula could look like. It contains two operands ({{#a}} and {{#b}}). What does "{{#a}}" and "{{#b}}" mean? It means that the element or input field with ID="a" ({{#a}}) is used as left-hand operand and the element or input field with ID="b" ({{#b}}) as the right-hand operand.

Internally this notation is called "accessor". An accessor could contain:

Accessors could also describe default values. Thus if an input field is empty, the default value (if set) is used. "{{a || 33}})" tells that if the input field (with id="a" or name="a") is empty, a value of 33 is used as operand.

Generating A Calculation Function

The plugin itself does not contain any formula parser. The idea was that JavaScript already has one: the operands are replace by some JavaScript code, then the eval() function of the JavaScript interpreter is used to generate the calculation function. The generated function does not call eval().

As described above the plugin uses JavaScript's build-in function eval() to generate a calculation function. This means a formula could contain calls to any JavaScript function that receives a numeric value and returns a numeric value, like Math.sqrt(x), Math.pow(2, x), ...

var formula1 = '{{left}} * Math.pow(2, {{right || 1}})";
var formula2 = '{{[name ^= "sum"]}} * 1.5';

.aggregate(jqAggregationFieldsOrSelector, [aggregatorFunction])

This method generates a so-called aggregation function and adds it to the input fields selected by the a JQuery selector. Internally this method uses .calculation().

$('#aggResult').aggregate('[name ^= "left"]', function(a, b) { ... });

This line adds an aggregation function to all input fields that match the selector "[name ^= "left"]". The generated function writes its result to the input field (or element) with id="aggResult".

aggregatorFunction

The 2nd parameter of .aggregate() is a function that will be invoked to calculate the result. If this parameter is omitted an exception is thrown.

An aggregatorFunction is called for each operand in the aggregation set. The function is called with tree parameters:

  1. the current result, null for the first time
  2. the next operand
  3. the number of operands

An example of an aggregator function that adds the operands could look like:

function add(currentResult, nextValue, ignoredNumberOfOperands)
{
   if (currentResult == null)
   {
     return nextValue;
   }
   
   return currentResult + nextValue;
}

See also Calculator Extension Plugin. There are some pre-defined aggregator like sum() and avg().

Now say there is an aggregation of the fields [ a, b, c, d, e, f, g ] collected by the selector myOperandsSelector, and these fields have the values [ 1, 2, 3, 4, 5, 6, 7 ] and the previously declared add function is set as an aggregator function:

$(myTargetSelector).aggregate(myOperandsSelector, add);

That add function will be called seven time (once per not-emtpy operand). The following code displays what happens:

add(null /* 1st call */,             1 /* a */) { return      1; }
add(     1,                          2 /* b */) { return  1 + 2; }
add(    (1 + 2),                     3 /* c */) { return  3 + 3; }
add(   ((1 + 2) + 3),                4 /* d */) { return  6 + 4; }
add(  (((1 + 2) + 3) + 4,            5 /* e */) { return 10 + 5; }
add( ((((1 + 2) + 3) + 4) + 5),      6 /* f */) { return 15 + 6; }
add((((((1 + 2) + 3) + 4) + 5) + 6), 7 /* g */) { return 21 + 7; }

Average Example

Another example for an aggregator function could look like the following. This function differs in the fact that it has some kind of state. This state is stored in local variables of the outer factory function that returns the real. aggregator function. createAvgAggregateFcn() creates an aggregator function that is passed to .aggregate().

function createAvgAggregateFcn()
{
   var ii = 0, sum = 0;
   return function(total, nextVal, ignoredNumberOfOperands)
   {
      if (!total)
      {
         ii = sum = 0;
      }
      
      return (sum += nextVal) / ++ii;
   };
}

$('#AVG').aggregate('[name ^= "avgop"]', createAvgAggregateFcn());

Consider that the returned function cannot be reused for other .aggregate() calls, because of the variables ii and sum.

.aggregateTo(jqTargetOrSelector, [aggregatorFunction])

This method is similar to .aggregate(). But this time the fields of the aggregation are determined by a JQuery query and the result is written to the field that is determined by the selector given as first parameter to .aggregateTo().

This method is also called with a 2nd parameter. This one is an aggregatorFunction as described above.

This method uses .aggregate().

Author's Comment

Calculation On The Client

Consider that all results calculated by .addFormula() or .aggregate() are made on the client for the client. These values are good for improving the user experience, but when sending the calculated values to the server they should not be trusted without checking!

The triggers for updating input fields or elements are made by using eval(), the created triggers themselves do not contain any eval()-calls (if it is not called explicitly within a formula). This means when creating formulae dynamicaly by using any values from a DB, file system, 3rd party Web Service or what ever, You should be aware that these values run through an eval()-call and should be escaped to avoid any cross-site-scripting issues.

Circular Dependencies

The plugin doesn't detect any cycles within the formulae or aggregations. Say there is a formula like s = a + b and an aggregation like a = agg(s, x, y). This would lead to an infinit loop. Thus it's up to the developer to take care of this.

There is another plugin that does similar things. Maybe You should also have a look at that one.

Licence

The code is free to use it and comes without any warranty.

If You like it or if You have any suggestions to improve it or if You have found any bug (I guess there're still a lot) let me know.

Demo

Project

JQuery Plugin - Calculator Extension

This plugin is based on Calculator Plugin and adds some convience functions and methods.

$.addFormulae(formulaSet, replacement, replaceValuesArray)

This is a wrapper function for .addFormula(). It expects one or three parameters:

  1. a set of formulae. The set is an object that's keys are the target selectors and the values are the formulae.
    var mySet = {
       '#result1' : '{{#a}} + Math.sqrt({{square}})',
       '#result2' : '{{#b || 42}} * {{#c}}',
       'square'   : '{{left}} * {{right}}'         
    }
  2. a so-called replacement. This is a patterns string used to find a substring in a formula and replace it by a value of the replaceValuesArray parameter.
  3. replaceValuesArray. This is an array of strings. Each string will be inserted into the formulae instead of the replacement.

Simple Example

var mySet = {
   'resultxxx' : '{{opAxxx}} * {{opBxxx}}',      
   'squarexxx' : '{{leftxxx}} * {{rightxxx}}'         
}

$.addFormulae(mySet, 'xxx', [ '1', '2', '3' ]);

is a short form for

$('result1').addFormula('{{opA1}} * {{opB1}}');
$('result2').addFormula('{{opA2}} * {{opB2}}');
$('result3').addFormula('{{opA3}} * {{opB3}}');

$('square1').addFormula('{{left1}} * {{right1}}');
$('square2').addFormula('{{left2}} * {{right2}}');
$('square3').addFormula('{{left3}} * {{right3}}');

$.withAggregatorFunctions

This function installs aggregation methods like sum or avg. This function is called with a string array. Each string is the name of a well-known aggregation function. The following names could be set.

Example

var fcnSet = $.withAggregatorFunctions(['sum', 'avg']);
$('#avgfield').avg('[name ^= "operands"]');
//OR:
$('#avgfield').aggregate('[name ^= "operands"]', fcnSet.avg);

JQuery Plugin - Formula Tracker

When adding formulae or aggregations to input fields (or elements) it is useful to know which input fields participate in the calculation of a result.

Say there are some fields (a, b, c) that are part of a sum aggregation (s). And the result itself is part of another calculation. When the form becomes a little more complex and the calculations are not that simple (as shown in the example above), than it could be hard to understand where the results come from and where do they go to then.

To help tracking which fields are a result's operands and what other fields or elements depend on these results, the Formula Tracker plugin was made.

The plugin does not have any (public) methods. Allthough it add the function .trackFormula() to $. This function is used when a formula or aggregation is added to a field/element.

To use this plugin it must be included.

<script type="text/javascript" src=".../jq-calc-formula-tracker.js"></script>

When double-clicking a field/element that is an operand or a result of a formula or an aggregation, a popup appears. When double-clicking again the popup disappears.

The popup consists of two or three columns.

When double-clicking one of the field names in the popup, this field's double-click trigger function is triggered. Thus that field's popup will be displayed. Or if it was already visible it will disappear.

Stylesheet Classes for Formula Tracker

By default the columns do not have any style. But they use css classes: