2.1.11 • Published 3 years ago

onfmready.js v2.1.11

Weekly downloads
-
License
MIT
Repository
github
Last release
3 years ago

OnFMReady.js

OnFMReady is a developer utility for working with the FileMaker web viewer JavaScript object in FileMaker Pro/WebDirect. This is version 2.0 of the utility. If you're looking for version 1.0, it's here.

What happened to version 1.0?

As stated above, you can still get version 1.0 of OnFMReady here, but if you read its updated documentation or the documentation for version 2.0, I'm sure you'll agree that the newer version is much better. If you'd like to know more about what's changed, continue reading below.

Introduction

FileMaker 19 introduced new JavaScript interaction between the web viewer layout object and the FileMaker environment. Developers can call FileMaker scripts from their JavaScript code using FileMaker.PerformScript() and FileMaker.PerformScriptWithOption(). However, neither method can be immediately called, as FileMaker Pro or FileMaker WebDirect must first inject the FileMaker object for use.

OnFMReady removes the need to introduce additional JavaScript or FileMaker script logic which "waits" for injection of the FileMaker object to occur. Most importantly, it does this without requiring the developer to "wrap" their script calls inside of helper functions. Simply calling either FileMaker.PerformScript() or FileMaker.PerformScriptWithOption() will queue script execution requests, and dispatch them immediately after the FileMaker object has been injected.

:pencil: NOTE: FileMaker Pro for Microsoft Windows

During development/publishing, I finished writing this README, and then decided to test OnFMReady in FileMaker Pro for Microsoft Windows, which now uses Microsoft Edge (a Chromium-based browser) to provide the web viewer object. I haven't had a chance to pull-apart the injection logic Claris is using there, but what I do know is that the FileMaker object is available for use immediately. No helper is needed, but OnFMReady will still provide the utility events filemaker-ready and filemaker-expected.

All this in-mind, details regarding the FileMaker object lifecycle, given below, apply to FileMaker Pro on macOS and FileMaker WebDirect (on all platforms, of course).

:pencil: NOTE: Microsoft Internet Explorer 11

Support for Microsoft Internet Explorer 11, which is used in FileMaker Pro prior to version 19.3, is built into the TypeScript source of OnFMReady and includes all required polyfills. This support is removed by default in the main distribution, onfmready.js and onfmready.min.js. If you require support for Microsoft Internet Explorer 11 in your project, use onfmready.es5.js or onfmready.es5.min.js — generated by the command npm run build or npm run build:es5.

After build or installation from CDN, please send me a postcard from the year 2013, which is where I assume your project is located.

Install

To install the helper, include it as the first linked <script> in the <head> tag of your document. You may link it via CDN, or provide it as inline code:

CDN

<!-- Without Support for Obsolete Web Browsers That Won't Die -->
<script src="https://unpkg.com/onfmready.js@2.1.11/dist/onfmready.min.js"></script>

<!-- With Support for Microsoft Internet Explorer 11 -->
<script src="https://unpkg.com/onfmready.js@2.1.11/dist/onfmready.es5.min.js"></script>

:pencil: NOTE: Versioning

It is recommended that you pin a version number (shown as @2.1.11) to prevent the introduction of potentially-breaking changes when updates are made to OnFMReady.

Inline Code

<!-- Without Support for The Browser You Used to Download Google Chrome -->
<script>
    !function(e){"function"==typeof define&&define.amd?define(e):e()}((function(){"use strict";(()=>{const e=window;let t,n,o=[];const i=()=>{if(!n){const t=new Event("filemaker-ready");e.dispatchEvent(t),document.dispatchEvent(t)}const t=Object.assign(new Event("filemaker-expected"),{filemaker:!n,FileMaker:!n});e.dispatchEvent(t),document.dispatchEvent(t)};if("object"==typeof e.FileMaker)return void setTimeout(i);e.OnFMReady=Object.assign({respondTo:{},noLogging:!1,unmount:!1},e.OnFMReady);const r={PerformScript:(e,t)=>r.PerformScriptWithOption(e,t),PerformScriptWithOption:(e,t,i=0)=>{n?s(e,t,i):o.push([e,t,i])}},s=(t,n,o)=>{const i=e.OnFMReady.respondTo[t];return i?i(n,o):e.OnFMReady.noLogging?null:console.log(Object.assign({script:t,param:n},o?{option:o}:{}))};let c,a,d=r;document.addEventListener("DOMContentLoaded",(()=>{(!t||t&&!t.stash)&&(d=null,a=!0),setTimeout((()=>{setTimeout((()=>{}))}))})),Object.defineProperty(window,"FileMaker",{set(e){d=e,a=!1,clearTimeout(c),null!=e&&setTimeout((()=>{const e=d,t=e?.PerformScriptWithOption||e.PerformScript;o.forEach((e=>{t(...e)})),o=[],i()}))},get:()=>t&&!t.stash&&!a&&(t.resolver(),t.stash)?null:(a&&(t=void 0,c=setTimeout((()=>{n=!0,e.FileMaker=e.OnFMReady.unmount?void 0:r}))),d)})})()}));
