@xrmforge/helpers
v0.17.0
Published
Browser-safe runtime helpers for Dynamics 365 form scripts: select(), parseLookup(), typedForm(), Xrm constants, Action/Function executors
Maintainers
Readme
@xrmforge/helpers
Browser-safe runtime helpers for Dynamics 365 form scripts. Zero Node.js dependencies, safe to bundle into esbuild IIFE Web Resources. Pairs with the types generated by @xrmforge/typegen to eliminate raw strings and magic values from your form code.
Part of XrmForge. This is the package you import in actual D365 form scripts.
Installation
npm install @xrmforge/helpersInstall as a regular dependency (not devDependency): generated Custom API action files import the action factories from this package at runtime.
Requirements: @types/xrm (>= 9.0.0) as a peer dependency.
What you get
| Area | Exports |
|------|---------|
| Web API query strings | select, selectExpand, parseLookup, parseLookups, parseFormattedValue, parseMultiSelect, expanded, expandedMany |
| Form lookups | formLookup, formLookupId, formLookupIdUnsafe, formLookupUnsafe |
| Typed form proxy | typedForm, normalizeGuid, isUnsavedRecord, types TypedForm, FormFields, FormTypeInfoProtocol |
| Xrm constants (const enum) | DisplayState, FormType, isFormType, FormNotificationLevel, AppNotificationLevel, RequiredLevel, SubmitMode, SaveMode, ClientType, ClientState, OperationType, StructuralProperty, BindingType |
| Custom API runtime | createBoundAction, createUnboundAction, createBoundFunction, createUnboundFunction, executeRequest, executeMultiple |
| Attribute submit | setAndSubmit, setUnsafeAndSubmit, clearAndSubmit |
| App notifications | addAppNotification, clearAppNotification |
| HTML WebResource context | parentXrm, getWebResourceContext |
| Environment variables | getEnvironmentVariable, clearEnvironmentVariableCache |
| Convenience | withProgress, callCloudFlow |
TypedForm: direct, typed field access
typedForm() wraps a FormContext so you can read and write fields as properties instead of getAttribute("...") chains. Pass the generated <Form>TypeInfo type (typegen >= 0.10.0) -- it bundles the field/attribute/control maps so type extraction works reliably across package boundaries.
import { typedForm } from '@xrmforge/helpers';
import type { AccountMainFormTypeInfo } from '../../generated/forms/account.js';
export function onLoad(ctx: Xrm.Events.EventContext): void {
const form = typedForm<AccountMainFormTypeInfo>(ctx.getFormContext());
form.name.getValue(); // string | null (typed StringAttribute)
form.revenue.setValue(150000); // typed NumberAttribute
form.controls.name.setDisabled(true); // typed control access
form.$context.ui.tabs.get('SUMMARY_TAB'); // full FormContext escape hatch
form.$unsafe('off_form_field')?.setValue('x'); // off-form field (nullable)
}setValue() on the proxy automatically calls setSubmitMode('always'), so programmatically set values are not silently dropped by D365 AutoSave. Assigning to a field directly (form.name = ...) throws a TypeError telling you to use .setValue().
Web API helpers
Build OData query strings from generated Fields enums and parse responses without hand-written annotation keys.
import { select, parseLookup, parseFormattedValue, parseMultiSelect } from '@xrmforge/helpers';
import { AccountFields } from '../../generated/fields/account.js';
import { AccountNavigationProperties as Nav } from '../../generated/entities/account.js';
const result = await Xrm.WebApi.retrieveRecord('account', id,
select(AccountFields.Name, AccountFields.WebsiteUrl, AccountFields.PrimaryContact));
const contact = parseLookup(result, Nav.PrimaryContact); // { id, name, entityType } | null
const status = parseFormattedValue(result, 'statecode'); // "Active" instead of 0
const types = parseMultiSelect(result.markant_typesmulticode); // normalize to number[]formLookup() / formLookupId() extract a lookup (with a brace-normalized GUID) straight from a form attribute:
import { formLookupId } from '@xrmforge/helpers';
const accountId = formLookupId(form.getAttribute(Fields.AccountId)); // string | nullXrm constants instead of raw strings
@types/xrm types these values as string literals but provides no runtime constants (XrmEnum does not exist at runtime under esbuild). These const enums are inlined at compile time -- zero runtime cost, no XrmEnum is not defined surprises.
import { DisplayState, FormNotificationLevel, RequiredLevel, SubmitMode, SaveMode } from '@xrmforge/helpers';
tab.setDisplayState(DisplayState.Expanded);
form.$context.ui.setFormNotification('Saved.', FormNotificationLevel.Info, 'saved');
form.email.setRequiredLevel(RequiredLevel.Required);Comparing form types needs care: getFormType() is typed as the distinct XrmEnum.FormType, so a direct === fails to compile under strict. Use isFormType():
import { isFormType, FormType } from '@xrmforge/helpers';
if (isFormType(form.$context, FormType.Create)) { /* only on create */ }Custom API executors
Generated action files (in generated/actions/) use the runtime factories from this package. Each executor exposes .execute() (calls Xrm.WebApi) and .request() (for batching).
import { WinQuote } from '../generated/actions/quote';
import { ValidateMandatoryFields } from '../generated/actions/global';
// Bound action: void result throws on failure, so just await
await WinQuote.execute(quoteId);
// Unbound action with typed params and response
const result = await ValidateMandatoryFields.execute({ TargetId: id, RuleSet: 'Full', Language: 'de' });
if (!result.IsValid) alert(result.MessageDe);Batch multiple calls in one round-trip with executeMultiple and .request():
import { executeMultiple } from '@xrmforge/helpers';
await executeMultiple([
ApproveRecord.request({ RecordId: id }),
NotifyOwner.request({ RecordId: id, Message: 'Approved' }),
]);You can also build executors by hand with createBoundAction / createUnboundAction / createBoundFunction / createUnboundFunction.
Convenience helpers
import { withProgress, callCloudFlow, clearAndSubmit, setUnsafeAndSubmit, addAppNotification, clearAppNotification, AppNotificationLevel } from '@xrmforge/helpers';
// Progress spinner around an async operation (errors propagate to your handler wrapper)
await withProgress('Processing...', () => WinQuote.execute(quoteId));
// Typed Power Automate cloud-flow call (HTTP-trigger URL from configuration, never hard-coded)
const price = await callCloudFlow<{ quoteId: string }, { total: number }>(FLOW_URL, { quoteId });
// Clear / set an attribute and force it to submit (avoids silent AutoSave drops)
clearAndSubmit(form.revenue);
setUnsafeAndSubmit(form, OpportunityFields.VslBeauftragung, closeDate);
// Global banner notification - addAppNotification returns the id, clearAppNotification removes it.
// (Use { autoHideMs } for a self-clearing banner; clearAppNotification handles the persistent case.)
const id = await addAppNotification('Saved.', AppNotificationLevel.Success, { showCloseButton: true });
await clearAppNotification(id);HTML WebResource context
An embedded HTML WebResource (a page in a form IFrame or the sitemap) gets no executionContext. These
helpers reach the host form API and the hosting record without hand-casting window.parent or the page context:
import { parentXrm, getWebResourceContext, select } from '@xrmforge/helpers';
const { entityId, entityName } = getWebResourceContext(); // hosting record (brace-stripped id)
if (entityId) {
const record = await parentXrm().WebApi.retrieveRecord(entityName, entityId, select(/* ... */));
}Both are browser-safe (zero Node.js dependencies, fine in esbuild IIFE bundles).
Documentation
Full guide and patterns: XrmForge on GitHub.
License
MIT (c) XrmForge Contributors.
