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 thedata
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.
- Allows the script to update the
- [
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 whenregisterAction
is called. CallingnotifyInteractiveReady
explicitly reinforces this intent, though the flag is the primary mechanism for keeping the iframe alive.)
- If your script uses
- [
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.
- If you want to stop proceeding the parent DOM, use
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:
- Script Registers Action: Your script calls [
context.registerAction("button#my-button", "click", "handleMyButtonClick")
]. - Parent Attaches Listener: The
ScriptManager
in the parent application receives thescriptRegisterAction
message. It uses the providedselector
("button#my-button"
) to find the element in the parent DOM and attaches an event listener for the specifiedeventType
("click"
). - User Triggers Event: The user interacts with the element in the parent application (e.g., clicks the button).
- Parent Notifies Iframe: The event listener in the parent is triggered. The
ScriptManager
then sends anexecuteUserAction
message back to the iframe where the script is running. This message includes theactionName
("handleMyButtonClick"
) and anactionPayload
containing details about the event that occurred (like target ID, tag name, input value, coordinates, key presses, etc.). - 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 namedglobalThis.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 actionName
s 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:- 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 finaldata
result. - Using [
context.setData(newData)
]: You can callcontext.setData()
one or more times during the script's execution. Each call updates thedata
state within the iframe and sends thenewData
back to the parent.
- Direct Return: If the main script function (the code within the IIFE) returns a value (anything other than
- If a script uses
context.setData
, the last state of thedata
object (either from a direct return or the lastsetData
call) when the script signals completion (by postingscriptResult
) 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 youruserScriptActions
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 parentScriptManager
. - Security: While the iframe sandbox (
allow-scripts allow-same-origin
) provides isolation, be aware that scripts have access to thecontext
object which allows them to trigger actions in the parent. Design your scripts and the availablecontext
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 isasync
, the value it resolves with will be used as the result if nocontext.setData
calls were made after the finalawait
. - 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
.