</script>

<!-- With Support for Microsoft Internet Explorer 11 -->
<script>
    !function(e){"function"==typeof define&&define.amd?define(e):e()}((function(){"use strict";!function(){var e,n=window;if(window.document.documentMode){if("function"!=typeof Object.assign&&Object.defineProperty(Object,"assign",{value:function(e,n){if(null==e)throw new TypeError("Cannot convert undefined or null to object");for(var t=Object(e),o=1;o<arguments.length;o++){var i=arguments[o];if(null!=i)for(var r in i)Object.prototype.hasOwnProperty.call(i,r)&&(t[r]=i[r])}return t},writable:!0,configurable:!0}),"function"!=typeof window.CustomEvent){function e(e,n){n=n||{bubbles:!1,cancelable:!1,detail:void 0};var t=document.createEvent("Event");return t.initEvent(e,n.bubbles,n.cancelable),Object.defineProperty(t,"detail",{value:n.detail}),t}e.prototype=window.Event.prototype,window.CustomEvent=e,window.Event=e}e={resolver:function(){try{var n=e.resolver.caller.caller.toString();e.stash=n.indexOf("if (window.FileMaker != null)")>=0}catch(n){e.stash=!1}},stash:!1}}var t,o=[],i=function(){if(!t){var e=new Event("filemaker-ready");n.dispatchEvent(e),document.dispatchEvent(e)}var o=Object.assign(new Event("filemaker-expected"),{filemaker:!t,FileMaker:!t});n.dispatchEvent(o),document.dispatchEvent(o)};if("object"!=typeof n.FileMaker){n.OnFMReady=Object.assign({respondTo:{},noLogging:!1,unmount:!1},n.OnFMReady);var r,a,c={PerformScript:function(e,n){return c.PerformScriptWithOption(e,n)},PerformScriptWithOption:function(e,n,i){void 0===i&&(i=0),t?u(e,n,i):o.push([e,n,i])}},u=function(e,t,o){var i=n.OnFMReady.respondTo[e];return i?i(t,o):n.OnFMReady.noLogging?null:console.log(Object.assign({script:e,param:t},o?{option:o}:{}))},l=c;document.addEventListener("DOMContentLoaded",(function(){(!e||e&&!e.stash)&&(l=null,a=!0),setTimeout((function(){setTimeout((function(){}))}))})),Object.defineProperty(window,"FileMaker",{set:function(e){l=e,a=!1,clearTimeout(r),null!=e&&setTimeout((function(){var e=l,n=(null==e?void 0:e.PerformScriptWithOption)||e.PerformScript;o.forEach((function(e){n.apply(void 0,e)})),o=[],i()}))},get:function(){return e&&!e.stash&&!a&&(e.resolver(),e.stash)?null:(a&&(e=void 0,r=setTimeout((function(){t=!0,n.FileMaker=n.OnFMReady.unmount?void 0:c}))),l)}})}else setTimeout(i)}()}));
</script>

Usage

OnFMReady provides both interception of calls to the FileMaker object as well an event emitter that fires once FileMaker is injected. In your own code, can make use of both or either utilities where appropriate:

Native Expression (Interception)

As stated before, OnFMReady intercepts calls to the FileMaker object, so you don't need to do anything other than use FileMaker.PerformScript() or FileMaker.PerformScriptWithOption() as you normally would. You can call either method anywhere in your code, so long as it's after the point in your document where you've installed OnFMReady.

In this way, you're able to use the web viewer as a kind of "script trigger" which calls a FileMaker script as soon as it's ready. There's no need to mess around with Pause[<duration>] script steps in your FileMaker scripts, or to introduce looping/checking logic in your JavaScript. As soon as FileMaker is injected, your script requests will automatically be fulfilled.

For example, to run a FileMaker script called Get Invoices you can simply call the following from anywhere in your code:

FileMaker.PerformScript('Get Invoices');

Event Listeners

When the FileMaker object is injected by FileMaker Pro or FileMaker WebDirect, OnFMReady will dispatch an Event, 'filemaker-ready', to window and document. You can add an event listener as you would any other:

window.addEventListener('filemaker-ready', () => {
  console.log('FileMaker is ready!');
});

/*--- or ---*/

document.addEventListener('filemaker-ready', () => {
  console.log('FileMaker is ready!');
});

In addition to dispatch of the filemaker-ready event, OnFMReady will also dispatch filemaker-expected with a filemaker boolean property at the point in the events cycle when FileMaker should have become available. You may leverage this event in your code to provide context in circumstances where you may be permitting access to your document both inside and outside of FileMaker, and thus wish to engage/disengage features which are exclusive to either context:

