Skip to main content

Custom Scripts: Extending Application Functionality

Custom scripts provide a powerful way to extend the application's functionality for specific entities and events. They allow developers to inject custom logic that can interact with the application's data and user interface.

Execution Environment

Custom scripts are executed within a sandboxed <iframe> environment. This provides a layer of isolation, preventing scripts from directly interfering with the main application's global scope or sensitive data, while still allowing controlled interaction through a defined API.

Script Execution Context

When a script is executed, it is provided with two main arguments: data and context.

(function(data, context) {
// Your script code here
})(data, context);

The data Object

The data object is a copy of the primary data related to the entity and event that triggered the script (e.g., the data of a selected item, a purchase order, etc.).

  • Scripts receive a deep copy of the initial data.
  • Modifications made directly to this data object within the script will be captured.
  • If the script function returns a value (that is not undefined), this returned value will be used as the new state of the data object in the parent application, potentially replacing the original data.

The context Object

The context object provides a set of functions that allow the script to interact with the parent application in a controlled manner.

  • [context.setData(newData)]:
    • Allows the script to update the data object asynchronously or at multiple points during execution.
    • Calling this function sends the newData back to the parent application, which can then update its state.
    • This is useful for scripts that perform asynchronous operations or need to provide incremental updates to the data.
  • [context.notify(message, type)]:
    • Sends a notification from the script to the main application.
    • The parent application will display this message to the user.
    • message: The text content of the notification.
    • type: The type of notification (e.g., 'info', 'success', 'warning', 'error').
  • [context.registerAction(selector, eventType, actionName)]:
    • Crucial for interactive scripts. This function allows the script to register event listeners on specific DOM elements within the parent application's user interface.
    • selector: A CSS selector string targeting the element(s) in the parent DOM you want to attach a listener to.
    • eventType: The type of DOM event to listen for (e.g., 'click', 'change', 'input').
    • actionName: A unique string name for this specific action. This name is used to identify which part of your script should handle the event when it occurs.
  • [context.notifyInteractiveReady()]:
    • If your script uses context.registerAction to set up interactive elements, you should call this function.
    • This signals to the parent ScriptManager that the iframe needs to remain active after the initial script execution finishes to handle potential user interactions triggered by the registered actions.
    • (Note: The ScriptManager also checks an internal flag (_didRegisterAction) which is set when registerAction is called. Calling notifyInteractiveReady explicitly reinforces this intent, though the flag is the primary mechanism for keeping the iframe alive.)
  • [context.abort(reason)]:
    • If you want to stop proceeding the parent DOM, use context.abort().
    • This function is useful for custom data validation before save.
    • reason is optional.

Interacting with the Parent DOM (Action Registration)

The [context.registerAction()] function is the gateway for scripts to make elements in the main application interactive. This enables scenarios where a script might display information and then wait for user input or interaction with specific UI elements.

Here's the lifecycle:

  1. Script Registers Action: Your script calls [context.registerAction("button#my-button", "click", "handleMyButtonClick")].
  2. Parent Attaches Listener: The ScriptManager in the parent application receives the scriptRegisterAction message. It uses the provided selector ("button#my-button") to find the element in the parent DOM and attaches an event listener for the specified eventType ("click").
  3. User Triggers Event: The user interacts with the element in the parent application (e.g., clicks the button).
  4. Parent Notifies Iframe: The event listener in the parent is triggered. The ScriptManager then sends an executeUserAction message back to the iframe where the script is running. This message includes the actionName ("handleMyButtonClick") and an actionPayload containing details about the event that occurred (like target ID, tag name, input value, coordinates, key presses, etc.).
  5. Script Handles Action: The iframe receives the executeUserAction message. It looks for a corresponding handler function within a global object defined by your script, typically named globalThis.userScriptActions.

Handling Registered Actions in the Script

To handle the actions registered via [context.registerAction()], your script must define a global object, conventionally named globalThis.userScriptActions. This object should contain functions whose names match the actionNames you used when calling registerAction.

// Inside your custom script:
globalThis.userScriptActions = {
// This function will be called when the element matching "#my-button" is clicked
handleMyButtonClick: function(currentData, ctx, actionPayload) {
// currentData: The latest state of the data object within the iframe.
// ctx: The same context object provided during initial script execution.
// actionPayload: An object containing details about the event that triggered this action.
console.log('Button was clicked!', actionPayload);

// Use the context object to interact with the parent
ctx.notify('Button click handled!', 'success');

// You can update the data based on the action
const newData = { ...currentData, clicked: true, lastClickTime: new Date().toISOString() };
ctx.setData(newData); // Update data in the iframe and send to parent
},

// You can define multiple action handlers
handleAnotherAction: function(data, ctx, payload) {
// ... handle another action
}
};

