Form


# Form

# Description

Form renders a form from FormXml, columns, data, and metadata that you provide through a Dataverse client API web resource.

# Visual example

Form control rendering a sales-to-delivery handoff record with tabbed sections Form rendering a record with a notification bar, Save button, tabbed layout, and mixed field types across two columns.

# Bindings

Property Name Description Of Type Input Output Usage Required
bindingField Bound multiline text field for the PCF host slot. Multiple N/A N/A bound true
ClientApiWebResourceName JavaScript web resource that contains the handler functions. SingleLine.Text form-script.js N/A input true
ClientApiConfigFunctionName Function called first to provide the form config. SingleLine.Text onLoadConfig N/A input true
ClientApiFormContextFunctionName Function called after the form is ready and receives formContext. SingleLine.Text onGetFormContext N/A input false
Height Not fully supported at the moment. When set to 100%, it currently only adds extra padding around the control. SingleLine.Text 100% N/A input false

# Integration model

# Runtime flow

At runtime the control:

  1. Calls Xrm.Utility.executeFunction(ClientApiWebResourceName, ClientApiConfigFunctionName, [{ setConfig }])
  2. Expects that function to call setConfig(...)
  3. Builds the form from that config
  4. If ClientApiFormContextFunctionName is set, calls Xrm.Utility.executeFunction(ClientApiWebResourceName, ClientApiFormContextFunctionName, [{ formContext }])

# Web resource contract

Your JavaScript web resource should expose a config function like this:

function onLoadConfig(api) {
  api.setConfig({
    formXml,
    columns,
    data,
    metadata,
    onSave,
  });
}
1
2
3
4
5
6
7
8
9

If you also want the runtime form context, add a second function:

function onGetFormContext(params) {
  const { formContext } = params;

  formContext.ui.setFormNotification("Loaded", "INFO", "loaded");
}
1
2
3
4
5

Typical property values then look like:

  • ClientApiWebResourceName: form-script.js
  • ClientApiConfigFunctionName: onLoadConfig
  • ClientApiFormContextFunctionName: onGetFormContext

# Config shape

The config passed to setConfig(...) supports this shape:

{
  formXml: string;
  columns: IColumn[];
  data?: Record<string, any>;
  metadata: {
    PrimaryIdAttribute: string;
    PrimaryNameAttribute: string;
  };
  onSave?: (params: {
    recordId: string;
    updatedData: Record<string, any>;
  }) => Promise<
    | { success: true }
    | { success: false; error: string }
  >;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Note: formXml, columns, and metadata are required. data is optional — when omitted, the form starts with an empty record object.

# Columns

columns is the field-definition array for the record. Each item describes one field that the form can bind to, including its logical name, display name, and data type.

In practice, this should follow the same IColumn[] shape used by the rest of the Base Controls stack.

const columns = [
  {
    name: "name",
    alias: "name",
    displayName: "Name",
    dataType: "SingleLine.Text",
    metadata: { IsValidForUpdate: true },
  },
  {
    name: "primarycontactid",
    alias: "primarycontactid",
    displayName: "Primary Contact",
    dataType: "Lookup.Simple",
    metadata: {
      IsValidForUpdate: true,
      Targets: ["contact"],
    },
  },
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Column definitions

# Metadata

metadata is the minimal record-level metadata object:

{
  PrimaryIdAttribute: string;
  PrimaryNameAttribute: string;
}
1
2
3
4

PrimaryIdAttribute identifies the record id field and PrimaryNameAttribute identifies the primary text field for the record.

# Data

data is the actual record payload. Its structure should match what you normally get back from Dataverse when retrieving a record. That means:

  • scalar values live under their logical names
  • lookup values follow the Dataverse lookup pattern, for example:
    • _primarycontactid_value
    • _primarycontactid_value@OData.Community.Display.V1.FormattedValue
    • _primarycontactid_value@Microsoft.Dynamics.CRM.lookuplogicalname
  • formatted values and option-set values can remain in the Dataverse-style record shape

So the control should be fed with a Dataverse-shaped record object, not a custom transformed payload.

const data = {
  accountid: "11111111-1111-1111-1111-111111111111",
  name: "Contoso Ltd.",
  telephone1: "+420 123 456 789",
  "_primarycontactid_value": "22222222-2222-2222-2222-222222222222",
  "_primarycontactid_value@OData.Community.Display.V1.FormattedValue": "Adele Vance",
  "_primarycontactid_value@Microsoft.Dynamics.CRM.lookuplogicalname": "contact",
};
1
2
3
4
5
6
7
8

Record payload

# Custom save

You can provide your own save implementation through onSave.

async function onSave(params) {
  const { recordId, updatedData } = params;

  try {
    await Xrm.WebApi.updateRecord("account", recordId, updatedData);
    return { success: true };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error.message : "Save failed.",
    };
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

When save succeeds, the control treats the updated fields as saved. When save fails, the returned error message is surfaced back through the form save flow.

# Form context

If ClientApiFormContextFunctionName is configured, your handler receives:

function onGetFormContext(params) {
  const { formContext } = params;
}
1
2
3

The exposed formContext is meant to work in the same way as the Microsoft model-driven app form context for the documented surface. The main entry points are formContext.data, formContext.ui, formContext.getAttribute(name), and formContext.getControl(name).

For the broader conceptual model, see the Microsoft docs for:

function onGetFormContext(params) {
  const { formContext } = params;

  const phone = formContext.getAttribute("telephone1");
  phone?.addOnChange(() => {
    console.log(phone.getValue());
  });

  formContext.data.entity.addOnSave((executionContext) => {
    const eventArgs = executionContext.getEventArgs();
    if (!formContext.data.isValid()) {
      eventArgs.preventDefault();
    }
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Reacting to field changes and gating save on validity

Note: Execution-context support is currently very limited. In most handlers it is effectively an empty object. The main meaningful behavior today is in save handlers, where executionContext.getEventArgs().preventDefault() can stop the save.

# Minimal example

function onLoadConfig(api) {
  api.setConfig({
    formXml: formXml,
    columns: columns,
    data: record,
    metadata: {
      PrimaryIdAttribute: "accountid",
      PrimaryNameAttribute: "name",
    },
    onSave: async ({ recordId, updatedData }) => {
      await Xrm.WebApi.updateRecord("account", recordId, updatedData);
      return { success: true };
    },
  });
}

function onGetFormContext(params) {
  const { formContext } = params;
  formContext.ui.setFormNotification("Ready", "INFO", "ready");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# Local testing

The control's repo also includes a local testing path under Form/local-dev/:

  • localMock.ts creates a local Xrm instance, adds Utility.executeFunction, and reloads the control through requestRender
  • form-script.js contains the local config/form-context handlers used by that mock path

That local mock is only for running the control outside a real Dataverse host. The primary integration model for real usage is still the web resource contract described above.

Note: The mock is currently disabled: the createLocalMock(this, context, container) call in index.ts's init is commented out (since the control switched to the published @talxis/base-controls package). To use it locally, uncomment that line before running npm start.