document.addEventListener('filemaker-expected', (event) => {
  if (event.filemaker) {
    /*--- feature enable/disable logic here ---*/
  }
});

Note that if your document is being accessed from dual contexts, you should not evaluate against window.FileMaker to determine context, as OnFMReady will still have provided its fallback instance of FileMaker to the browser. Instead, make use of the utility event as shown above. In the event that you wish to remove the FileMaker object entirely when outside of a FileMaker context, declare the following global variable:

window.OnFMReady.unmount = true;

When unmount is truthy, window.FileMaker will be set to undefined if not fulfilled by FileMaker Pro/WebDirect, and OnFMReady will disable its logging utilities, as well as execution of any functions declared in window.OnFMReady.respondTo (documented below).

Logging and Responding/Mocking (Outside FileMaker)

As it's very likely that a majority of the development on your web-based components will take place outside of FileMaker Pro/WebDirect, there may be many times when you wish to see what data your code will be sending to FileMaker once it's in production. Additionally, you may also wish to "mock" or "fake" responses from FileMaker for the purposes of testing.

Logging

By default, if OnFMReady is running outside of FileMaker (that is, FileMaker never injected), all requests to the PerformScript() or PerformScriptWithOption() methods are logged to the developer console. For example, if you call a script named New Invoice Line Item with some JSON, you'd see the following in the console:

{script: "New Invoice Line Item", param: '{invoiceId: "0C79ACAD-1D17-4387-A35F-DD61CA6D0147", type: "expense"}'}

While this feature may prove useful during debugging, if you wish to disable it, you can declare the following global variable:

window.OnFMReady.noLogging = true;

Regardless of whether or not the noLogging property is set, when operating in a FileMaker Pro/WebDirect context where the FileMaker object is injected, logging will never occur.

Responding

From a component-development context, calling of a FileMaker script is usually done in anticipation of a response from FileMaker. For example, if you invoke the New Invoice Line Item script from the example above, you might expect FileMaker to then call a JavaScript function, window.acceptNewLineItem(), passing JSON-defined properties of the newly-created line item into the function's argument:

{
  "id": "9C37599C-E64A-434B-90CE-321E00EACF46",
  "invoiceId": "0C79ACAD-1D17-4387-A35F-DD61CA6D0147",
  "description": "Enter Expense Description...",
  "type": "expense",
  "created": "2021-08-30"
}

During development, OnFMReady can simulate FileMaker responses to script requests by piping such requests into developer-defined functions. To engage this behavior, declare the global variable window.OnFMReady.respondTo as an object whose keys correspond to FileMaker script names, and whose property values are functions whose arguments accept either the script parameter or both the parameter and the option:

{
    'Name of FileMaker Script': (param, option = 0) => {
        // response logic here
    }
}

From within the response functions, you can then call those functions which would traditionally be invoked using Perform JavaScript in Web Viewer by FileMaker Pro/WebDirect. For example, to mock a FileMaker response to the example New Invoice Line Item script request, you might declare the following:

