From b0ec7e6eb7c2fc5227d7070e4c758156f6fcce4e Mon Sep 17 00:00:00 2001 From: Zdenek Srejber Date: Fri, 7 Aug 2026 08:12:48 +0200 Subject: [PATCH] Add form validation initialization documentation --- src/.vuepress/config.js | 20 +- .../controls/{form.md => Form/general.md} | 2 +- .../applications/controls/Form/validation.md | 173 ++++++++++++++++++ 3 files changed, 188 insertions(+), 7 deletions(-) rename src/en/developer-guide/applications/controls/{form.md => Form/general.md} (99%) create mode 100644 src/en/developer-guide/applications/controls/Form/validation.md diff --git a/src/.vuepress/config.js b/src/.vuepress/config.js index bf8a44e6..dae0881e 100644 --- a/src/.vuepress/config.js +++ b/src/.vuepress/config.js @@ -71,6 +71,7 @@ module.exports = config({ logo: '/assets/img/talxis_logo.png', darkLogo: '/assets/img/talxis_logo_white.png', smoothScroll: true, + blog: false, pageInfo: ['Author', 'Time', 'ReadTime'], footer: { display: true, @@ -392,9 +393,9 @@ module.exports = config({ ['/en/developer-guide/applications/controls/GeneralInformation/authentication.md', 'Authentication'], ] }, + ['/en/developer-guide/applications/controls/announcementcard.md', 'Accouncement Card'], ['/en/developer-guide/applications/controls/addresspicker.md', 'Address Picker'], ['/en/developer-guide/applications/controls/annotations.md', 'Annotations'], - ['/en/developer-guide/applications/controls/announcementcard.md', 'Accouncement Card'], ['/en/developer-guide/applications/controls/attachmentsgrid.md', 'Attachments Grid'], ['/en/developer-guide/applications/controls/codeeditor.md', 'Code Editor'], ['/en/developer-guide/applications/controls/colorfuloptionset.md', 'Colorful Optionset'], @@ -403,8 +404,8 @@ module.exports = config({ ['/en/developer-guide/applications/controls/datasetgeolocationviewer.md', 'Dataset Geolocation Viewer'], ['/en/developer-guide/applications/controls/dynamicattribute.md', 'Dynamic Attribute'], ['/en/developer-guide/applications/controls/dynamicattributegrid.md', 'Dynamic Attribute Grid'], - ['/en/developer-guide/applications/controls/emailpicker.md', 'Email Picker'], ['/en/developer-guide/applications/controls/emaildesigner.md', 'Email Designer'], + ['/en/developer-guide/applications/controls/emailpicker.md', 'Email Picker'], { title: 'File Explorer', collapsable: true, @@ -417,13 +418,20 @@ module.exports = config({ ['/en/developer-guide/applications/controls/FileExplorer/custom-view-columns.md', 'Custom View Columns'], ] }, - ['/en/developer-guide/applications/controls/filepicker.md', 'File Picker'], ['/en/developer-guide/applications/controls/FileExplorer/fileexplorer.md', 'File Explorer'], - ['/en/developer-guide/applications/controls/gallerygrid.md', 'Gallery Grid'], - ['/en/developer-guide/applications/controls/grid.md', 'Grid'], + ['/en/developer-guide/applications/controls/filepicker.md', 'File Picker'], ['/en/developer-guide/applications/controls/filepreview.md', 'File Preview'], - ['/en/developer-guide/applications/controls/form.md', 'Form'], + ['/en/developer-guide/applications/controls/gallerygrid.md', 'Gallery Grid'], + { + title: 'Form', + collapsable: true, + children: [ + ['/en/developer-guide/applications/controls/Form/general.md', 'General'], + ['/en/developer-guide/applications/controls/Form/validation.md', 'Validation'], + ] + }, ['/en/developer-guide/applications/controls/formbutton.md', 'Form Button'], + ['/en/developer-guide/applications/controls/grid.md', 'Grid'], ['/en/developer-guide/applications/controls/htmlcontentdisplay.md', 'HTML Content Display'], ['/en/developer-guide/applications/controls/infocard.md', 'Info Card'], ['/en/developer-guide/applications/controls/invoicerecognition.md', 'Invoice Recognition'], diff --git a/src/en/developer-guide/applications/controls/form.md b/src/en/developer-guide/applications/controls/Form/general.md similarity index 99% rename from src/en/developer-guide/applications/controls/form.md rename to src/en/developer-guide/applications/controls/Form/general.md index 71c9e07c..ca9d6e59 100644 --- a/src/en/developer-guide/applications/controls/form.md +++ b/src/en/developer-guide/applications/controls/Form/general.md @@ -307,4 +307,4 @@ The control's repo also includes a local testing path under `Form/local-dev/`: 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`. +> **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`. \ No newline at end of file diff --git a/src/en/developer-guide/applications/controls/Form/validation.md b/src/en/developer-guide/applications/controls/Form/validation.md new file mode 100644 index 00000000..204f8c9e --- /dev/null +++ b/src/en/developer-guide/applications/controls/Form/validation.md @@ -0,0 +1,173 @@ +--- +Author: Zdenek Srejber +--- +# Form Validation In Model-Driven Forms + +## Problem + +When the Form PCF is embedded inside a Power Apps model-driven form, there are effectively two validation surfaces: + +- the main model-driven form +- the embedded Form PCF + +The standard ribbon Save button already knows how to validate the main form. It does not automatically know whether your embedded Form PCF is valid, whether it has blocking errors, or whether it should stop the save. + +Without an explicit bridge between those two layers, users can click Save on the main form while the PCF still contains invalid data. That usually leads to invalid state. + +The practical goal is simple: + +1. User clicks Save in the main form ribbon. +2. Main form save handler asks the embedded Form PCF to validate itself. +3. If the PCF is invalid, the handler cancels the host save. +4. If both the host form and the PCF are valid, save continues. + +## Simplified guide + +1. Configure the Form PCF with `ClientApiFormContextFunctionName` so your web resource receives the embedded Form PCF `formContext`. +2. Store that Form PCF `formContext` where the host form script can access it during save. +3. Register a main model-driven form `OnSave` handler that validates both the host form and the embedded Form PCF. +4. Call `preventDefault()` when validation fails, show a clear message, and continue with save only when both layers are valid. + +### Used pattern + +Use this split of responsibilities: + +- the main model-driven form decides whether the overall save is allowed +- the embedded Form PCF decides whether its own data is valid +- the host script bridges those two decisions in the main form `OnSave` handler + +This keeps the validation logic understandable and avoids duplicating business rules in multiple places. + +## Sample + +### Step 1: Form XML definition within a model-driven app + +This example shows the binding of the Form PCF in a model-driven form. + +```xml + + + + talxis_multiline + + + + + talxis_multiline + myscripts.js + onLoadPCFConfig + onLoadPCFFormContext + + + + + talxis_multiline + myscripts.js + onLoadPCFConfig + onLoadPCFFormContext + + + + + talxis_multiline + myscripts.js + onLoadPCFConfig + onLoadPCFFormContext + + + +``` + +Then you need to register the `onload` handler in the form XML. That is what allows the form script to attach the save validation bridge. + +```xml + + + + + + + + + + +``` + +This is the full wiring chain: + +1. Form XML binds the Form PCF. +2. Form XML points the PCF to `onLoadPCFConfig` and `onLoadPCFFormContext`. +3. Form XML also registers `onLoadModelDrivenForm` on the form. +4. `onLoadModelDrivenForm` attaches the save interception logic. + +### Step 2: Store the PCF Form Context + +The PCF setup and inner workings are already covered by: + +- [Form Control Overview](https://talxis.github.io/base-controls/?path=/docs/form-get-started--overview) + +For the context bridge solution, the only part that matters here is exposing the embedded Form PCF `formContext` to the host form save handler. + +```ts +public static PCFFormContext: Xrm.FormContext; +public static onLoadPCFFormContext(params: GetFormContextParams): void { + // save PCF context to a place where it can be accessed from the main form's onsave script + Main.PCFFormContext = params.formContext; +} +``` + +### Step 3: On main form load, register the `OnSave` handler and access the PCF form context + +This is the context bridge itself. The main form `OnSave` handler prevents the default save, validates both the host form and the bound Form PCF, and only then continues with `formContext.data.save()`. + +```ts +public static onLoadModelDrivenForm(executionContext: Xrm.Events.EventContext): void { + const formContext: Xrm.FormContext = executionContext.getFormContext(); + formContext.data.entity.addOnSave(async (executionContext: Xrm.Events.SaveEventContext) => { + executionContext.getEventArgs().preventDefault(); + const isPCFFormValid: boolean = Main.isPcfFormContextValid(); + const isValid: boolean = formContext.data.isValid(); + if (!isValid || !isPCFFormValid) { + alert('Form is not valid. Save operation will be canceled.'); + } + else { + await formContext.data.save(); + await Main.savePCFData(); + } + }); +} + +private static isPcfFormContextValid(): boolean { + const PCFFormContext: Xrm.FormContext = Main.PCFFormContext; + const textAttribute: Xrm.Attributes.Attribute = PCFFormContext.getAttribute('text'); + //replace setIsValid to always false with custom business logic + textAttribute.setIsValid(false, 'NOT VALID'); + return PCFFormContext.data.isValid(); +} + +private static savePCFData(): boolean { + const PCFFormContext: Xrm.FormContext = Main.PCFFormContext; + //custom logic to save data from the PCF Form PCF +} +``` + +The important behavior here is: + +- `executionContext.getEventArgs().preventDefault()` stops the original ribbon save +- `formContext.data.isValid()` checks the host model-driven form +- `isPcfFormContextValid()` checks the embedded Form PCF +- `formContext.data.save()` resumes save only after both validations pass + +## When to use this approach + +Use this pattern when: + +- the Form PCF is embedded on a model-driven form +- users save through the standard ribbon Save button +- the embedded Form PCF contains required fields or business validation that must block the overall record save \ No newline at end of file