Unleashing JavaScript power in your FileMaker application

By Jan Zelenka.

Hi, my name is Jan Zelenka, and I want to show you how an efficient implementation of a JavaScript function library in FileMaker can be achieved.

In some areas, the JavaScript engine is considerably faster than FileMaker. You can get serious performance gains if you use JavaScript functions instead of FileMaker custom functions or scripts.

Here’s an example: It is in fact the same business case I’m using to demonstrate the techniques mentioned in this article (download link below): Suppose you receive a big set of measurement results in JSON. Your app must digest these results and perform some aggregation, for instance, calculating the average of the received results.

I knew FileMaker’s handling of JSON was pretty slow. Still, I found the results pretty amazing. The task is to calculate an average of 10,000 values stored in a sample JSON array. Here is how long it took for my machine to perform the calculation:

  • FileMaker custom function: 180 seconds
  • JavaScript: 100 ms

As you can see, seamless integration of JavaScript in a FileMaker application can be a crucial part of your project. I will share some thoughts on how to store a set of JavaScript functions in your FileMaker app and how to call these functions from any script in your project – and that with just one Perform Script script step.
Think of it as a replacement for FileMaker custom functions available only for your scripts.

To avoid any confusion right away, this article is not about HTML. It’s not about how to improve FileMaker visually using JavaScript-enriched web pages. This is about pure JavaScript function libraries that allow taking advantage of the JavaScript engine performance that exceeds the one of the FileMaker calculation engine.

Background information

Since FileMaker version 19, a new script step has been added: Perform JavaScript in Web Viewer. This script step will take the Web Viewer specified by its object name and call the requested JavaScript function. That all works very well for specific use cases, but once you start thinking of building a reusable JavaScript function library, you realize it’s not that simple to access this library from anywhere within your file. Let’s first have a look at some less efficient methods that I would suggest avoiding.

JavaScript function library—methods to avoid.

Hidden Web Viewer on each layout

Since every FileMaker script runs in a context of a layout, you could opt to place a Web Viewer object outside the layout boundary on each layout of your file. From the perspective of future maintenance, this is the least favorable method. If you plan on storing your JavaScript in a table, every layout would require a relationship to that table. And even if you chose a custom function to keep the JavaScript code, you still need the same web viewer on every layout, which goes against one of the basic rules of software development: don’t repeat yourself.

Hidden window

You create a dedicated layout for the Web Viewer and every time you need to call your library, you open a new window outside of the screen boundaries, for instance at -5000; -5000. This is already better; you have to maintain only one layout and one Web Viewer. Unfortunately, opening and closing a new window means a considerable performance hit, so I wouldn’t recommend it either.

The solution

Let’s then have a look at what can be done to circumvent the abovementioned shortcomings.

Separate file

I would suggest placing such a library into a separate file. You get to maintain one version only, and it can be made accessible to multiple projects. Let’s call it js-worker. You add js-worker to your master file as an External Data Source before using it for the first time (mostly in the OnFirstWindow trigger handler), you open it

Open File [ Open hidden: On ; "js-worker" ]

Open Hidden: On is the interesting part here. It makes sure no extra window is open and the Web Viewer hosting our JavaScript library will work in the FileMaker virtual window (with a bit of convincing needed – I’ll get back to that later on) created for the js-worker file.

Implementation details

Attached to this blog you will find 2 files:

  • js-worker.fmp12: a simplified implementation of the JS library file.
  • master.fmp12: a sample file used to demonstrate loading the library and using it.
  • experiment.json: a data file containing a JSON formatted array of hypothetical measurements with the result of each measurement in milliseconds.

Let’s have a look at the interesting parts of the js-worker.fmp12 implementation.
The file is configured to auto-open with a Read-Only Access account. To get Full Access, use the menu Scripts > Utilities > Relogin. The account is Admin, the password is admin.

The Concept