window.OnFMReady.respondTo = {
  'New Invoice Line Item': (param) => {
    const { invoiceId, type } = JSON.parse(param);

    const description = {
      expense: 'Enter Expense Description...',
      product: 'Enter Product Name...'
    }[type];

    const date = new Date();

    const payload = {
      id: Math.random().toString(36).substring(2),
      invoiceId,
      description,
      type,
      created: `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
    };

    window.acceptNewLineItem(JSON.stringify(payload));
  }
};

Now, when a request is made for the New Invoice Line Item FileMaker script, a fake JSON payload will be sent to the window.acceptNewLineItem() function — effectively providing usable data with which to test a component in the browser — without FileMaker. Of course, once operating inside of FileMaker Pro/WebDirect, none of this will occur, and script requests will be fulfilled as usual.

:pencil: NOTE: Utility Order

When using OnFMReady's utilities, you should declare noLogging, respondTo, and unmount after the point in your code where you've installed OnFMReady. Declaring before is optional, but you will need to use object syntax:

window.OnFMReady = {
  noLogging: true,
  respondsTo: { 'New Invoice Line Item': (param) => {} },
  unmount: true
};

Either method will work, and OnFMReady will not overwrite declarations made prior to installation, but I think the latter is nicer than the former.

How does this work?

I lost a weekend over this one. :frowning_face:

Version 2.0 of OnFMReady works on the principle of deferral, which leverages the synchronous nature of JavaScript code. To be synchronous is to operate at the same time, in order, or all at once. With the knowledge that JavaScript can only do one thing at a time (I'm not talking about async), we can deeply control that which is provided to our code at any given moment so long as we understand the order in which provisions are declared and that which is being anticipated by providers of such provisions. That was fun to read, wasn't it?

What's Known vs. What's Needed

All of that is a roundabout way of saying "there's a lot of moving parts, but they always move in-order, and if we know where they are, we can do things with them." With respect to the FileMaker object, we know the following:

  • We need it to exist before we use it, or a ReferenceError will be thrown by the browser, and our code will crash.
  • It doesn't exist immediately, because injection occurs after page load.
  • It can't already exist as anything except for null or undefined, or FileMaker Pro/WebDirect will never inject it.
    • null and undefined are not the same, but someone at Claris used loose type checking.

That last bullet point is the reason that I spent the last Saturday and Sunday of August 2021 sitting at my desk instead of going out on a date (there are other reasons too, but I'll sleep better if I blame Claris for this).

When FileMaker Pro or FileMaker WebDirect injects the FileMaker object for use into a web viewer layout object, it performs a simple one-line check:

if (window.FileMaker != null) return; // see, i told you it was loose type-checking

This check presents a really big problem, because it effectively states that if we want FileMaker to inject, it can't exist. However, as we outlined in the first bullet point, we need it to already exist if we want to use it immediately. Those are two realities which would seem to be impossibly at odds with each other.

"Tricking" the System

If you're thinking about a Proxy, let me stop you right there — go outside and enjoy your weekend. There is absolutely no way to wrap the window object inside of a proxy. If you're thinking about Function.caller, it's deprecated. If you're thinking about tracing with Error().stack, it's non-standard. If you're thinking of overriding Object.valueOf(), it won't affect equality operators. If you're thinking of Object.assign(null, {...}), that isn't a thing you can do. The only way we can have an object which both exists and does not exist, is to leverage the synchronous behavior of JavaScript — "tricking" FileMaker into being unaware. The object must exist when we need it, and then cease to exist entirely.

Luckily, well-tested code behaves with consistency, and once we understand the lifecycle of the FileMaker object, we can manipulate the order of things quite nicely. It really just comes down to this: injection of FileMaker is the last thing to happen. With that in-mind, when our code runs, the task is simple:

  1. We need an object that "pretends" to be FileMaker and which can collect any script execution requests made by either PerformScript() or PerformScriptWithOption().
  2. We need to destroy the object "pretending" to be FileMaker before the injection routine runs.
  3. FileMaker Pro or FileMaker WebDirect must be allowed to inject the "real" FileMaker object
  4. All of the collected script requests must be dispatched to FileMaker for handling via the injected/"real" FileMaker object.
  5. If FileMaker never injects, we must dispatch our script requests to the logger or their response functions.

Were I simply interested in deferral of script requests, this might have been a very easy task. However, the additional logging/responding utilities require continuous fulfillment of the FileMaker object, and thus the order of execution is extremely important.

Pursuing Predictability

JavaScript provides us with the ability to execute code every time a variable is accessed or written. This is done by defining the getter and setter of a target. Through this definition, we can return different values for an Object's properties depending on how the object is accessed. Notice, however, that I said an Object's properties, and not an Object. That's important.

Suppose we have an Object named Foo with the property Bar, and the method PerformScript() on Bar. If we wanted to make Bar appear as undefined all times except when accessing PerformScript, we could wrap Foo inside of a Proxy object, and check to see if Bar is being accessed with or without properties. Based on that condition, we could return undefined or the method PerformScript(), etc. This approach can be incredibly useful in many scenarios, but it unfortunately doesn't apply to manipulation of the FileMaker object because it's a property of the global scope, window, which cannot be re-asserted as a Proxy.

Another option in conditionally returning a value is attempting to trace what asked for the value. If we can know what's accessing our target, we can return a different value based on such a condition. In this pursuit, there's potential with a pretty low possibility of predictability when pattern-matching Error().stack (yes, I know what I said). As this is a non-standard property, implemented differently in each browser, it's entirely possible that code which works today won't work at all tomorrow.

Despite my wishes and best efforts to base declaration/assignment of FileMaker on dynamic evaluation of conditions, the most-predictable/repeatable interception method is to rely on the event cycles of JavaScript itself — using setTimeout() to carefully curate the order of each assignment.

Follow The Code

Thus far, I've reviewed the principles which were tested and applied to engage the behavior of OnFMReady, but to really get an idea of how things come together, it's best to follow the code itself. In the TypeScript source, I've left comment blocks prefixed with MS (standing for "milestone"), which indicate at what point in the code an anticipated event is expected to occur, relative to FileMaker. If you'd like to know more about the event cycles which occur prior to, during, and after FileMaker injection, take a look at the source, onfmready.ts. If you're unfamiliar with TypeScript, it's effectively the same as JavaScript — just with some added syntax thrown-in to help-out during development.

License

MIT — "Hell, yeah! Free software!"