diff --git a/src/.vuepress/config.js b/src/.vuepress/config.js index 86fd59817..bf8a44e6b 100644 --- a/src/.vuepress/config.js +++ b/src/.vuepress/config.js @@ -422,6 +422,7 @@ module.exports = config({ ['/en/developer-guide/applications/controls/gallerygrid.md', 'Gallery Grid'], ['/en/developer-guide/applications/controls/grid.md', 'Grid'], ['/en/developer-guide/applications/controls/filepreview.md', 'File Preview'], + ['/en/developer-guide/applications/controls/form.md', 'Form'], ['/en/developer-guide/applications/controls/formbutton.md', 'Form Button'], ['/en/developer-guide/applications/controls/htmlcontentdisplay.md', 'HTML Content Display'], ['/en/developer-guide/applications/controls/infocard.md', 'Info Card'], diff --git a/src/.vuepress/public/.attachments/applications/Controls/Form/form_overview.png b/src/.vuepress/public/.attachments/applications/Controls/Form/form_overview.png new file mode 100644 index 000000000..cdb45b739 Binary files /dev/null and b/src/.vuepress/public/.attachments/applications/Controls/Form/form_overview.png differ diff --git a/src/en/developer-guide/applications/controls/form.md b/src/en/developer-guide/applications/controls/form.md new file mode 100644 index 000000000..71c9e07c5 --- /dev/null +++ b/src/en/developer-guide/applications/controls/form.md @@ -0,0 +1,310 @@ +--- +Author: Dominik Brych +--- +# 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](/.attachments/applications/Controls/Form/form_overview.png) +*Form rendering a record with a notification bar, Save button, tabbed layout, and mixed field types across two columns.* + +## Bindings + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Property NameDescriptionOf TypeInputOutputUsageRequired
bindingFieldBound multiline text field for the PCF host slot.MultipleN/AN/Aboundtrue
ClientApiWebResourceNameJavaScript web resource that contains the handler functions.SingleLine.Textform-script.jsN/Ainputtrue
ClientApiConfigFunctionNameFunction called first to provide the form config.SingleLine.TextonLoadConfigN/Ainputtrue
ClientApiFormContextFunctionNameFunction called after the form is ready and receives formContext.SingleLine.TextonGetFormContextN/Ainputfalse
HeightNot fully supported at the moment. When set to 100%, it currently only adds extra padding around the control.SingleLine.Text100%N/Ainputfalse
+ +## 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: + +```js +function onLoadConfig(api) { + api.setConfig({ + formXml, + columns, + data, + metadata, + onSave, + }); +} +``` + +If you also want the runtime form context, add a second function: + +```js +function onGetFormContext(params) { + const { formContext } = params; + + formContext.ui.setFormNotification("Loaded", "INFO", "loaded"); +} +``` + +Typical property values then look like: + +- `ClientApiWebResourceName`: `form-script.js` +- `ClientApiConfigFunctionName`: `onLoadConfig` +- `ClientApiFormContextFunctionName`: `onGetFormContext` + +## Config shape + +The config passed to `setConfig(...)` supports this shape: + +```ts +{ + formXml: string; + columns: IColumn[]; + data?: Record; + metadata: { + PrimaryIdAttribute: string; + PrimaryNameAttribute: string; + }; + onSave?: (params: { + recordId: string; + updatedData: Record; + }) => Promise< + | { success: true } + | { success: false; error: string } + >; +} +``` + +> **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. + +```ts +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"], + }, + }, +]; +``` +*Column definitions* + +### Metadata + +`metadata` is the minimal record-level metadata object: + +```ts +{ + PrimaryIdAttribute: string; + PrimaryNameAttribute: string; +} +``` + +`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. + +```ts +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", +}; +``` +*Record payload* + +## Custom save + +You can provide your own save implementation through `onSave`. + +```js +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.", + }; + } +} +``` + +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: + +```js +function onGetFormContext(params) { + const { formContext } = params; +} +``` + +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: + +- [formContext](https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/clientapi-form-context) +- [formContext.data](https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/reference/formcontext-data) +- [formContext.ui](https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/reference/formcontext-ui) +- [attribute methods](https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/reference/attributes) +- [control methods](https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/reference/controls) + +```js +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(); + } + }); +} +``` +*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 + +```js +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"); +} +``` + +## 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`.