Script Objects: Deep Dive

Anyone looking to reduce code repetition in their forms will be familiar with script objects.  Script objects are a way to group functionality in a single location and share it from multiple places.  Most users will localize some logic in a series of JavaScript functions.  Advanced JavaScript programmers will try to do more.  And at that point they will notice some quirks around script object usage.  In this blog entry I’ll give a little background as to how script objects are implemented internally and also how advanced programmers can use them and still flex their programming skills.  We’re pretty much in the deep end here.  If you’re a novice/intermediate script writer, you’re not the target audience for this information :-)

Managing Script Objects

An XFA script object is very much like a JavaScript "class" (where a class is represented as a function).  When you create a JavaScript class object you might code something like this:

function Rectangle() { 
    this.width = "0in";
    this.height = "0in";
    this.getInches = function(measurement) { … }
}

The user of this class can then write code such as:

var rect = new Rectangle();
rect.width = "8cm";
rect.height = "4cm";
var area= rect.getInches(rect.width) * rect.getInches(rect.height);

The script object equivalent would look like:

form1.#variables[0].Rectangle – (JavaScript, client)

var width = "0in";
var height = "0in";
function getInches(measurement) { … }

and your calculation using the script object would look like:

form1.NumericField1::calculate – (JavaScript, client)
Rectangle.width = "8cm";
Rectangle.height = "4cm";
var area= rect.getInches(rect.width) * rect.getInches(rect.height);

