Sam's Dynamics, Power Platform & AI Blog

Monday, 14 September 2026

Working with Lookups, Option Sets, and Subgrids in Dynamics 365 Forms Using JavaScript

So far in this series we've covered basic form events and field-level show/hide/required logic. This post covers the three field types that trip people up most: lookups, option sets, and subgrids — including how to filter a lookup so users only see relevant records.

Reading and Setting Lookup Values

A lookup's value is always an array of objects, even though it only ever holds one record on most forms. Each object has id, name, and entityType:

var formContext = executionContext.getFormContext();
var lookupValue = formContext.getAttribute("parentcustomerid").getValue();

if (lookupValue) {
    var id = lookupValue[0].id;
        var name = lookupValue[0].name;
            var entityType = lookupValue[0].entityType;
            }
            
            // Setting a lookup value
            formContext.getAttribute("parentcustomerid").setValue([
                {
                        id: "00000000-0000-0000-0000-000000000000",
                                name: "Contoso Ltd",
                                        entityType: "account"
                                            }
                                            ]);

Filtering a Lookup's Results

By default a lookup searches the whole entity. To narrow it — say, only show contacts belonging to the account selected elsewhere on the form — use addPreSearch with a custom filter:

function onLoad(executionContext) {
    var formContext = executionContext.getFormContext();
        formContext.getControl("primarycontactid").addPreSearch(filterContactsByAccount);
        }
        
        function filterContactsByAccount(executionContext) {
            var formContext = executionContext.getFormContext();
                var accountValue = formContext.getAttribute("parentaccountid").getValue();
                
                    if (accountValue) {
                            var accountId = accountValue[0].id.replace("{", "").replace("}", "");
                                    var filter = "<filter type='and'>" +
                                                "<condition attribute='parentcustomerid' operator='eq' value='" + accountId + "' />" +
                                                            "</filter>";
                                                                    formContext.getControl("primarycontactid").addCustomFilter(filter, "contact");
                                                                        }
                                                                        }

One gotcha: addCustomFilter only applies to the next search, so it has to run inside addPreSearch every time, not just once on load.

Working with Option Sets

Option sets (choice fields) store and return the underlying numeric value, not the label. Use getOption or getOptions when you need the label or want to build the list dynamically:

var attribute = formContext.getAttribute("new_prioritylevel");

// Read/write the numeric value
var currentValue = attribute.getValue(); // e.g. 2
attribute.setValue(1);

// Get the label for the current value
var options = attribute.getOptions();
var selected = options.find(function (o) { return o.value === currentValue; });
console.log(selected ? selected.text : "none selected");

// Remove an option a user shouldn't be able to pick
var control = formContext.getControl("new_prioritylevel");
control.removeOption(3);

Refreshing a Subgrid

Subgrids don't automatically know when related data changes elsewhere on the form. Refresh one explicitly after an action that affects its rows:

var gridControl = formContext.getControl("Subgrid_Orders");
if (gridControl) {
    gridControl.refresh();
    
        // Reading the currently loaded rows
            var grid = gridControl.getGrid();
                var rows = grid.getRows();
                    console.log("Row count: " + rows.getLength());
                    }

Common Pitfalls

  • Treating a lookup value as a single object. It's always an array — forgetting the [0] is one of the most common form-script bugs.
  • Comparing option set values as strings. They're numbers; "2" === 2 is false in JavaScript.
  • Custom filters that don't reset. If you conditionally apply a filter, make sure the "else" branch clears it, or users will get a stale filter after changing the related field.
  • Calling grid methods before the subgrid has loaded. On OnLoad, wrap subgrid access in the control's onLoad event rather than assuming it's ready immediately.

What's Next

Next post: calling the Dataverse Web API directly from a form script — reading related records, creating child rows, and handling the async responses properly.

No comments:

Post a Comment

Packaging and Deploying Web Resources: A Practical ALM Workflow for Dynamics 365 JavaScript

You've written the script. You've unit-tested the logic and stepped through it in dev tools. It works perfectly on your form. Now co...