Search code examples
xpageslotus

How to reverse the order of evaluation in partial update?


I have encountered that when using an onChange event on a field with partial update set, the execution of the partial update is performed before the SSJS-code on the actual event.

Is there a possible way to reverse this?

In my enclosed example I have three fields, one hidden input and a computed value.

Users enters values in these three fields and the hidden component has a formula to calculate the sum of the fields above. Nominal * Price + Fee = PaymentAmount.
Now the customer wants to have a predefined Fee value that gets calculated before PaymentAmount; in my example I use a hardcoded value of 0.15%.
The formula will then be PaymentAmount = (Nominal * Price * feeDef) + (Nominal*Price)

We have set the PaymentAmount inside a table where we have tagged the Row as PaymentAmountRow just to be able to update both components within.
All components used to have partial update on PaymentAmountRow but now I need component Price to update Fee1 as well. I thought if I changed the partial update to a component above everything, in this case the surrounding panel called dealInfo it will work fine but to my surprise the calculation of the partial component is triggered before the actual code that calculates Fee.

I have tried all available parameters on the event but none of them is working…

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
    <xp:this.resources>
        <xp:script
            src="/CommonSSJS.jss"
            clientSide="false"
        >
        </xp:script>
    </xp:this.resources>
    <xp:panel id="dealInfo">
        <xp:this.data>
            <xp:dominoDocument
                var="document1"
                formName="EquityTrade"
            >
            </xp:dominoDocument>
        </xp:this.data>
        <xp:label
            value="Nominal"
            id="label2"
        >
        </xp:label>
        <xp:inputText
            id="Nominal"
            value="#{document1.Nominal}"
        >
            <xp:eventHandler
                event="onchange"
                submit="true"
                refreshMode="partial"
                refreshId="PaymentAmountRow"
            >
                <xp:this.action><![CDATA[#{javascript:print("Nominal onChange - partial PaymentAmount");}]]></xp:this.action>
            </xp:eventHandler>
            <xp:this.converter>
                <xp:convertNumber type="number"></xp:convertNumber>
            </xp:this.converter>
            </xp:inputText>
        <xp:br></xp:br>
        <xp:label
            value="Price"
            id="label1"
        >
        </xp:label>
        <xp:inputText
            id="Price"
            value="#{document1.Price}"
        >

            <xp:this.converter>
                <xp:convertNumber type="number"></xp:convertNumber>
            </xp:this.converter>
            <xp:eventHandler
                event="onchange"
                submit="true"
                refreshMode="partial"
                refreshId="Fee1">
                <xp:this.action><![CDATA[#{javascript:print("Price onChange - update")
var Fee1:com.ibm.xsp.component.xp.XspInputText = getComponent("Fee1");
var feeDef = 0.15;
var Nominal:com.ibm.xsp.component.xp.XspInputText = getComponent("Nominal");
var Price:com.ibm.xsp.component.xp.XspInputText = getComponent("Price");
var fee = Nominal.getValue()*Price.getValue()*feeDef
Fee1.setValue(fee);
}]]></xp:this.action>
            </xp:eventHandler>
        </xp:inputText>
        <xp:br></xp:br>
        <xp:label
            value="Fee 1"
            id="label3"
        >
        </xp:label>
        <xp:inputText
            id="Fee1"
            value="#{document1.Fee1}"
        >
            <xp:this.converter>
                <xp:convertNumber type="number"></xp:convertNumber>
            </xp:this.converter>
            <xp:this.valueChangeListener><![CDATA[#{javascript:print("Value changed");
}]]></xp:this.valueChangeListener>

            <xp:eventHandler
                event="onchange"
                submit="true"
                refreshMode="partial"
                refreshId="PaymentAmountRow"
            ></xp:eventHandler>
        </xp:inputText>
        <xp:br></xp:br>

        <xp:table>
            <xp:tr id="PaymentAmountRow">
                <xp:td>
                    <xp:inputHidden
                        id="PaymentAmount"
                        value="#{document1.PaymentAmount}"
                    >
                        <xp:this.converter>
                            <xp:customConverter getAsString="#{javascript:return value.toString();}">
                                <xp:this.getAsObject><![CDATA[#{javascript:try {
    print("PaymentAmount calculation");
    var Nominal:com.ibm.xsp.component.xp.XspInputText = getComponent("Nominal");
    var Price:com.ibm.xsp.component.xp.XspInputText = getComponent("Price");
    var Fee1:com.ibm.xsp.component.xp.XspInputText = getComponent("Fee1");

    var nominal = Nominal.getValue();
    var price = Price.getValue();
    var fee1= Fee1.getValue();

    var paymentAmount = nominal * price;
    paymentAmount+=fee1;
    return paymentAmount;
} catch(e){
    dBar.info(e);
}}]]></xp:this.getAsObject>
                            </xp:customConverter>
                        </xp:this.converter>
                    </xp:inputHidden>
                    <xp:text
                        escape="true"
                        id="computedField14"
                    >
                        <xp:this.value><![CDATA[#{document1.PaymentAmount}]]></xp:this.value>
                        <xp:this.converter>
                            <xp:convertNumber type="number"></xp:convertNumber>
                        </xp:this.converter>
                    </xp:text>
                </xp:td>
            </xp:tr></xp:table>
    </xp:panel>
</xp:view>

Solution

  • I'm not 100% sure I understand the code or which part is causing the problem, but I think it's that the converter's getAsObject() is running before the application logic of the eventHandler and is not getting the right values. So here goes:

    XPages (and JSF) partial refresh lifecycle runs through six phases: 1) Restore View rebuilds the component tree so we can map the HTML being sent back to the underlying Java objects. 2) Apply Request Values takes the string values entered in the HTML and puts them in the submittedValue property of each relevant component. 3) Process Validation runs all converters and validators to ensure the submittedValue properties can be converted to the underlying data type and pass any validation rules defined on the server. 4) Update Model Values takes those submittedValues (now we know we can apply them to the data model without screwing things up!), converts the strings to the relevant datatype, puts them in the value property and sets submittedValue to null. (It might have converted the strings during Process Validation phase and stored them in internal variables, so it only needs to update value and submittedValue at this phase, not sure.) 5) Invoke Application runs your eventHandler logic. 6) Render Response generates the updated HTML for the browser.

    You can't change the order. So Process Validations has to run before Update Model Values. Otherwise, Update Model Values could break because the data type's wrong, or you've stored an invalid value which will then get written to your underlying document.

    I think the problem is you're converter is using getComponent().getValue(). But value won't have been set during the Process Validations phase, which is when the converter's getAsObject() method is running.

    If so, what you actually need is either:

    • To use getComponent().getSubmittedValue(), validate the values of the other components in your converter, and throw errors accordingly.

    or

    • Just use a basic number converter and move your code setting the payment amount to the eventHandler, interacting with the backend data - so getting document1.Price etc and setting document1.paymentAmount.

    If you use the latter, you should be able to avoid needing the inputHidden.