@sguez/d365-event-decorators
v1.0.2
Published
**TypeScript decorators for Dynamics 365 form events**
Downloads
8
Readme
D365 Event Decorators
TypeScript decorators for Dynamics 365 form events
A lightweight, type-safe, and extensible library for declaring Dynamics 365 model-driven app form event handlers using TypeScript decorators legacy.
Use decorators to bind handlers to form-level, control-level, tab-level, subgrid, iframe, PCF, process, and knowledge-base search events without manually wiring everything in the form editor. The library provides runtime registration, event dispatching utilities, and basic profiling/logging to help debug and optimize event attachment.
Events are based on the official Microsoft documentation. For more information, see:
Table of contents
- Key features
- Install
- Quick Start
- API & Decorators
- Runtime pieces
- Examples (exhaustive)
- Debugging & Profiling
- Known Issues (Dynamics-specific)
Key features
- Declarative syntax for form events with TypeScript decorators.
- Runtime registry and dispatcher that attaches handlers to the form through a single
FormEventHandlerBaseinstance. - Supports global form events (
OnLoad,OnSave,OnPostSave, etc.). - Control-level events (
OnChange,OnPreSearch, etc.). - Tab and section visibility events.
- Subgrid, iframe, and PCF events.
- Business process flow events.
- Knowledge base search events.
- Built-in profiling to measure decorator initialization and attachment times.
- Debugging utilities for inspecting registered event handlers.
Install
This README assumes you already have a build pipeline that compiles TypeScript for use in Dynamics 365. The library is designed to be bundled with your form scripts.
Install as an internal dependency (example with npm):
npm install @sguez/d365-event-decorators @sguez/d365-form-helpersTypeScript configuration
You need to set the following in your tsconfig.json:
{
"compilerOptions": {
"moduleResolution": "NodeNext", // optional
"module": "NodeNext", // optional
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
}
}Compiling
Keep your business logic in TypeScript, compile to a single (minified) JS file for Dynamics web resource consumption.
Quick Start
- Create a class that extends
FormEventHandlerBase. - Decorate methods with
D365Event.*decorators to declare the events you want to bind. - Instantiate the handler in your form
onLoadfunction and pass the execution context.
import { FormEventHandlerBase } from "@sguez/d365-event-decorators/HandlerBase";
import D365Event from "@sguez/d365-event-decorators/Decorators";
class ContactFormHandler extends FormEventHandlerBase {
@D365Event.Form.OnLoad()
onFormLoad(executionContext: Xrm.Events.EventContext) {
// your form logic
}
@D365Event.Column.OnChange("firstname")
onFirstNameChange(executionContext: Xrm.Events.EventContext) {
// logic when firstname changes
}
}
export function onLoad(executionContext: Xrm.Events.EventContext) {
// instantiate once per form
new ContactFormHandler(executionContext);
}API & Decorators
All decorators are exported under the D365Event namespace. They come in several categories. Where decorators accept a control/name parameter, they accept one or many names (e.g. @D365Event.Column.OnChange("firstname", "lastname")).
D365Event.Form— global form events:OnLoad()OnDataLoad()Loaded()OnSave()OnPostSave()
D365Event.Tab— tab events (one or many tab names):OnStateChange(tabName1, tabName2, ...)OnExpand(tabName1, ...)OnCollapse(tabName1, ...)
D365Event.Column— attribute events (one or many attribute names):OnChange(attributeName1, attributeName2, ...)
D365Event.Lookup— lookup control events (one or many lookup control names):OnTagClick(controlName1, ...)OnPreSearch(controlName1, ...)
D365Event.SubGrid— subgrid events (one or many grid names):OnLoad(gridName1, ...)OnRecordSelected(gridName1, ...)
D365Event.IFrame— iframe-ready event (one or many webresource names):OnReadyStateComplete(webResourceName1, ...)
D365Event.Process— BPF events:OnStatusChange()OnPreStatusChange()OnPreStageChange()OnStageChange()OnStageSelected()
D365Event.PCF— PCF control events (one or many control names):OnOutputChange(controlName1, ...)
D365Event.KnowledgeBaseSearch— knowledge base search events (one or many control names):OnResultOpened(controlName1, ...)OnSelection(controlName1, ...)PostSearch(controlName1, ...)
D365Event.Filter.FormTypes(formTypes1, ...)— optional filter decorator to restrict a handler to specific form types. Note: you may passXrmEnum.FormType.Createor its numeric equivalent (for example1) — Dynamics form type constants are numeric under the hood.
Decorator behavior
Decorators register metadata into an in-memory registry (no metadata reflection dependency). At runtime, when an instance of a FormEventHandlerBase derived class is created, the FormEventDispatcher reads this registry and attaches the declared handlers to the actual form or controls.
Runtime pieces
Major runtime modules included in this library:
DecoratorProfiler— collects initialization duration for decorator upserts.Registry— stores per-constructor event metadata and exposesgetFormEventsandupsertFunctionEvent.Dispatcher—FormEventDispatcherattaches handlers to the form and component APIs using helpers from@sguez/d365-form-helpers.HandlerBase— base class (FormEventHandlerBase) that you extend and instantiate inonLoadto wire events.
Examples
Below are examples showing usage for each decorator category. In all examples the handler method receives the executionContext: Xrm.Events.EventContext parameter and the form-level instantiation uses executionContext.
Form events
class FormExamples extends FormEventHandlerBase {
@D365Event.Form.OnLoad()
onLoadHandler(executionContext: Xrm.Events.EventContext) {
// called on form load
}
@D365Event.Form.OnDataLoad()
onDataLoadHandler(executionContext: Xrm.Events.EventContext) {
// called when form data is loaded
}
@D365Event.Form.Loaded()
onLoadedHandler(executionContext: Xrm.Events.EventContext) {
// called after the form UI is fully rendered
}
@D365Event.Form.OnSave()
onSaveHandler(executionContext: Xrm.Events.EventContext) {
// called during save
}
@D365Event.Form.OnPostSave()
onPostSaveHandler(executionContext: Xrm.Events.EventContext) {
// called after save completes
}
}Tab events
class TabExamples extends FormEventHandlerBase {
@D365Event.Tab.OnStateChange("tab_general", "tab_details")
onAnyTabStateChange(executionContext: Xrm.Events.EventContext) {
// called when tab_general or tab_details expand/collapse
}
@D365Event.Tab.OnExpand("tab_general")
onTabExpand(executionContext: Xrm.Events.EventContext) {
// called only when tab_general becomes expanded
}
@D365Event.Tab.OnCollapse("tab_details")
onTabCollapse(executionContext: Xrm.Events.EventContext) {
// called only when tab_details becomes collapsed
}
}Column/Attribute events
class ColumnExamples extends FormEventHandlerBase {
@D365Event.Column.OnChange("firstname", "lastname")
onNameChanged(executionContext: Xrm.Events.EventContext) {
// called when either firstname or lastname changes
}
}Lookup events
class LookupExamples extends FormEventHandlerBase {
@D365Event.Lookup.OnTagClick("primarycontactid")
onLookupTagClick(executionContext: Xrm.Events.EventContext) {
// called when a tag is clicked on the lookup
}
@D365Event.Lookup.OnPreSearch("parentaccountid")
onLookupPreSearch(executionContext: Xrm.Events.EventContext) {
// called before lookup search executes
}
}SubGrid events
class SubGridExamples extends FormEventHandlerBase {
@D365Event.SubGrid.OnLoad("contactsGrid")
onSubGridLoad(executionContext: Xrm.Events.EventContext) {
// called when the subgrid has loaded
}
@D365Event.SubGrid.OnRecordSelected("contactsGrid")
onSubGridRecordSelect(executionContext: Xrm.Events.EventContext) {
// called when a record is selected in the subgrid
}
}IFrame events
class IFrameExamples extends FormEventHandlerBase {
@D365Event.IFrame.OnReadyStateComplete("webResource_myframe")
onIFrameReady(executionContext: Xrm.Events.EventContext) {
// access via formContext.getControl("webResource_myframe").getContentWindow()
const formContext = executionContext.getFormContext();
formContext.getControl("webResource_myframe")?.getContentWindow().then(
function (contentWindow) {
contentWindow.doStuff();
}
)
}
}Process (BPF) events
class ProcessExamples extends FormEventHandlerBase {
@D365Event.Process.OnStatusChange()
onProcessStatusChange(executionContext: Xrm.Events.EventContext) {
// called when process status changes
}
@D365Event.Process.OnPreStatusChange()
onPreStatusChange(executionContext: Xrm.Events.EventContext) {
// called before the status change is applied
}
@D365Event.Process.OnPreStageChange()
onPreStageChange(executionContext: Xrm.Events.EventContext) {
// called before stage change
}
@D365Event.Process.OnStageChange()
onStageChange(executionContext: Xrm.Events.EventContext) {
// called when active stage changes
}
@D365Event.Process.OnStageSelected()
onStageSelected(executionContext: Xrm.Events.EventContext) {
// called when a stage is explicitly selected
}
}PCF control events
class PCFExamples extends FormEventHandlerBase {
@D365Event.PCF.OnOutputChange("pcf_currency")
onPcfOutputChange(executionContext: Xrm.Events.EventContext) {
// called when a PCF control notifies an output change
}
}Knowledge Base Search events
class KbExamples extends FormEventHandlerBase {
@D365Event.KnowledgeBaseSearch.OnResultOpened("kbSearch1")
onKbResultOpened(executionContext: Xrm.Events.EventContext) {
// called when a KnowledgeBase search result is opened
}
@D365Event.KnowledgeBaseSearch.OnSelection("kbSearch1")
onKbSelection(executionContext: Xrm.Events.EventContext) {
// called when a KnowledgeBase search result is selected
}
@D365Event.KnowledgeBaseSearch.PostSearch("kbSearch1")
onKbPostSearch(executionContext: Xrm.Events.EventContext) {
// called after a KnowledgeBase search finishes
}
}Filter decorator with numeric form type
class FilterExamples extends FormEventHandlerBase {
// You can use the enum or the numeric value (Create is 1)
@D365Event.Filter.FormTypes(XrmEnum.FormType.Create)
@D365Event.Form.OnLoad()
onCreateOnlyLoad(executionContext: Xrm.Events.EventContext) {
// runs only if form type is Create (enum or numeric 1)
}
}Debugging & Profiling
The library provides simple debugging helpers and a profiling utility.
FormEventHandlerBase.logRegisteredFormEvents()— prints all registered handlers for the class instance.
This method usesconsole.debugto output information. In many browsers this output is visible only when the DevTools logging level is set to Verbose.FormEventHandlerBase.logDecoratorProfilingTimes()— prints measured decorator initialization time (aggregated viaDecoratorProfiler.total()) and the time taken to attach events for the current instance.
This method also usesconsole.debugand therefore appears when DevTools logging level is Verbose.
Example
In your form script:
export function onLoad(executionContext: Xrm.Events.EventContext) {
const handlerClass = new ContactFormHandler(executionContext);
// Debug: list all registered event handlers for this form
handlerClass.logRegisteredFormEvents();
// Profiling: show decorator initialization and attach timings
handlerClass.logDecoratorProfilingTimes();
}When DevTools logging level is set to Verbose, the console will show:
- Each decorated handler (function name, event types, component names, applicable form types)
- Initialization and attach timings in milliseconds
Warning aggregation
When a decorator references a control/tab/attribute name that cannot be found on the current form, the library aggregates these warnings and emits grouped console.warn entries instead of spamming the console for every missing item.
Known Issues (Dynamics-specific)
These are not limitations of the library but quirks of Dynamics 365 itself:
- OnLoad → corresponds to the Form Loaded event in the user interface.
- OnDataLoad → triggered after OnLoad on the first load. By design, only the Form Load event is configurable in the form editor.
- Lookup Events → only applied to the first duplicated control bound to an attribute. They will not fire for duplicated controls (
controlName1,controlName2, etc.), only for the original attribute control.