The js-worker file provides an entry point script. When called, it invokes in turn the requested JS function and waits until a global variable indicates the JS is finished and a result is available. It’s achieved by the JS function calling another FM script and passing the result as a parameter to this script. This script then sets the required global variables and exits, and the entry point script can pick them up and return the result to the original caller.

Library table

This table holds JS libraries you would want to have available. Each record represents one library, identified by a unique name (field Name), and holding the complete JS code of your library (field Code). Optionally, you can designate a library as the default. If a default library exists, it will be used when a call is made without specifying the library name.

JSW.Call script

This is the entry point script for any calls to the library. It accepts a parameter in the form of a JSON string and allows you to specify the library to use (optional), the function to call, and one parameter to pass to the function (optional). After navigating to the working layout and locating the library, this script performs the call to the JS function:

Set Variable [ $$finished ; Value: "" ]
Refresh Window [ ]
Perform JavaScript in Web Viewer [ Object Name: "js-worker" ; Function Name: $function ; Parameters: $parameters ]

First, let’s not forget that all of this is happening in a FileMaker virtual window. I suspect that is the reason why the Refresh Window step is necessary. Without it, the call to the JS function doesn’t work.
Also, due to the asynchronous nature of JavaScript, the last script step can’t return whatever the JS function return value is. Therefore we need to reset the $$finished global variable so it can be used to retrieve the result once the JS is done:

Loop
    Exit Loop If [ $$finished ]
End Loop
Exit Script [ Text Result: $$response ]
Demo Library

The JS Worker file contains one library named Demo with one function named averageTime. This function takes a JSON array of measurement objects and calculates the average time of all the measurements in the array. Here is a 3-item sample of the array:

{
    "measurements": [
        {
            "time": 5.3499795065668
        },
        {
            "time": 5.48264791500604
        },
        {
            "time": 5.05030523259211
        }
    ]
}

Here is the JavaScript function itself:

function averageTime ( experiment ) {
    if ( typeof experiment === 'string' )
        experiment = JSON.parse( experiment );
    var totalTime = 0;
    for ( const measurement of experiment.measurements ) {
        totalTime += measurement.time;
    }
    const averageTime = totalTime / experiment.measurements.length;
    FileMaker.PerformScriptWithOption(
            'Internal.ReceiveResult'
            , averageTime
            , '5'
            );
}

The FileMaker.PerformScriptWithOption method is the only direct way to pass data from the JS function to the FM script engine. The 2nd parameter needs to be set to whatever result we want to obtain from the JS function call. The third one must be 5 in order to have the JSW.Call script paused while Internal.ReceiveResult sets the global variables and has it resume again afterwards.

Internal.ReceiveResult script

As the JS function sets the JSW.Call script on pause, this one gets executed, sets the $$response and $$finished global variables, and exits:

Set Variable [ $$response ; Value: JSONSetElement( "{}"; [ "result"; "data"; JSONString ]; [ "data"; Get( ScriptParameter ); JSONRaw ] ) ]
Set Variable [ $$finished ; Value: True ]
Exit Script [ Text Result: "" ]

Once that happens, the JSW.Call script resumes, picks up the $$response value, and passes it to the original caller.

This file allows you to test the js-worker and do some comparisons of JS and FM performance. It opens with the passwordless Admin account auto-logged in and presents a simple layout with 2 buttons: JavaScript and FileMaker. They both load the content of the file experiment.json, calculate the average time of all the measurements and present the result in Custom Dialog. Additionally, they measure the time it takes to calculate this average. Clearly, the first button uses the above-explained JavaScript technique, and the other one a while loop wrapped around by a custom function.

Conclusion

In short, we have explored an efficient technique that puts the JavaScript power of just one Perform Script call away from any scripts in your files. On top of that, we have demonstrated that in time-critical operations, JavaScript absolutely has to be the calculation engine of choice.
I hope you found this article useful and I wish you a happy JavaScripting!