Apache » Cocoon »

  Cocoon Forms
      1.0
   homepage

Cocoon Forms 1.0

Calculated fields

Concept

A calculated field is a widget that calculated it's own value using an algorithm and values of other widgets. For example, if you are creating a form for a shopping cart, you'll probably have a repeater with items your customer is buying, each one with it's price, it's quantity, a subtotal and a grand total at the end of it. That total and subtotal fields are a typical use case of calculated fields.

Definition

Configuration example :

<fd:calculatedfield id="..." required="true|false">
  <fd:label>...</fd:label>
  <fd:datatype base="...">
    <fd:convertor type="..."/>
  </fd:datatype>
  <fd:value [type="..." eval="..." triggers="..."]/>
</fd:calculatedfield>

It also supports all the other configuration options of a common field, like listeners, attributes etc.. Nothing special is needed in form template, binding or XSLTs, just use a calculated field as you would use a common field.

As you can see, the special part is the fd:value element, it has the following standard attributes :

  • type indicates the type of algorith to use, currently simple, repeated, javascript and java are provided. If not specified, the simple expression algorithm will be used.
  • eval contains the expression to use.
  • triggers contains a comma separated list of widgets that will trigger a recalculation when their value changes.
No attribute is mandatory, while others may be required, depending on the algorithm that is used.The triggers attribute is not mandatory because many algorithms are able to understand which widget should trigger a recalculation parsing their expression, but actually the user can always specify this list manually to override the algorithm assumption.The calculations are done at server side. Whenever a trigger field gets its value changed by user interaction, the form is submitted, recalculated server side, and redisplayed to the user. Thus the usage of AJAX is highly recommended in a form that uses many calculated fields.

Algorithms

Four algorithms are provided in cocoon : simple expression (default), repeated expression, javascript and java.

Simple expression

The simple expression algorithm is the default one, and is based on XReporter expressions.

Only the eval attribute is required, because this is the default algorithm (so no type needed) and it can guess its triggers parsing the expression.

So you can simply write :

<fd:calculatedfield id="subTotal" state="output">
  <fd:label>Subtotal</fd:label>
  <fd:datatype base="double">
    <fd:convertor type="formatting" variant="currency"/>
  </fd:datatype>
  <fd:value eval="items * price"/>
</fd:calculatedfield>

And you'll obtain that this field will be calculated when the items or price fields change, multiplicating items with price.

As in asserts and other validations that uses XReporter expressions, widget names are evaluated relative to the container, so items and price are siblings of subTotal in the form definition tree, for example contained in the same repeater.

You can access widgets in other part of the form as well, using a path-link syntax and placing it in brackets, for example :

<fd:value eval="(subTotal / 100) * {/appliedVat}"/>

Could be the value of a calculated field for VAT, that uses the VAT value (which could change for foreign countries) taken from a field outside the repeater.

You can also use all the XReporter functions, for example :

<fd:calculatedfield id="dayOfweek" state="output">
  <fd:value eval="DayOfWeek(birthDay)"/>
  <fd:selection-list>
    <fd:item value="0"><fd:label>Sunday</fd:label></fd:item>
    ....
  </fd:selection-list>
</fd:calculatedfield>

Will use the DayOfWeek function to display the day of week of the date contained in the birthDay field.

There is one special function called Sum that operates on a list of widgets instead that just one like other functions. This function uses a "special" syntax for determining which widgets to use :

<fd:calculatedfield id="itemsTotal" state="output">
  <fd:label>Items in your cart</fd:label>
  <fd:datatype base="integer"/>
  <fd:value eval="Sum({cart/./items})"/>
</fd:calculatedfield>

<fd:calculatedfield id="grandTotal" state="output">
  <fd:label>Total cost</fd:label>
  <fd:datatype base="double">
    <fd:convertor type="formatting" variant="currency"/>
  </fd:datatype>
  <fd:value eval="Sum({cart/./subTotal})"/>
</fd:calculatedfield>

"cart" is the name of a repeater, and "/./" means "every row". So Sum({cart/./items}) simply means "Sum the value of the items widget in every row of the cart repeater". As you can see, the grandTotal field is a sum of calculated fields.

Repeated expression

The repeated expression is similar to a simple expression, but is repeated for each indicated widget. The syntax is as follows :

<fd:value type="repeated" repeat-on="..." eval="..." [initial-value="..."]/>
  • type is used to indicate which algorithm we are using, in this case "repeated"
  • repeat-on contains a list of widgets (comma separated, and/or with the /./ notation) we are going to iterate on
  • eval contains the expression to repeat
  • initial-value optionally contains and expression that will be used as an initial value for the computation.
