Skip to main content
Version: 10

Migrate custom code to zoneless change detection

Level: advanced
note

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:

  1. Open the Configuration section. Instructions: Open the Configuration section.

  2. Select the user-made package that contains schemas of the "Client module" type.

  3. Open the schema of the "Client module" type.

  4. Find and fix affected code.

    When you fix affected code, apply the following rules:

    • Use the await operator for all asynchronous work that must finish before the page closes or the next page opens.
    • Do not use the await operator for a timer that must run while the page is open, for example, a timer that tracks a call. Stop such a timer in the crt.HandleViewModelDestroyRequest request handler instead.
    • Creatio waits for requests that the request handler sends using the request.$context.executeRequest() method even without the await operator. 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 timer
    handler: 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 await operator.

    Migrated code: wait for the delay
    handler: 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 await operator for the work itself, then perform the action.

    Example

    The crt.SaveRecordRequest request handler saves the record and reloads the AddressListDS data 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 await operator.

    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 await operator for the work in the try block. Unlock the button or hide the mask in the finally block.

    Example

    The request handler disables the Submit button and runs the UsrSubmitOrderProcess business process for the current order.

    In the migrated code, the request handler unlocks the button in the finally block. 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 completes
    handler: 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, the XMLHttpRequest object, the sdk.HandlerChainService.instance.process() method, and web service calls with callbacks.

    Solution. Call a promise-based API using the await operator. 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 the HttpClientService class and wait for the response using the await operator.

    Migrated code: call a promise-based API
    const 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 callback
    and 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, the document.querySelector() method, and the click() method.

    Solution. Send the base request that performs the same action using the await operator:

    • To save the record, use the crt.SaveRecordRequest request.
    • To refresh a list, use the crt.LoadDataRequest request.

    Affected code. Reading zone.js internal fields to get a view model attribute value without the await operator. In zoneless mode, promises do not have these fields, and the code receives undefined.

    Code to find. The __zone_symbol__value and __zone_symbol__state fields.

    Solution. Get the view model attribute value using the await operator. If code that cannot use the await operator, for example, a DOM event callback, needs the value, read the value using the await operator in a request handler first and store it in a variable the callback uses.

    Example

    The code gets the value of the Id view 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 await operator.

    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 await operator in a request handler
    • timers in the crt.HandleViewModelInitRequest, crt.HandleViewModelResumeRequest, crt.HandleViewModelAttributeChangeRequest, and crt.HandleModelEventRequest request handlers
    • callbacks of messages from the server or from other browser tabs
    • timers that use the setInterval() and requestAnimationFrame() 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.

  5. Save the schema.

  6. 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:

  1. Open the Angular project of the remote module in an external IDE.

  2. 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 update command adds the provideZoneChangeDetection() provider to the app providers. Remove this provider. Otherwise, the remote module keeps using zone.js.
  3. Open the "angular.json" file.

  4. Remove zone.js from the polyfills property of the build and test targets.

  5. Update the components to follow Angular change detection rules for zoneless applications. Learn more: Angular without ZoneJS (Zoneless) (official vendor documentation).

  6. Remove the code that relies on NgZone events. In zoneless mode, these events never fire.

  7. Run the npm run build command in the terminal of the external IDE to build the project. The command adds the build to the "dist" directory of the Angular project.

  8. 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​

  1. Deploy a pre-production environment of Creatio version 10.1.1. Learn more: Pre-production environment.

  2. Transfer the packages that contain migrated code to the pre-production environment. Instructions: Transfer the package.

  3. For Creatio version 10.1.1, register and enable the EnableZoneless additional feature in the pre-production environment. Instructions: Add a custom additional feature in the front-end.

  4. Open the Freedom UI pages that contain migrated request handlers or remote modules.

  5. Repeat the user actions on the pages: click buttons, save records, close the pages, and run the business processes that open them.

  6. 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​

"handlers" schema section

Migrate remote module functionality to the deferred format

Custom Freedom UI component implemented using remote module

Environment overview

Angular without ZoneJS (Zoneless) (official vendor documentation)