Notice two things that the form author didn’t have to worry about: creating the script object and exposing properties for external access (prefixing members with "this.".  The internal machinery to manage the script object makes authoring easy.  However, this machinery can get in the way of advanced users.  But hopefully by the end of this entry the advanced users will have enough new data to accomplish what they want as well.

Creating Script Objects

Each script object gets created the first time it is referenced. If there are no form scripts that use the script object, it will never get created.  Note also that if your script object is housed inside a repeating subform, we will create one instance of that script object for each instance of the repeating subform.

There are a couple of things you want to be aware of related to the creation of script objects:

1. When a script object is created, Reader will execute the body of code in the object.   This is where any variable initialization happens.  In our example, this is where width and height get initialized.

2. If there are problems in your script object code and if executing the code results in a JavaScript exception, the script object will not initialize correctly. Depending on the error, it might be partially initialized or it might fail completely. Unfortunately, Reader is inconsistent in reporting errors during initialization.  You might not get a message in your console and the symptom will be that any script that references the script object will get an error such as:

Rectangle.getInches() is not a function
2:XFA:form1[0]:NumericField1[0]:calculate

The reason for this error is that because of the initialization failure the script object didn’t expose
getInches() as a function.

Exposing Properties

It is a bit much to expect novice (or intermediate) form authors to understand the need to define object properties in order to use them externally.  For the 95% use case, the form author just wants to define some functions and reference them from other script.  To make this easy, the XFA processor examines the JavaScript looking for function and variable declarations.  For each declaration, it exposes the declared object as a property.  The actual internal mechanism for exposing properties varies between client and server.  On the server, the script code gets modified to add declarations at the end:

form1.#variables[0].Rectangle – (JavaScript, client)
var width = "0in";
var height = 0in";
function getInches() { … }; 

this.getArea=getArea;
this.width=width;
this.height=height;

On the client (Reader/Acrobat), the properties are exposed by registering them with the internal script object.

Script Object Behaviours

References to "this"

As mentioned in previous posts, we use different JavaScript engines on the client and on the server.  If you are pushing on the boundaries of script object functionality, you will notice some differences in the two implementations. 

  • On the client (Reader/Acrobat), references to "this" inside a script object refer to the subform hosting the script object.
  • On the server, "this" evaluates to the script object itself

Dynamic Properties

On the client, a script object can be dynamically extended with new properties (even with strict scoping on).  On the server a script object cannot be dynamically extended with new properties

Naming

Don’t give script object functions the same name as the script object.  eg. don’t create a function named "foo" inside a script object named "foo".  The name lookup mechanism will fail to resolve this correctly.  You’ll get an error such as:

form1.foo.foo() is not a function
1:XFA:form1[0]:NumericField3[0]:calculate

Using the new operator

If you want to define a JavaScript ‘class’ in your script object, you need a workaround to manage it correctly.

eg. suppose your script object defines a rectangle:

form1.#variables[0].R – (JavaScript, client)
function Rectangle(width, height) {
  this.w = width;
  this.h = height;
}

In your script you’d like to create an instance of this rectangle.  Ideally you would simply code:

var rect = new R.Rectangle(4,5);

Unfortunately, the new operator does not work in this context.  The workaround is to include a helper function in your script object:

form1.#variables[0].R – (JavaScript, client)
function Rectangle(widt
h, height) {
  this.w = width;
  this.h = height;
}
function newRectangle(width, height) {
    return new Rectangle(width, height);
}

In order to create a Rectangle object your code can call:

var rect = newRectangle(4,5);

Exposing Variables as Properties

If you have variables that you want to use both within your script object *and* expose outside your script object, you need to be careful how you reference them.  Consider this example:

form1.#variables[0].R – (JavaScript, client) 
var width=0;
var height=0; 
function area() {
    return width * height;
}

As described above, on the server, this gets modified to:

form1.#variables[0].R – (JavaScript, client) 
var width=0;
var height=0; 
function area() {
    return width * height;
}
this.width = width
this.height = height;
this.area = area;

While this code addition nicely exposes the properties to the rest of the form, in doing so we create a copy of the width and height variables.  We now have both variables and properties named width and height.  When the width and height are referenced outside the script object, it is the property versions that are used.   However, when referenced inside the script object, it is the copies declared as variables that are referenced.  For the example above, on the server an external script could modify R.width and R.height, but the area() function will always return zero.  On the client because we expose the properties using a different mechanism, the external and internal references both access the property versions of the object.  In order to make your script work reliably on both client and server, make sure that references within the script object always predicate with "this":

form1.#variables[0].R – (JavaScript, client) 
var width=0;
var height=0; 
function area() {
    return this.width * this.height;
}

Variables in Script Objects

If you want to use variables in script objects that are preserved during your forms session, you need to be aware of variable scope issues.  These are described in previous blog entries: Scope of JavaScript Objects and Form Compatibility.

Design Pattern 1

Not happy with the way the XFA processor is managing your script object?  There is a workaround that allows you to regain some control.  Personally, I’m most interested in keeping my variables private.  Those of us who have written code in C++ and Java have always been encouraged to follow this pattern.  You provide the outside world access to your private members through accessor functions/methods.

The code in Reader that’s parsing your script looking for declarations can be fooled.  When the parser encounters a brace, it stops looking for declarations to expose.  If you enclose parts of your script with braces, you will have more control over how your class is handled. This is best explained by example:

form1.#variables[0].Rectangle – (JavaScript, client) 
{   // Opening brace causes parser in Reader to ignore contents

    // Declare these variables and keep them private
    // calcs (in Reader) will not be able to access Rectangle.width
    var width = 0;
    var height = 0; 
}
function setW(W) { width = W; }
function setH(H) { height = H; }
function getW()  { return width;  }
function getH()  { return height; }
function area()  { return width * height; }

Code referencing this script object can look like:

Rectangle.setH(4);
Rectangle.setW(5);
this.rawValue = Rectangle.area();

Note that this code works on the client and server — however on the server, the XFA processor discovers the variables without performing a parse — we use a function on the script engine itself.  The server script engine is not fooled by our trickery.  The impact is that on the server, code will continue to be able to access Rectangle.width and Rectangle.height.  As long as your form is tested on both client and server you might be willing to live with this behaviour.

Design Pattern 2

A second technique works a bit harder to hide member variables:

form1.#variables[0].Rectangle – (JavaScript, client) 
function R() {

    // Declare these variables and keep them private 
    var width = 0;
    var height = 0;

    // Declare these functions and make them public
    this.area = function() {return width * height;}

    // Provide accessors to private variables
    this.setW = function(W) { width = W; }
    this.setH = function(H) { height = H; }
    this.getW = function()  { return width;  }
    this.getH = function()  { return height; }
}
{   // add braces to hide this declaration in Reader

    var Rect = new R();
}
function setW(W) { Rect.setW(W); }
function setH(H) { Rect.setH(H); }
function getW(W) { return Rect.setW(W); }
function getH(H) { return Rect.setH(H); }
function area()  { return Rect.area(); }

Notice that the variable Rect will end up exposed on the server, but since it has kept its private members hidden, other scripts will not be able to modify the object (although on the server the entire variable could be replaced).  But this is a good design pattern for a coup
le of reasons:

  1. On the server when the XFA processor adds the code: this.Rect=Rect; we do not end up with a copy of the variable, because assigning to an object creates a reference, not a copy.
  2. Limiting access to member variables via function calls means that we will not fall into the classic JavaScript trap where we inadvertently assign a new property to an object when we misspell the member name.

4 Responses to Script Objects: Deep Dive

  1. Keith Gross says:

    Talk about luck. I’ve had this little mystery sitting around for a bit. It involves a difference in how a function defined within a script object is seen when viewed from a event handler.I’ve pasted the code from my sample form in below. The form is very simple. It has one script object with the code shown and a button with a click event with the code also shown below. Further down is the output you’d get in the console from clicking the button.In the output you can see when I do a toString on a reference to a function defined in the script object from in the event handler it displays as [native code] though I’m able to execute that reference fine.Next when I’m in a function of the script object and do a toString of a reference to another function in that script object I’m correctly shown the code of the function and of course I can execute the reference fine.Now when I’m in a function of the script object and do a toString of a reference to another function in that script object but were the reference was acquired back in the event script it displays as [native code] and any attempt to execute that reference fails. And in fact if I compare the remotely acquired reference to a locally acquired reference they are not equal.There were some other more subtle effects as well and I eventually redesigned things to move code that interacted into the same script object as much as possible and try and make the event code very simple. I’ve always been curious though what the barrier is between the two worlds and why it exists.On a somewhat related note I have a question. Is the Reader script engine based on Spidermonkey and if so what version does it use?================ Form Code ===========form1.#subform[0].#variables[0].testing – (JavaScript, client)function one(a) {console.println(target.toString());target();console.println(a.toString());//a();console.println(a == target);}function target() {console.println(‘target() function executed’);}form1.#subform[0].Button1::click – (JavaScript, client)console.println(testing.target.toString());testing.target();testing.one(testing.target);================ Output ==============function target() {[native code]}target() function executedfunction target() {console.println(“target() function executed”);}target() function executedfunction target() {[native code]}false

  2. Keith:Here I thought I was hopelessly in the deep end, and you want to drill deeper still…Internally there are actually two objects representing the script — an XFA object and a JavaScript object. If you added a call that did “testing.toString()” the button will see:[object XFAObject]whereas within the script itself that same test will return:[object XFAScriptObject]The XFAObject wraps the XFAScriptObject.After scanning the script for functions (and variables) we add those definitions to the XFAObject which then passes those requests through to the XFAScriptObject when invoked.The reason the script object is wrapped is so that it can inherit basic functionality that comes with every XFAObject: e.g. Properties like name, className, parent etc.The JavaScript engine in Reader is Spidermonkey. Reader 9 uses version 1.7.John

  3. Dave says:

    Hi John, This blog has helped me a lot, thanks for the info. I am reasonably good with JavaScript but new to Designer and have had some trouble getting my objects working.

    I had hoped to write some generic methods to handle both my custom objects and JavaScript objects and was hoping to override the toString method. The example below is from the SpiderMonkey site and works in FireBug but does not seem to in Designer.

    function Dog(name,breed,color,sex) {
    this.name=name;
    this.breed=breed;
    this.color=color;
    this.sex=sex;
    }

    theDog = new Dog(“Gabby”,”Lab”,”chocolate”,”female”);

    console.println(theDog.toString()); //returns [object Object]

    Dog.prototype.toString = function dogToString() {
    var ret = “Dog ” + this.name + ” is a ” + this.sex + ” ” + this.color + ” ” + this.breed;
    return ret;
    }

    console.println(theDog.toString());

    I get ‘[object Object]” from both calls to toString().

    I then thought I would use the instanceOf operator to identify my object (very hacky) but this does not seem to work either, I assume because I’m actually dealing with a wrapper object?

    Then I though I would have a base object with a common property (a bit like className in the XFA object) but I haven’t been able to get the prototype chain working either.

    I’m starting to feel like I am trying something I should be trying. Or am I missing something.

    Thanks

    Dave

    • John Brinkman says:

      Dave:

      The JavaScript environment in Acrobat/Reader does not allow you to override toString(). Similarly, you cannot override toSource(). I don’t know why these have been disabled, but I suspect it is so that JavaScript that is part of the Reader install cannot be disrupted by document script.

      John