Overcharging Model‑Driven Forms! JavaScript 101

Overcharging Model‑Driven Forms! JavaScript 101

Mastering Form Events & Best Practices

🚀 Why JavaScript Still Matters

Model‑driven apps in Dynamics 365 / Power Apps are built on declarative logic, but there are still scenarios where you need to:

  • Hide/Show controls based on user role or data.
  • Validate fields before a record is saved.
  • Calculate values on the fly.
  • Call external services from the form.

JavaScript gives you that flexibility. In this post we’ll dive into the three core form events—OnLoad, OnSave, and OnChange—and share the best practices that keep your code fast, maintainable, and future‑proof.

Note: We’re not covering Ribbon Bar buttons here; but we have a post about it right here! Ribbon Bar Redefined: Classic Workbench vs. Modern Command‑Bar Designer

📚 Understanding Form Events

EventWhen It FiresTypical Use Cases
OnLoadAfter the form and all controls are rendered, but before data is fully loaded.Initializing UI (hide/show fields), setting default values, loading reference data via Web API.
OnSaveJust before the record is persisted to the database.Validation that can cancel the save (eventArgs.preventDefault()), updating related fields, calling server‑side actions.
OnChangeWhen a specific attribute’s value changes (user input or programmatic).Dynamic UI updates, cascading lookups, calculations, enabling/disabling controls.
Tip: Use formContext instead of the deprecated Xrm.Page. It works in both web and mobile clients.

⚙️ OnLoad – The “First‑Look” Event

What it does

  • Runs once per form open.
  • Ideal for setting up the UI before the user interacts with the form.

Sample Code

function onFormLoad(executionContext) {
    "use strict";
    var formCtx = executionContext.getFormContext();

    // 1️⃣ Hide a field on load
    formCtx.getControl("new_customfield").setVisible(false);

    // 2️⃣ Set a default value (if empty)
    var attr = formCtx.getAttribute("new_defaultvalue");
    if (!attr.getValue()) {
        attr.setValue(42);
    }

    // 3️⃣ Load reference data asynchronously
    loadReferenceData(formCtx);
}

async function loadReferenceData(formCtx) {
    try {
        const result = await Xrm.WebApi.retrieveMultipleRecords(
            "new_referenceentity",
            "?$select=new_name"
        );
        // Do something with the data...
    } catch (err) {
        console.error("Failed to load reference data:", err);
        formCtx.ui.setFormNotification(
            "Unable to load reference data.",
            "ERROR",
            "refDataError"
        );
    }
}

Best Practices

Recommendation
1️⃣Keep it light – Avoid heavy loops or synchronous Web API calls. Use async/await and handle errors gracefully.
2️⃣Cache data – Store results in a global (but scoped) variable if you’ll reuse them later.
3️⃣Use notifications, not alerts  formCtx.ui.setFormNotification() is non‑blocking and mobile‑friendly.
4️⃣Avoid hard‑coding IDs – Use schema names (new_fieldname) so your code survives entity renames.

💾 OnSave – The “Guard” Event

What it does

  • Fires just before the record is written to the database.
  • You can cancel the save, modify values, or trigger server‑side logic.

Sample Code

function onFormSave(executionContext) {
    "use strict";
    var formCtx = executionContext.getFormContext();
    var eventArgs = executionContext.getEventArgs();

    // Example: Ensure a required field is not blank
    var reqAttr = formCtx.getAttribute("new_requiredfield");
    if (!reqAttr.getValue()) {
        eventArgs.preventDefault(); // Cancel the save

        formCtx.ui.setFormNotification(
            "The 'Required Field' cannot be empty.",
            "ERROR",
            "requiredFieldError"
        );
        return;
    }

    // Example: Update a calculated field before saving
    var amount = formCtx.getAttribute("new_amount").getValue() || 0;
    formCtx.getAttribute("new_total").setValue(amount * 1.2);
}

Best Practices

