Wicked Smart Data
LearnInsightsAboutContact
Sign InLet's Build
LearnInsightsAboutContact
Sign InLet's Build
Wicked Smart Data

Intelligence, automation, and expert execution — plus an elite library of free knowledge. We turn complexity into competitive advantage.

Start a conversation

Platform

  • Learning Paths
  • Insights
  • RSS Feed

Company

  • About
  • Contact
  • Work With Us

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Wicked Smart Data. All rights reserved.

Intelligence · Automation · Advantage

All Insights
Power Apps

Configuring Model-Driven App Form Event Handlers with JavaScript: Responding to OnLoad, OnSave, and OnChange Events

Learn how to write and register JavaScript event handlers for model-driven app forms in Dataverse. This hands-on lesson walks through OnLoad, OnSave, and OnChange events with realistic business logic, the formContext API, and step-by-step registration in the form designer.

🌱 Foundation16 min readOct 31, 2026Updated Oct 31, 2026
Configuring Model-Driven App Form Event Handlers with JavaScript: Responding to OnLoad, OnSave, and OnChange Events
On this page
  • Introduction
  • Prerequisites
  • Understanding Form Events: The Basics
  • Setting Up Your JavaScript Web Resource
  • Writing an OnLoad Handler
  • Writing an OnChange Handler
  • Writing an OnSave Handler
  • Registering Event Handlers on the Form
  • Adding the Web Resource to the Form
  • Registering an OnChange Handler
  • The formContext API: A Quick Reference
  • Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • Summary & Next Steps
  • Configuring Model-Driven App Form Event Handlers with JavaScript: Responding to OnLoad, OnSave, and OnChange Events to Drive Dynamic Form Behavior in Dataverse

    Introduction

    Picture this: your sales team uses a model-driven app to manage service contracts. When a rep selects "Government" as the account type, several fields — compliance certification number, procurement reference, and contract review date — suddenly need to become visible and required. When they choose "Commercial," those fields should disappear entirely. And when they save a record with a contract value above $500,000, the form should warn them that executive approval is required before submission.

    You could handle some of this with business rules in Dataverse, which offer a no-code approach to field visibility and requirements. But business rules have limits — they can't make async callouts, they can't compute complex multi-field logic, and they can't interact with the full form context the way JavaScript can. When your form behavior needs to get genuinely dynamic, JavaScript form event handlers are the tool you reach for.

    By the end of this lesson, you'll be able to write and register JavaScript event handlers for the three most important model-driven form events: OnLoad, OnSave, and OnChange. You'll understand what the form context API gives you, how to register your code against specific events, and how to avoid the mistakes that send developers chasing phantom bugs for hours.

    What you'll learn:

    • What form events are and why they exist in model-driven apps
    • How to use the formContext API to read and manipulate form data
    • How to write OnLoad, OnSave, and OnChange event handlers with realistic business logic
    • How to register JavaScript libraries on a form using the form editor
    • How to debug and troubleshoot common event handler failures

    Prerequisites

    You should be comfortable navigating Power Apps and working with model-driven apps at a basic level. If you're new to model-driven apps, work through Building Your First Model-Driven App: Site Map, Tables, Forms, and Views first. You should also understand what columns and tables are in Dataverse — if those terms are fuzzy, start with Dataverse Fundamentals: Tables, Columns, and Rows Explained for Power Apps Makers.

    You don't need to be a JavaScript expert, but you should be comfortable reading and writing basic functions, variables, and if/else logic.


    Understanding Form Events: The Basics

    A form event is a trigger that fires when something happens on a model-driven app form. The platform watches what users do — opening a record, changing a field value, clicking Save — and when those actions occur, it fires the corresponding event. If you've registered a JavaScript function against that event, your code runs.

    Think of it like a relay race. The platform runs its leg of the race (rendering the form, processing the save), then it hands the baton to your JavaScript function, which runs its leg before handing control back. You're not replacing the platform's behavior — you're extending it.

    There are three events you'll work with constantly:

    • OnLoad — fires after a form finishes loading, before the user can interact with it
    • OnSave — fires when the user clicks Save (or autosave triggers), before the record is written to the database
    • OnChange — fires when the value of a specific column changes, after the user tabs out of or otherwise leaves the field

    Each event gives your function a special object called the execution context (executionContext), which is the gateway to everything on the form. From it, you get the form context (formContext), which lets you read column values, show or hide fields, set field requirements, display notifications, and much more.

    Note

    In older Dataverse JavaScript code you may encounter, you'll see Xrm.Page used directly. This approach is deprecated and shouldn't be used in new code. Always work through executionContext.getFormContext() instead.


    Setting Up Your JavaScript Web Resource

    Before you can register an event handler, you need a place to put your code. In model-driven apps, JavaScript lives in web resources — files stored inside your Dataverse environment that the platform can serve to the browser.

    Here's how to create one:

    1. Open make.powerapps.com and navigate to your solution (always work inside a solution to keep your changes portable).
    2. In the solution, click New → Other → Web Resource.
    3. Set the Type to JavaScript (JS).
    4. Give it a meaningful name following the convention publisher_EntityName_FormName — for example, contoso_Contract_MainForm.
    5. Add your JavaScript code in the editor (we'll write the code in the sections below).
    6. Click Save.

    Tip

    Name your web resource and all your JavaScript functions with a consistent namespace prefix that matches your solution publisher (e.g., contoso). This prevents naming collisions when multiple customizations exist in the same environment. A function called handleOnLoad will eventually collide with someone else's function called handleOnLoad. A function called contoso_Contract_handleOnLoad almost certainly won't.

    Your JavaScript file will contain a namespace object that holds all your form functions. This is the standard pattern:

    // Web resource: contoso_Contract_MainForm.js
    
    var contoso = contoso || {};
    contoso.Contract = contoso.Contract || {};
    
    contoso.Contract.handleOnLoad = function (executionContext) {
        var formContext = executionContext.getFormContext();
        // OnLoad logic goes here
    };
    
    contoso.Contract.handleOnSave = function (executionContext) {
        var formContext = executionContext.getFormContext();
        // OnSave logic goes here
    };
    
    contoso.Contract.accountType_handleOnChange = function (executionContext) {
        var formContext = executionContext.getFormContext();
        // OnChange logic goes here
    };
    

    Notice the pattern: every handler accepts executionContext as its first argument, then immediately calls executionContext.getFormContext() to get the form context. Everything else flows from formContext.


    Writing an OnLoad Handler

    The OnLoad event fires once, right after the form renders. This is where you set the form's initial state based on existing data — useful for configuring visibility and field requirements when someone opens an existing record.

    Here's a realistic scenario: when a contract form opens, you want to check the account type and immediately show or hide the government-specific fields.

    contoso.Contract.handleOnLoad = function (executionContext) {
        var formContext = executionContext.getFormContext();
    
        // Read the current value of the "Account Type" choice column
        var accountTypeAttr = formContext.getAttribute("contoso_accounttype");
        if (!accountTypeAttr) return; // Column not on this form — exit safely
    
        var accountTypeValue = accountTypeAttr.getValue();
    
        // Apply field visibility based on the current value
        contoso.Contract.applyAccountTypeVisibility(formContext, accountTypeValue);
    };
    
    contoso.Contract.applyAccountTypeVisibility = function (formContext, accountTypeValue) {
        // 100000000 = "Government" option value in the choice column
        var isGovernment = (accountTypeValue === 100000000);
    
        // Show or hide government-specific fields
        formContext.getControl("contoso_compliancecertification").setVisible(isGovernment);
        formContext.getControl("contoso_procurementreference").setVisible(isGovernment);
        formContext.getControl("contoso_contractreviewdate").setVisible(isGovernment);
    
        // Set required level accordingly
        var requiredLevel = isGovernment ? "required" : "none";
        formContext.getAttribute("contoso_compliancecertification").setRequiredLevel(requiredLevel);
        formContext.getAttribute("contoso_procurementreference").setRequiredLevel(requiredLevel);
        formContext.getAttribute("contoso_contractreviewdate").setRequiredLevel(requiredLevel);
    };
    

    A few things worth noting here. We extracted the shared visibility logic into a separate function (applyAccountTypeVisibility) so both the OnLoad handler and the OnChange handler can call the same code without duplication. We also added a null-check (if (!accountTypeAttr) return) — if this web resource is ever registered on a form that doesn't include that column, the code won't crash; it just exits quietly.

    Warning

    formContext.getControl() and formContext.getAttribute() both use the logical column name (e.g., contoso_accounttype), not the display label. Logical names are lowercase and include the publisher prefix. You can find them in the column properties within the form editor, or in the table definition in Power Apps.


    Writing an OnChange Handler

    The OnChange event is column-specific — you register it against a particular column, and it fires each time that column's value changes. This is how you react in real time as a user fills out a form.

    Building on the scenario above, we want the government fields to appear or disappear the moment someone changes the Account Type dropdown:

    contoso.Contract.accountType_handleOnChange = function (executionContext) {
        var formContext = executionContext.getFormContext();
    
        var accountTypeAttr = formContext.getAttribute("contoso_accounttype");
        var accountTypeValue = accountTypeAttr ? accountTypeAttr.getValue() : null;
    
        contoso.Contract.applyAccountTypeVisibility(formContext, accountTypeValue);
    };
    

    Because we factored the logic into applyAccountTypeVisibility, the OnChange handler is just a few lines. It reads the new value and delegates. Clean and maintainable.

    Here's a second OnChange example that's independent — auto-calculating a contract end date when a start date and duration are filled in:

    contoso.Contract.duration_handleOnChange = function (executionContext) {
        var formContext = executionContext.getFormContext();
    
        var startDateAttr = formContext.getAttribute("contoso_startdate");
        var durationAttr = formContext.getAttribute("contoso_durationmonths");
    
        var startDate = startDateAttr ? startDateAttr.getValue() : null;
        var durationMonths = durationAttr ? durationAttr.getValue() : null;
    
        if (!startDate || !durationMonths) return; // Can't calculate without both values
    
        // Calculate end date
        var endDate = new Date(startDate);
        endDate.setMonth(endDate.getMonth() + durationMonths);
    
        formContext.getAttribute("contoso_enddate").setValue(endDate);
    };
    

    This fires whenever the duration field changes. If the start date is already set, it computes the end date automatically — a small quality-of-life improvement that prevents data entry errors.

    Key insight

    OnChange fires when the field value changes and the user moves focus away from the field. It does not fire on every keystroke. This is by design — you get the committed value, not intermediate typing states.


    Writing an OnSave Handler

    The OnSave event fires when the user initiates a save — whether they clicked the Save button, used the keyboard shortcut, or autosave triggered. Critically, it fires before the record is written to the database, which means you can cancel the save if validation fails.

    Here's a handler that checks contract value and warns users when executive approval is required:

    contoso.Contract.handleOnSave = function (executionContext) {
        var formContext = executionContext.getFormContext();
        var saveEvent = executionContext.getEventArgs();
    
        var contractValueAttr = formContext.getAttribute("contoso_contractvalue");
        var approvalStatusAttr = formContext.getAttribute("contoso_approvalstatus");
    
        if (!contractValueAttr || !approvalStatusAttr) return;
    
        var contractValue = contractValueAttr.getValue() || 0;
        var approvalStatus = approvalStatusAttr.getValue();
    
        // 100000001 = "Approved" in the Approval Status choice column
        var isApproved = (approvalStatus === 100000001);
    
        if (contractValue >= 500000 && !isApproved) {
            // Cancel the save
            saveEvent.preventDefault();
    
            // Show a form-level notification
            formContext.ui.setFormNotification(
                "Contracts valued at $500,000 or more require executive approval before saving. " +
                "Set Approval Status to Approved and resubmit, or contact your manager.",
                "ERROR",
                "approval_required_notification"
            );
        } else {
            // Clear the notification if the condition is no longer true
            formContext.ui.clearFormNotification("approval_required_notification");
        }
    };
    

    Two key APIs are at work here. First, executionContext.getEventArgs() gives you the save event arguments object. Calling saveEvent.preventDefault() on it tells the platform: don't write this record to the database. The form stays open with unsaved changes.

    Second, formContext.ui.setFormNotification() displays a banner at the top of the form. It takes three arguments: the message text, the notification type ("ERROR", "WARNING", or "INFO"), and a unique ID you assign. Using a consistent ID lets you later clear that specific notification with clearFormNotification().

    Tip

    Always clear form notifications when the condition that created them is resolved. A stale error banner that persists after the user fixes the problem will confuse and frustrate them.


    Registering Event Handlers on the Form

    Writing the JavaScript is only half the job. You need to tell the form editor which functions to call for which events. This is done through the form properties and column event configuration in the modern form designer.

    Adding the Web Resource to the Form

    1. Open your model-driven app, navigate to your table's main form, and click Edit form to open the form designer.
    2. In the form designer, find the Events tab in the right-side panel (you may need to click somewhere on the form canvas first to see form-level properties).
    3. Under On Load, click + Event Handler.
    4. In the dialog, set Library to your web resource (contoso_Contract_MainForm). If it doesn't appear, click + Add library and search for it.
    5. Set Function to contoso.Contract.handleOnLoad.
    6. Check Pass execution context as first parameter — this is critical. Without it, your function receives no context and will fail.
    7. Click Done.

    Repeat the same steps for the On Save event, pointing to contoso.Contract.handleOnSave.

    Registering an OnChange Handler

    OnChange handlers are registered on the column itself, not the form:

    1. Click on the Account Type column in the form canvas to select it.
    2. In the right panel, click the Events tab.
    3. Under On Change, click + Event Handler.
    4. Set the library and function to contoso.Contract.accountType_handleOnChange.
    5. Check Pass execution context as first parameter.
    6. Click Done.

    Repeat for the Duration field with its corresponding handler.

    After registering all your events, click Save and then Publish in the form designer. Changes won't appear in the live app until you publish.

    Warning

    The "Pass execution context as first parameter" checkbox must be checked for every event handler. If you forget it, your function will be called with no arguments, executionContext will be undefined, and the very first line of your handler will throw an error. This is one of the most common mistakes beginners make.


    The formContext API: A Quick Reference

    The formContext object is rich. Here's a practical reference for the methods you'll use most often:

    Reading and writing column values:

    // Get a value
    var value = formContext.getAttribute("column_name").getValue();
    
    // Set a value
    formContext.getAttribute("column_name").setValue(newValue);
    

    Controlling field visibility:

    formContext.getControl("column_name").setVisible(true);  // show
    formContext.getControl("column_name").setVisible(false); // hide
    

    Setting field requirement level:

    formContext.getAttribute("column_name").setRequiredLevel("required"); // mandatory
    formContext.getAttribute("column_name").setRequiredLevel("recommended"); // soft hint
    formContext.getAttribute("column_name").setRequiredLevel("none"); // not required
    

    Disabling and enabling fields:

    formContext.getControl("column_name").setDisabled(true);  // read-only
    formContext.getControl("column_name").setDisabled(false); // editable
    

    Displaying notifications:

    // Form-level banner
    formContext.ui.setFormNotification("Message text", "ERROR", "unique_id");
    formContext.ui.clearFormNotification("unique_id");
    
    // Field-level error indicator
    formContext.getControl("column_name").setNotification("Message", "unique_id");
    formContext.getControl("column_name").clearNotification("unique_id");
    

    Getting the form type (new vs. existing record):

    var formType = formContext.ui.getFormType();
    // 1 = Create, 2 = Update, 3 = Read Only, 4 = Disabled
    

    This last one is handy in OnLoad handlers — you often want to behave differently when a new record is being created versus when an existing one is being opened.

    For comparison, keep in mind that business rules in Dataverse cover many of these same scenarios (show/hide fields, set required levels) without any code. Reach for JavaScript when you need logic that business rules genuinely can't express — async operations, complex computed values, or integration with external context.


    Hands-On Exercise

    Let's put it all together. You'll build a JavaScript event handler set for a Support Case form with the following behavior:

    Scenario: Your company tracks support cases in Dataverse. The form has these columns:

    • contoso_casetype — Choice column: "Internal" (100000000) or "External" (100000001)
    • contoso_customername — Text column (only relevant for External cases)
    • contoso_customercontact — Lookup column (only relevant for External cases)
    • contoso_priority — Choice column: "Critical" (100000002)
    • contoso_escalationapproved — Two-option (Yes/No) column

    Requirements:

    1. On load: show/hide customer fields based on case type
    2. On change of case type: update field visibility dynamically
    3. On save: if priority is Critical and escalation is not approved, block the save and show an error

    Step 1: Create a JavaScript web resource named contoso_SupportCase_MainForm with this content:

    var contoso = contoso || {};
    contoso.SupportCase = contoso.SupportCase || {};
    
    // Shared logic for case type field visibility
    contoso.SupportCase.applyCaseTypeVisibility = function (formContext, caseTypeValue) {
        var isExternal = (caseTypeValue === 100000001);
        formContext.getControl("contoso_customername").setVisible(isExternal);
        formContext.getControl("contoso_customercontact").setVisible(isExternal);
        formContext.getAttribute("contoso_customername").setRequiredLevel(isExternal ? "required" : "none");
        formContext.getAttribute("contoso_customercontact").setRequiredLevel(isExternal ? "required" : "none");
    };
    
    // OnLoad handler
    contoso.SupportCase.handleOnLoad = function (executionContext) {
        var formContext = executionContext.getFormContext();
        var caseTypeAttr = formContext.getAttribute("contoso_casetype");
        var caseTypeValue = caseTypeAttr ? caseTypeAttr.getValue() : null;
        contoso.SupportCase.applyCaseTypeVisibility(formContext, caseTypeValue);
    };
    
    // OnChange handler for Case Type
    contoso.SupportCase.caseType_handleOnChange = function (executionContext) {
        var formContext = executionContext.getFormContext();
        var caseTypeAttr = formContext.getAttribute("contoso_casetype");
        var caseTypeValue = caseTypeAttr ? caseTypeAttr.getValue() : null;
        contoso.SupportCase.applyCaseTypeVisibility(formContext, caseTypeValue);
    };
    
    // OnSave handler
    contoso.SupportCase.handleOnSave = function (executionContext) {
        var formContext = executionContext.getFormContext();
        var saveEvent = executionContext.getEventArgs();
    
        var priorityAttr = formContext.getAttribute("contoso_priority");
        var escalationAttr = formContext.getAttribute("contoso_escalationapproved");
    
        var priority = priorityAttr ? priorityAttr.getValue() : null;
        var escalationApproved = escalationAttr ? escalationAttr.getValue() : false;
    
        if (priority === 100000002 && !escalationApproved) {
            saveEvent.preventDefault();
            formContext.ui.setFormNotification(
                "Critical priority cases require escalation approval before saving.",
                "ERROR",
                "critical_escalation_check"
            );
        } else {
            formContext.ui.clearFormNotification("critical_escalation_check");
        }
    };
    

    Step 2: Register all four handlers (OnLoad, OnSave, and two OnChange registrations — one for case type) on the Support Case main form using the steps described earlier.

    Step 3: Save and publish, then test by opening a new case, switching case type between Internal and External, and trying to save a Critical case without escalation approval.


    Common Mistakes & Troubleshooting

    "My handler doesn't seem to fire at all." The first thing to check is whether "Pass execution context as first parameter" is checked in the event registration dialog. The second is whether the web resource is actually added to the form's library list. The third is whether you published the form after saving changes. All three are extremely common causes of silent failures.

    "I get a console error: Cannot read properties of undefined (reading 'getFormContext')" This is the "pass execution context" checkbox problem. Your function is being called but executionContext is undefined. Fix the event registration.

    "My setVisible call isn't hiding the field." Verify that you're using formContext.getControl() (not getAttribute()). Visibility is a property of the control (the UI element), not the attribute (the data). Also confirm the logical name of the column matches exactly what's on the form.

    "The field I'm trying to hide reappears." If you're designing forms with sections and tabs, check whether the column appears on the form more than once. getControl() returns the first instance by default; if a second instance exists, it won't be hidden by your call.

    "My OnSave preventDefault isn't blocking the save." Ensure you're calling saveEvent.preventDefault() synchronously within the handler — not inside a callback or after an await. The event can only be cancelled during synchronous execution.

    Key insight

    Use your browser's developer tools (F12 → Console) liberally during development. JavaScript errors from web resources appear there. You can also add console.log() statements throughout your handlers to trace execution and inspect values without affecting users.

    For more complex server-side logic that should be enforced regardless of which interface a user uses, consider the complementary approach of configuring Dataverse table event plugins and real-time workflows. JavaScript form handlers are client-side only — a savvy user could bypass them with the API. Critical business rules belong at the data layer too.


    Summary & Next Steps

    You now know how to write and register JavaScript event handlers for the three core model-driven form events. You can use OnLoad to configure a form's initial state when it opens, OnChange to react dynamically as users fill in fields, and OnSave to validate data and block invalid submissions before they reach the database. You understand how to structure your code with a namespace pattern that scales cleanly, how to use the formContext API to read values and manipulate the UI, and how to register everything through the form designer.

    The key habits to take forward: always pass execution context, always use logical column names, always null-check attributes before reading them, and always clear notifications when the condition that triggered them is resolved.

    From here, you have several natural directions to explore. If you want to go deeper on the form design that hosts your event handlers, study Designing Model-Driven Forms: Sections, Tabs, Subgrids, and Quick View Forms. If you need truly sophisticated UI components that go beyond what JavaScript can manipulate, Extending Model-Driven Apps with PCF Controls is your next stop. And if you want to package your web resources professionally for deployment across environments, get familiar with Solutions for Model-Driven Apps.

    Work With Us

    From insight to implementation

    Reading is the start. When you're ready to build the data, automation, or AI systems behind it, our team turns strategy into shipped results.

    Let's Build

    Model-Driven Apps & Dataverse

    Previous

    Configuring Dataverse Column Security Profiles and Field-Level Permission Assignments: Restricting Sensitive Column Access Across Security Roles, Teams, and Users in Model-Driven Apps

    Related Insights

    Power AppsFoundation

    Connecting Your Canvas App to a SharePoint List: Adding, Editing, and Deleting Records with a Basic Form

    15 min
    Power AppsExpert

    Configuring Dataverse Column Security Profiles and Field-Level Permission Assignments: Restricting Sensitive Column Access Across Security Roles, Teams, and Users in Model-Driven Apps

    30 min
    Power AppsExpert

    Canvas App Conditional Formatting at Scale: Dynamically Styling Controls with Color, Visibility, and DisplayMode Based on Data State and User Role

    25 min

    On this page

    • Introduction
    • Prerequisites
    • Understanding Form Events: The Basics
    • Setting Up Your JavaScript Web Resource
    • Writing an OnLoad Handler
    • Writing an OnChange Handler
    • Writing an OnSave Handler
    • Registering Event Handlers on the Form
    • Adding the Web Resource to the Form
    • Registering an OnChange Handler
    • The formContext API: A Quick Reference
    • Hands-On Exercise
    • Common Mistakes & Troubleshooting
    • Summary & Next Steps