In java this would sound something similar to :
Iterator iter = widgetsList.iterator();
int formulaResult = initialValue;
while (iter.hasNext()) {
  formulaCurrent = ((Widget)iter.next()).getValue();
  formulaResult = <your formula here>;
}
this.setValue(formulaResult);
Or more formally :
  • The initial-value is computed, relative to the calculated widget itself, and stored in a variable called formulaResult
  • For each widget specified in the repeat-on list :
    • The value of the widget is assigned to a variable called formulaCurrent.
    • The expression is evaluated relative to the current widget
    • The result of the expression is assigned to the formulaResult variable
  • formulaResult (so, the result of last evaluation of the expression) is used for the calculated field value.
For example, to calculate the grandTotal we could have used :
<fd:value type="repeated" eval="formulaResult + formulaCurrent" repeat-on="cart/./subTotal"/>
Obviously, we have the Sum function which is by far simpler, but the repeated expression could be used for example :To count all items with a price higher than 100:
<fd:value type="repeated" repeat-on="/cart/./price" eval="formulaResult + If(formulaCurrent > 100, 1, 0)"/> 
(read : the result is the previous result plus one if current price value is over 100, 0 if it's less than 100)To obtain a sum of all movements in a report, wether they are positive or negative amount movements:
<fd:value type="repeated" repeat-on="report/./amount" eval="formulaResult + Abs(formulaCurrent)"/>
To obtain a multiplicatory :
<fd:value type="repeated" repeat-on="data/./operand" eval="formulaResult * formulaCurrent" initial-value="1"/>

Javascript

There are obviously situations where the simple or repeated expressions are not enought, so you can use javascript to quickly write your own calculated field algorithm :
<fd:value type="javascript" triggers="...">
  ... you javascript code here ...
</fd:value>
In this case, no eval attribute is used, since javascript code is written directly inside the fd:value element.

As opposite to simple and repeated expressions, you have to specify the triggers. Triggers are widgets that, when modified, trigger a recalculation of this calculated field.

The triggers attribute is a comma separated list of widgets, eventually their full path and/or with the /./ notation.

In the javascript snippet :

  • you can access the Form object with the form variable.
  • you can access the parent widget with the parent variable (this is useful in repeaters, cause the parent will be the current row).
  • you must return the value for the calculated field, not assign it yourself.
So, for example, to implement a discount policy for our cart we could write :
<fd:calculatedfield id="discount" state="output">
  <fd:label>Total cost</fd:label>
  <fd:datatype base="double">
    <fd:convertor type="formatting" variant="currency"/>
  </fd:datatype>
  <fd:value type="javascript" triggers="cart/./items,cart/./price,discountCode">
     var code = form.lookupWidget('discountCode').getValue();
     var verifier = new Packages.com.mycompany.discounts.Verifier();
     if (!verifier.isValidDiscountCode(code)) {
       return 0;
     } else {
       var discountAmount = 0;
       ...
       return discountAmount;
     }
  </fd:value>
</fd:calculatedfield>

Java

An algorithm can also be implemented as a java class. In this case the class must either implement the CalculatedFieldAlgorithm interface, or subclass the AbstractBaseAlgorithm class. The definition syntax is :
<fd:value class="..." [triggers="..."]/>
A sample algorithm for the same discount policy could be the following :
public class DiscountAlgorithm extends AbstractBaseAlgorithm {

    // This method is called to check if this algorithm is suitable
    // for the field it has been assigned to. 
    public boolean isSuitableFor(Datatype dataType) {
        return dataType.getTypeClass().isAssignableFrom(Double.class);
    }

    // This method is actually called to perform the calculation,
    // it must return the value for the widget.
    public Object calculate(Form form, Widget parent, Datatype datatype) {
        String discountCode = form.lookupWidget("discountCode").getValue();
        Verifier verifier = new Verifier();
        if (!verifier.isValidDiscountCode(code)) {
            return new Double(0);
        } else {
            double discountAmount = 0;
            ...
            return new Double(discountAmount);
        }
    }
}

Extending the AbstractBaseAlgorithm class is the preferred way of implementing a custom algorithm, because it would benefit from some standard features (triggers list for example) and from a better insulation from future calculated fields improvements.

Errors and Improvements? If you see any errors or potential improvements in this document please help us: View, Edit or comment on the latest development version (registration required).