If a script registers actions using [context.registerAction()], the initial script execution will signal completion (e.g., by posting scriptResult), but the iframe will not be immediately cleaned up by the ScriptManager. It remains active, waiting for executeUserAction messages triggered by the registered parent DOM events. The iframe will persist until explicitly cleaned up (e.g., when the entity view is closed or the cleanupActiveIframes method is called in the parent).

Data Flow and Return Values

Understanding how data flows is key to writing effective scripts:

  • The data object passed to your script initially is a copy of the relevant entity data from the parent application.
  • You have two primary ways to influence the final data state that the parent application will receive:
    1. Direct Return: If the main script function (the code within the IIFE) returns a value (anything other than undefined), this value will be used as the final data result.
    2. Using [context.setData(newData)]: You can call context.setData() one or more times during the script's execution. Each call updates the data state within the iframe and sends the newData back to the parent.
  • If a script uses context.setData, the last state of the data object (either from a direct return or the last setData call) when the script signals completion (by posting scriptResult) will be considered the final result for the initial execution phase.
  • If a script registers actions, the initial execution might finish, but subsequent calls to context.setData() from within your userScriptActions handlers will continue to update the data state in the parent application.

Simple Examples

Here are a few examples demonstrating common script patterns:

Example 1: Modifying Data Directly

This script receives item data and adds a new property to it.

// Script for an 'Item' entity on 'load' event
// Adds a custom property to the item data.
console.log("Original item data:", data);

// Modify the data object directly
data.customProperty = "Hello from script!";
data.processedByScript = true;

// The modified 'data' object is implicitly returned
// when the script finishes execution.
// Alternatively, you can explicitly return it:
// return data;

Example 2: Using context.setData and context.notify

This script validates a field in a Purchase Order and notifies the user.

// Script for a 'PurchaseOrder' on 'submit' event
// Validates a field and notifies the user.

if (!data.supplierReference || data.supplierReference.trim() === "") {
// Use context.notify to send an error message to the user
context.notify("Supplier Reference is missing and is required for submission!", "error");
// Note: This script doesn't prevent the parent action (submission) itself,
// it only provides feedback. Stopping the parent action would require
// additional mechanisms not shown here.
} else {
// Use context.notify for a success message
context.notify("Supplier Reference found. Purchase Order validation passed.", "success");
// You could also update data here if needed:
// const updatedData = { ...data, validated: true };
// context.setData(updatedData);
}

// This script doesn't return data directly, so the original data is kept
// unless context.setData was called.

Example 3: Registering an Action

This script assumes a button with the ID customItemActionBtn exists in the parent application's DOM. It registers a click listener on this button and defines a handler for the action.

// Script for an 'Item' entity on 'load' event
// Adds a button to the parent DOM (assuming a button with id="customItemActionBtn" exists in parent)
// and handles its click.

context.notify("Script loaded. Look for a button with ID 'customItemActionBtn' in the parent DOM and click it.", "info");

// Register a click action on the button in the parent DOM
context.registerAction("#customItemActionBtn", "click", "handleItemButtonClick");

// Define the global object to hold action handlers
globalThis.userScriptActions = {
// This function will be called when the registered button is clicked
handleItemButtonClick: function(currentData, ctx, actionPayload) {
ctx.notify(`Button clicked! Item: ${currentData.name}`, 'success');

// Update the data based on the action
const newData = {
...currentData,
lastAction: 'button_clicked',
actionTimestamp: new Date().toISOString(),
clickDetails: actionPayload // Store details about the click event
};
ctx.setData(newData); // Update data in the iframe and send to parent
}
};

// This script doesn't return data directly, but sets up an interaction.
// Because context.registerAction was called, the iframe will remain active
// to handle clicks on the button.

Best Practices and Notes

  • Idempotency: Where possible, design your scripts to be idempotent, meaning executing them multiple times with the same input has the same effect as executing it once. This can help prevent unexpected behavior if scripts are triggered more than once.
  • Performance: Be mindful of the code you run in scripts. Complex calculations, long loops, or excessive DOM manipulations (if you were somehow able to achieve them, though the sandbox limits this) can impact the user experience.
  • Error Handling: Use try...catch blocks within your script code to gracefully handle errors. Use [context.notify(errorMessage, 'error')] to inform the user of issues. Errors are also caught by the iframe executor and reported to the parent ScriptManager.
  • Security: While the iframe sandbox (allow-scripts allow-same-origin) provides isolation, be aware that scripts have access to the context object which allows them to trigger actions in the parent. Design your scripts and the available context functions carefully. Avoid executing arbitrary, untrusted code.
  • Asynchronous Operations: Scripts can perform asynchronous operations (e.g., fetch calls, setTimeout). Ensure you handle promises correctly. If your script is async, the value it resolves with will be used as the result if no context.setData calls were made after the final await.
  • Interactive Scripts Cleanup: Remember that if a script registers actions, the iframe remains active. Ensure you have a mechanism (like closing the view associated with the entity) that eventually triggers the cleanup of these active iframes via the ScriptManager.