Recommendation
1️⃣Validate, don’t just set – Use preventDefault() to stop bad data from reaching the server.
2️⃣Keep it synchronous – The save will wait for your code; avoid long‑running loops or blocking calls. If you need async work, consider a pre‑save custom action instead.
3️⃣Use eventArgs.getFormContext() if you’re registering the handler programmatically.
4️⃣Avoid UI changes that trigger other events – Changing an attribute inside OnSave can fire its own OnChange; guard against recursion with flags or setValue without triggering events (setValue(value, true) in newer SDKs).

🔄 OnChange – The “Reactive” Event

What it does

  • Fires whenever a specific field’s value changes (user input or programmatic).
  • Perfect for dynamic UI updates and calculations.

Sample Code

function onAmountChange(executionContext) {
    "use strict";
    var formCtx = executionContext.getFormContext();
    var control = executionContext.getEventArgs().getControl();

    if (control.getName() !== "new_amount") return; // Safety check

    var amount = control.getValue() || 0;
    var taxRate = 0.15;

    // Update related field
    formCtx.getAttribute("new_tax").setValue(amount * taxRate);
    formCtx.getAttribute("new_total").setValue(amount + (amount * taxRate));
}

Best Practices

Recommendation
1️⃣Guard against recursion – If you change a field inside its own OnChange, it can trigger itself again. Use a flag or setValue(value, true) to suppress events.
2️⃣Debounce rapid changes – For fields that users type into (e.g., lookup search), consider debouncing with setTimeout to avoid flooding the UI.
3️⃣Use getEventArgs().getControl() to identify which control fired the event, especially if you register a single handler for multiple attributes.
4️⃣Keep logic lightweight – Calculations should be simple; heavy work belongs in OnLoad or server‑side actions.

📌 Registering Events

  1. Open the form in the editor.
  2. Go to Form Properties → Events tab.
  3. Add your JavaScript web resource and select the function name for:
    • OnLoad
    • OnSave (check “Pre‑save” if you want it before validation)
    • OnChange (select the attribute in the drop‑down)

Programmatically

// Example: Register OnLoad programmatically
function registerEvents(executionContext) {
    var formCtx = executionContext.getFormContext();
    formCtx.data.entity.addOnLoad(onFormLoad);
    formCtx.data.entity.addOnSave(onFormSave);

    // For attribute change
    formCtx.getAttribute("new_amount").addOnChange(onAmountChange);
}
Tip: Registering events in the editor keeps your code cleaner and avoids accidental double‑registration.

⚠️ Common Pitfalls & Debugging Tips

IssueFix
Global variablesWrap your code in an IIFE or module. Use var/let inside functions.
Deprecated Xrm.PageReplace with formContext.
Using alert()Switch to setFormNotification() or Xrm.Navigation.openAlertDialog().
Heavy logic on OnLoadMove non‑critical work to a separate async function; use caching.
Infinite loops in OnChangeUse flags or the second parameter of setValue to suppress events.
Not handling async errorsWrap await calls in try/catch and log errors.

🚀 Performance & Maintainability

  • Minimize handlers: Combine logic where possible; avoid registering multiple functions for the same event unless necessary.
  • Cache data: Store results of Web API calls in a scoped variable to reuse across events.
  • Debounce rapid changes: Use setTimeout or libraries like lodash’s debounce.
  • Use strict mode: "use strict"; catches common mistakes early.
  • Follow naming conventions: Prefix functions with the form name (account_onLoad, etc.) to avoid collisions.

📌 Summary

EventWhen to use it
OnLoadUI initialization, default values, async data fetches.
OnSaveValidation that can cancel save, final field adjustments before persistence.
OnChangeDynamic UI updates, calculations, cascading lookups.

By keeping each event focused and following the best practices above, you’ll write JavaScript that’s fast, reliable, and easy to maintain—no more “overcharging” your forms with unnecessary code.


🔜 Next in the Series

Ribbon Bar Buttons: Customizing the Command Bar with JavaScript.

Stay tuned for a deep dive into adding custom buttons, handling click events, and integrating with Power Automate flows.

Happy coding! 🚀

Read more