Client Scripting: Form Events & the Client API
Add intelligence to model-driven app forms with JavaScript. Learn the Client API object model, form event handlers, and how to call the Dataverse Web API from client scripts.
Adding brains to model-driven forms
Think of a smart home system.
Without automation, you manually turn lights on, adjust the thermostat, and lock the doors. With a smart system, things happen automatically: motion sensor triggers lights, temperature drops trigger heating, and the door locks when you leave.
Client scripts are the smart system for model-driven app forms. When a user opens a form (OnLoad), changes a field (OnChange), or saves a record (OnSave) — your JavaScript responds automatically. You can show or hide fields, set values, validate data, display notifications, and even call external APIs.
The Client API object model
The Client API gives you access to everything on the form:
| Object | Access Via | What It Controls |
|---|---|---|
| formContext | executionContext.getFormContext() | The form itself — all controls, fields, tabs |
| formContext.data.entity | Form data | Field values, save state, entity name |
| formContext.ui | Form UI | Tabs, sections, controls, notifications |
| formContext.getAttribute(“name”) | Specific field | Get/set value, add onChange handler |
| formContext.getControl(“name”) | Specific control | Show/hide, enable/disable, set label |
| Xrm.Navigation | Global | Open forms, dialogs, web resources |
| Xrm.WebApi | Global | CRUD operations on Dataverse tables |
| Xrm.Utility | Global | Show progress indicator, get entity metadata |
The formContext pattern
Every event handler receives an execution context. From that, you get the form context:
// Modern pattern — ALWAYS use this
function onFormLoad(executionContext) {
var formContext = executionContext.getFormContext();
// Read a field value
var priority = formContext.getAttribute("priority").getValue();
// Show/hide a control
if (priority === 1) {
formContext.getControl("escalation_notes").setVisible(true);
}
// Set a field as required
formContext.getAttribute("description").setRequiredLevel("required");
// Display a notification
formContext.ui.setFormNotification(
"This is a high-priority case", "WARNING", "priority_warning"
);
}
Exam tip: Xrm.Page is deprecated
If you see Xrm.Page.getAttribute(...) in an exam answer, it is almost certainly wrong. The modern approach is:
- Receive
executionContextas a parameter - Call
executionContext.getFormContext()to getformContext - Use
formContextfor all form operations
Xrm.Page still works (backwards compatibility) but is deprecated and does not support multi-session apps where multiple forms can be open simultaneously.
Event handler registration
Event handlers connect your JavaScript functions to form events. There are two ways to register them:
| Method | Form Designer (Recommended) | Programmatic (addOnChange) |
|---|---|---|
| How | Configure in the form editor UI under Events tab | Call formContext.getAttribute('field').addOnChange(handler) in code |
| Visible to makers | Yes — shows in form properties | No — hidden in JavaScript |
| Solution-aware | Yes — travels with the form customisation | Depends on when/where the code runs |
| Best for | OnLoad, OnSave, field OnChange events you always want | Dynamic handlers added conditionally at runtime |
| Exam preference | Almost always correct | Correct only when dynamic registration is needed |
Calling the Dataverse Web API from scripts
The Xrm.WebApi object lets you perform CRUD operations from client scripts without leaving the form:
// Read a related record
function loadAccountDetails(formContext) {
var accountRef = formContext.getAttribute("parentaccountid").getValue()[0];
var accountId = accountRef.id.replace(/[{}]/g, ""); // Strip braces from GUID
Xrm.WebApi.retrieveRecord("account", accountId,
"?$select=name,revenue,numberofemployees"
).then(
function(result) {
// Use the data
formContext.getControl("account_info").setLabel(
result.name + " - Revenue: $" + result.revenue
);
},
function(error) {
console.error("Error: " + error.message);
}
);
}
// Create a related record
function createFollowUpTask(formContext) {
var caseId = formContext.data.entity.getId();
var record = {
subject: "Follow up on case",
"regardingobjectid_incident@odata.bind": "/incidents(" + caseId + ")",
scheduledend: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days from now
};
Xrm.WebApi.createRecord("task", record).then(
function(result) {
formContext.ui.setFormNotification(
"Follow-up task created", "INFO", "task_created"
);
},
function(error) {
formContext.ui.setFormNotification(
"Error creating task: " + error.message, "ERROR", "task_error"
);
}
);
}
Scenario: Kai adds smart form behaviour
Kai is building a shipment form at LogiFlow. When the destination country changes:
- OnChange handler fires on the
destinationcountryfield - Script calls
Xrm.WebApi.retrieveMultipleRecords()to fetch customs requirements for that country - If the country requires customs documentation, the script shows the “Customs Documents” tab and sets the “Customs Declaration” field as required
- If the country is domestic, the script hides the tab and clears the requirement
This logic is registered through the form designer (OnChange on destinationcountry) and uses formContext for all form interactions. The Web API call is async — the form remains responsive while data loads.
Kai registers an OnLoad handler on a model-driven form. The handler should read the Priority field and conditionally show a tab. Which code pattern is correct?
🎬 Video coming soon
Next up: Commands, Buttons & Custom Page Navigation — customising the command bar with Power Fx and JavaScript.