Search This Blog

Saturday 14 February 2015

Custom Properties

The XFA specification defines customs properties that can be store in a field, exclGroups (or radio buttons) or subforms under the <desc> and <extras> elements.  These allow us to write scripts that process a form but allow the individual fields and subforms to control that processing.  The example used here is a script that sets all the fields to their default value, except if an allowResetData property is set to false.  Other scenarios might be setting all fields to read only, except some, or some have additional processing, validation frameworks, setting min/max values, etc.

There doesn’t seem to be much difference between using the <desc> or <extras> elements but I tend to use <desc> as the names and values are shown in the Info palette.


The <desc> element is also were the <xs:annotation> information is stored is you are using a XML Schema for your data connection.
 
The XFA for a field with this custom property would then look like;
 
<field name="TextField1" w="62mm" h="9mm">
    <desc>
        <boolean name="allowResetData">1</boolean>
    </desc>
    <ui>
        <textEdit/>
    </ui>
    ...
</field>
 
There is no support for updating these values in LiveCycle Designer but I will include two macros that can be used to update the allowResetData property and hopefully can be used as a guide for updating your own properties  … or you could just edit the XML Source window. 

The data stored under a <desc> element can be typed, so JavaScript references are returned in properties with the appropriate data type.  The exception is the date/time related elements that are returned as strings. 



desc element
JavaScript
Data Type
Value Range
boolean Boolean 0 – false, 1 – true
date String  
dateTime

String  
decimal Number For integers;
9007199254740992 to -9007199254740992;
(Same as JavaScript, that is 253)
For floats
1.79E+308 to 1E-15 (max 15 decimal digits if specified by the fracDigits attribute e.g.
<decimal name="Decimal" fracDigits="15"/>,
Otherwise defaults to 2 decimal places
exData String  
float Number 9007199254740992 to -9007199254740992;
(Same as JavaScript)
For floats
1.79E+308 to 1E-08 (max 8 decimal digits)
image String Prints the number 2 in a width of 5 characters with "0" characters padding
integer Number 2147483647 to -2147483647
Attempting to assign a number outside this range raises an "Operation failed." GeneralError exception
text String  
time String  


I haven’t seen any limits documented, these are the values I have found by playing around. Also remember for decimal and float values that exceed the maximum integer value then the precision starts to fail like all usual float values, e.g.


9007199254740992 + 1 = 9007199254740992
9007199254740994 + 2 = 9007199254740994
9007199254740992 + 3 = 9007199254740996


More about float and decimal values here, http://en.wikipedia.org/wiki/Double-precision_floating-point_format

A value under a <desc> element value can be referenced directly in JavaScript using TextField1.desc.allowResetData.value but if the property does not exist on the form object you will get a “Invalid property get operation; desc doesn't have property 'allowResetData'” exception.  To check if a property exists use the namedItem() method, TextField1.desc.nodes.namedItem("allowResetData"), this will return null if the property does not exist or an object with a value property if it does.

So now the JavaScript function to process the form (or part of the form) could look like this;

function resetData(node)
{
    function pushResetDataList(node)
    {
        var allowResetData = node.desc.nodes.namedItem("allowResetData");
        // default is too allow reset
        if (allowResetData === null || allowResetData.value)
        {
            resetDataList.push(node.somExpression);
        }
    }

    function resetDataInner(node)    
    {
        if (node.className === "exclGroup" && !node.isNull) // don't reset fields that are null
        {
            pushResetDataList(node);
        }
        else
        {
            if (node.className === "field")
            {
                if (node.ui.oneOfChild.className !== "button" && !node.isNull) // buttons always null
                {
                    pushResetDataList(node);
                }
            }
            else
            {
                for
(var i = 0; i < node.nodes.length; i++)
                {
                    var nextNode = node.nodes.item(i);
                    if (nextNode.className === "instanceManager")
                    {
                        if (nextNode.count.toString() !== nextNode.occur.min)
                        {
                            nextNode.setInstances(nextNode.occur.min);
                        }
                    }
                    else
                    {
                        if (nextNode.className === "variables")
                        {
                            var scriptObject = nextNode.resolveNode("Script");
                            if (scriptObject && scriptObject.hasOwnProperty("resetData"))
                            {
                                scriptObject.resetData();
                            }
                        }
                        else
                        {
                            if (nextNode.isContainer && nextNode.className !== "draw")
                            {
                                resetDataInner(nextNode);
                            }
                        }
                    }
                }
            }
        }
    }
    var resetDataList = [];
    resetDataInner(node);
    if (resetDataList.length > 0)
    {
        xfa.host.resetData(resetDataList.join(","));
    }
}   

All subforms can have a script object and this resetData function looks for a script object called “Script” that contains a JavaScript function called “resetData” and if found executes it , the code under the nextNode.className === “variables”.

    var scriptObject = nextNode.resolveNode("Script");
    if (scriptObject && scriptObject.hasOwnProperty("resetData"))
    {
        scriptObject.resetData();
    }

This allows parts of the form to perform any custom operations required when a form is reset.  This example (CustomProperties.pdf) uses this function to remove a file that has been attached to the form.

I use two macros for setting and clearing allowResetData flag.  Once installed you will be able to select the appropriate form objects and run a macro instead of editing the XML Source.  To run select Tools … Macros … “Set allowResetData Flag to False” or Tools … Macros … “Clear allowResetData Flag”.  The macros and macro.xml configuration file are in the zip file (CustomProperties.Macros.zip).

If you haven’t written or installed a macro before then refer to the help page. Designer 10 - Macros

The <exData> element can be used to store rich text and be used to populate a Text control like;
Text1.value.exData.loadXML(TextField1.desc.ExData.saveXML(), true, true);

Likewise the <image> custom property can be used to store image and used to populate an image control like;  Image1.value.image.value = TextField1.desc.Image.value;

Form variables created using Form Properties … Variables are stored under a <variables> element, when created by LiveCycle Designer they are always text, but it is valid for them to be any of the types that can be used under the <desc> element.  This allows references to the variables in JavaScript to be typed, so by editing the XML Source can make your JavaScript code simpler, at least when dealing with Number and Boolean form variables.  Once you have made this change Designer will show them as a “?” icon in the hierarchy palette (see image below) but this has never caused any problem in my forms.

And looks like this in the XML Source window.


<variables>
    <text name="Text"/>
    <integer name="Integer"/>
</variables>