Migrate custom code to zoneless change detection
The functionality is available for beta testing in Creatio version 10.1.1. If you have any feedback, contact us at: beta@creatio.com. All feedback is appreciated.
To take advantage of this functionality, register and enable the EnableZoneless additional feature. Learn more: Add a custom additional feature in the front-end.
Creatio uses the zone.js library to decide when to refresh the UI. zone.js tracks every asynchronous task on the page, such as timers, promises, events, and network requests, and Creatio refreshes the UI after each of them even if the page data does not change. Since version 10.1.1, Creatio supports zoneless change detection. In zoneless mode, Freedom UI pages work without zone.js, and Creatio refreshes the UI only when the data it shows changes. This makes pages open faster and makes errors in custom code easier to find because stack traces are shorter and clearer. Since version 10.1.2, Creatio enables zoneless mode by default. Migrate custom code and test it in zoneless mode before you update Creatio to version 10.1.2.
Zoneless change detection in Creatio
In zoneless mode, Creatio does not intercept browser API calls and loads less code on page startup. Freedom UI pages, elements of Freedom UI Designer, and navigation between pages work without zone.js. Since version 10.1.1, composable Creatio apps support zoneless mode as well.
Zoneless mode affects custom code in the following cases:
- A request handler starts asynchronous work and relies on Creatio to wait for it.
- Custom code reads zone.js internal fields, for example,
__zone_symbol__value. - A remote module loads its own zone.js. Such a module keeps working, but the page loses the benefits of zoneless mode.
Most custom code works in zoneless mode as is, but some request handler patterns and remote modules need changes to keep custom pages reliable after the switch.
General procedure
1. Migrate custom code
Migrate the custom code that zoneless mode affects. The migration procedure depends on the code type.
Migrate schemas of the "Client module" type
In schemas of the "Client module" type, zoneless mode affects the code of request handlers that start asynchronous work, and any code that reads zone.js internal fields, for example, in the converters or validators section of a Freedom UI page schema.
With zone.js, Creatio waits for asynchronous work that a request handler starts before it closes a page or a modal window, opens the next page, or moves a business process to the next step. In zoneless mode, Creatio waits only for the work that the request handler awaits and for the requests that the request handler sends using the request.$context.executeRequest() method. This means that a timer, a web service call, or a loop that repeatedly checks the DOM can still run after the page is closed.
To migrate schemas of the "Client module" type:
-
Open the Configuration section. Instructions: Open the Configuration section.
-
Select the user-made package that contains schemas of the "Client module" type.
-
Open the schema of the "Client module" type.
-
Find and fix affected code.
When you fix affected code, apply the following rules:
- Use the
awaitoperator for all asynchronous work that must finish before the page closes or the next page opens. - Do not use the
awaitoperator for a timer that must run while the page is open, for example, a timer that tracks a call. Stop such a timer in thecrt.HandleViewModelDestroyRequestrequest handler instead. - Creatio waits for requests that the request handler sends using the
request.$context.executeRequest()method even without theawaitoperator. Creatio does not wait for requests sent from a timer or a callback.
Search the schema code for the following affected code and fix it based on the solution.
Affected code. A timer that delays an action. In zoneless mode, the delayed code runs after Creatio has moved on, possibly on a closed page.
Code to find. The
setTimeout()function.Solution. Add
await new Promise((resolve) => setTimeout(resolve, ms));before the action.Example
The request handler hides the Next steps component, passes the request further along the handler chain, and shows the component again after 100 ms.
In the original code, the
setTimeout()function shows the component after Creatio has already processed the request.Original code: show the component using a timerhandler: async (request, next) => {request.$context.IsNextStepsVisible = false;/* Creatio does not wait for this timer in zoneless mode. */setTimeout(() => {request.$context.IsNextStepsVisible = true;}, 100);return next?.handle(request);}In the migrated code, the request handler waits for the request processing and the delay using the
awaitoperator.Migrated code: wait for the delayhandler: async (request, next) => {request.$context.IsNextStepsVisible = false;/* Wait until the next handlers in the chain process the request. */const result = await next?.handle(request);/* Wait for the delay before the action. */await new Promise((resolve) => setTimeout(resolve, 100));request.$context.IsNextStepsVisible = true;return result;}
Affected code. A timer that gives a record save, a business process, or a data load a fixed time to finish, then reloads a list or reads the result. In zoneless mode, the page can be closed when the timer fires, and the work might not have finished.
Code to find. The
setTimeout()function.Solution. Use the
awaitoperator for the work itself, then perform the action.Example
The
crt.SaveRecordRequestrequest handler saves the record and reloads theAddressListDSdata source.In the original code, the
setTimeout()function reloads the data source 1 second after the save starts, whether or not the save is complete.Original code: reload the data source after a fixed delay{request: "crt.SaveRecordRequest",handler: async (request, next) => {/* The save is not awaited. */const result = next?.handle(request);/* The timer guesses how long the save takes. */setTimeout(() => {request.$context.executeRequest({type: "crt.LoadDataRequest",$context: request.$context,config: { loadType: "reload", useLastLoadParameters: true },dataSourceName: "AddressListDS"});}, 1000);return result;}}In the migrated code, the request handler waits for the save and the reload using the
awaitoperator.Migrated code: reload the data source after the save{request: "crt.SaveRecordRequest",handler: async (request, next) => {/* Wait until the record is saved. */const result = await next?.handle(request);/* Reload the data source and wait until the reload completes. */await request.$context.executeRequest({type: "crt.LoadDataRequest",$context: request.$context,config: { loadType: "reload", useLastLoadParameters: true },dataSourceName: "AddressListDS"});return result;}}
Affected code. A timer that unlocks a button or hides a custom mask after a period long enough for the slowest case.
Code to find. The
setTimeout()function.Solution. Use the
awaitoperator for the work in thetryblock. Unlock the button or hide the mask in thefinallyblock.Example
The request handler disables the Submit button and runs the
UsrSubmitOrderProcessbusiness process for the current order.In the migrated code, the request handler unlocks the button in the
finallyblock. This way, the button becomes available as soon as the request that runs the business process completes, even if the request fails.Migrated code: unlock the button after the request completeshandler: async (request, next) => {/* Disable the button while the request runs. */request.$context.IsSubmitDisabled = true;try {/* Run the business process and wait until the request completes. */await request.$context.executeRequest({type: "crt.RunBusinessProcessRequest",processName: "UsrSubmitOrderProcess",processRunType: "ForTheSelectedPage",recordIdProcessParameterName: "OrderId",$context: request.$context});} finally {/* Unlock the button whether the request succeeds or fails. */request.$context.IsSubmitDisabled = false;}return next?.handle(request);}
Affected code. A request or web service call with callbacks that the request handler does not await. In zoneless mode, Creatio does not wait for the callback.
Code to find. The
fetch()function, theXMLHttpRequestobject, thesdk.HandlerChainService.instance.process()method, and web service calls with callbacks.Solution. Call a promise-based API using the
awaitoperator. If a web service supports only callbacks, wrap the call in a promise.Example
The request handler calls web services and waits for the response.
To call a web service that has a promise-based API, use the
post()method of theHttpClientServiceclass and wait for the response using theawaitoperator.Migrated code: call a promise-based APIconst httpClientService = new sdk.HttpClientService();/* Send the request and wait for the response. */const response = await httpClientService.post(serviceUrl, payload);To call a web service that supports only callbacks, wrap the call in a promise. Resolve the promise in the success callback and reject it in the error callback.
Migrated code: wrap a callback-based web service in a promise/* Resolve the promise in the success callbackand reject it in the error callback. */const response = await new Promise((resolve, reject) => {callLegacyService(payload, resolve, reject);});
Affected code. A timer loop that repeatedly checks the DOM, finds an element, and clicks it.
Code to find. The
setTimeout()function, thedocument.querySelector()method, and theclick()method.Solution. Send the base request that performs the same action using the
awaitoperator:- To save the record, use the
crt.SaveRecordRequestrequest. - To refresh a list, use the
crt.LoadDataRequestrequest.
Affected code. Reading zone.js internal fields to get a view model attribute value without the
awaitoperator. In zoneless mode, promises do not have these fields, and the code receivesundefined.Code to find. The
__zone_symbol__valueand__zone_symbol__statefields.Solution. Get the view model attribute value using the
awaitoperator. If code that cannot use theawaitoperator, for example, a DOM event callback, needs the value, read the value using theawaitoperator in a request handler first and store it in a variable the callback uses.Example
The code gets the value of the
Idview model attribute.In the original code, the value is read from the internal zone.js field.
Original code: read the internal zone.js field/* Returns "undefined" in zoneless mode. */const activityId = request.$context.Id.__zone_symbol__value;In the migrated code, the value is read using the
awaitoperator.Migrated code: await the attribute value/* Wait until the attribute value is available. */const activityId = await request.$context.Id;
Do not change the following code. Creatio does not wait for such code in either mode.
- code after the first
awaitoperator in a request handler - timers in the
crt.HandleViewModelInitRequest,crt.HandleViewModelResumeRequest,crt.HandleViewModelAttributeChangeRequest, andcrt.HandleModelEventRequestrequest handlers - callbacks of messages from the server or from other browser tabs
- timers that use the
setInterval()andrequestAnimationFrame()functions
Do not change timers that the request handler already awaits either, for example,
await new Promise((resolve) => setTimeout(resolve, 500));. Creatio waits for such timers in both modes. - Use the
-
Save the schema.
-
Repeat steps 2-5 for every schema of the "Client module" type in every user-made package.
As a result, schemas of the "Client module" type will work in zoneless mode. Request handlers will wait for all asynchronous work that must finish before the page closes or the next page opens, and request handlers will not run code on a closed page.
Migrate remote modules
Remote modules keep working in zoneless mode. Creatio does not load zone.js for itself, but if a remote module delivers its polyfills bundle next to its remote entry, Creatio loads the bundle before the module. However, zone.js that the module loads intercepts browser API calls on the whole page, and the page loses the benefits of zoneless mode. To keep these benefits, migrate remote modules to zoneless change detection.
If a module needs zone.js but does not deliver a polyfills bundle, the module fails to start and the browser console shows the "NG0908: In this configuration Angular requires Zone.js" error. Migrate the module to zoneless change detection or, as a temporary fix, import zone.js at the start of the module bootstrap code.
To migrate a remote module to zoneless change detection:
-
Open the Angular project of the remote module in an external IDE.
-
Upgrade the project to Angular 20 or later. Instructions: Update Angular (official vendor documentation). Since Angular 21, applications are zoneless by default.
- If the project uses Angular 20, add the
provideZonelessChangeDetection()provider. - If you upgrade the project to Angular 21 or later, the
ng updatecommand adds theprovideZoneChangeDetection()provider to the app providers. Remove this provider. Otherwise, the remote module keeps using zone.js.
- If the project uses Angular 20, add the
-
Open the "angular.json" file.
-
Remove
zone.jsfrom thepolyfillsproperty of the build and test targets. -
Update the components to follow Angular change detection rules for zoneless applications. Learn more: Angular without ZoneJS (Zoneless) (official vendor documentation).
-
Remove the code that relies on
NgZoneevents. In zoneless mode, these events never fire. -
Run the
npm run buildcommand in the terminal of the external IDE to build the project. The command adds the build to the "dist" directory of the Angular project. -
Upload build artifacts to Creatio. Instructions: Custom Freedom UI component implemented using remote module (step 2).
As a result, the remote module will work in zoneless mode without zone.js, and Freedom UI pages that include the module will keep the benefits of zoneless mode.
2. Test the migrated code
-
Deploy a pre-production environment of Creatio version 10.1.1. Learn more: Pre-production environment.
-
Transfer the packages that contain migrated code to the pre-production environment. Instructions: Transfer the package.
-
For Creatio version 10.1.1, register and enable the
EnableZonelessadditional feature in the pre-production environment. Instructions: Add a custom additional feature in the front-end. -
Open the Freedom UI pages that contain migrated request handlers or remote modules.
-
Repeat the user actions on the pages: click buttons, save records, close the pages, and run the business processes that open them.
-
Make sure that:
- every action completes: messages appear, lists refresh, values are saved
- no code runs on a page that is already closed or on the next page
- the browser console shows no new errors
As a result, you will make sure that the migrated schemas of the "Client module" type and remote modules work correctly in zoneless mode before you update the production environment to version 10.1.2.
See also
Migrate remote module functionality to the deferred format
Custom Freedom UI component implemented using remote module
Angular without ZoneJS (Zoneless) (official vendor documentation)