The async/await feature in Typescript fails to trigger updates in the AngularJS view

Currently, I am utilizing Typescript 2.1 (developer version) to transpile async/await to ES5.

An issue I have encountered is that when I modify any property linked to the view within my async function, the view does not automatically reflect the updated value. As a result, I find myself having to invoke $scope.$apply() at the end of each function to ensure the latest values are displayed.

Below is an example of async code:

async testAsync() {
     await this.$timeout(2000);
     this.text = "Changed";
     //$scope.$apply(); <-- hoping to find an alternative
}

Unfortunately, the new value of text is not immediately visible in the view after this change.

Are there any workarounds available so that I can avoid manually calling $scope.$apply() every single time?

Answer №1

The information provided here is accurate in indicating that AngularJS lacks awareness of the method, necessitating the need to inform Angular about any updated values.

In my opinion, I prefer utilizing $q for handling asynchronous actions instead of relying on await as it aligns with "The Angular way".

It's quite simple to wrap non-Angular methods with $q. For instance, this is how I wrap all Google Maps functions, as they consistently require passing a callback function for completion notification.

function doAThing()
{
    var defer = $q.defer();
    // Note that this method takes a `parameter` and a callback function
    someMethod(parameter, (someValue) => {
        $q.resolve(someValue)
    });

    return defer.promise;
}

You can then implement it like this

this.doAThing().then(someValue => {
    this.memberValue = someValue;
});

Alternatively, if you opt to continue using await, there is an improved approach over employing $apply, which involves utilizing $digest. Like so

async testAsync() {
   await this.$timeout(2000);
   this.text = "Changed";
   $scope.$digest(); <-- This leads to much faster processing :)
}

In this scenario, $scope.$digest proves more effective than $scope.$apply because the latter performs dirty checking (Angular's change detection mechanism) on all bound values across all scopes, potentially bearing performance costs - particularly noticeable when dealing with numerous bindings. However, $scope.$digest restricts its checking to bound values within the current $scope, enhancing performance significantly.

Answer №2

If you want to streamline this process, you can utilize the angular-async-await extension:

class SomeController {
  constructor($async) {
    this.testAsync = $async(this.testAsync.bind(this));
  }

  async testAsync() { ... }
}

The main purpose of this extension is to wrap promise-returning functions with a wrapper that triggers $rootScope.$apply() after execution. This ensures proper synchronization.

Attempting to automatically trigger digestion on an async function would require complex manipulations within both the framework and underlying Promise implementation. Consequently, this is not supported for native async functions as it relies on internal mechanisms rather than the global Promise. Controlled behavior is preferred over automatic actions in such scenarios.

To manage multiple calls to testAsync efficiently without causing unnecessary digest cycles, it is advisable to apply $async solely to the parent function testsAsync:

class SomeController {
  constructor($async) {
    this.testsAsync = $async(this.testsAsync.bind(this));
  }

  private async testAsync() { ... }

  async testsAsync() {
    await Promise.all([this.testAsync(1), this.testAsync(2), ...]);
    ...
  }
}

Answer №3

After thoroughly examining the code at angular-async-await, it appears that they are relying on $rootScope.$apply() to process the expression once the asynchronous promise is resolved.

However, this approach may not be optimal. By utilizing AngularJS's original $q and implementing a small workaround, you can achieve superior performance.

To start, create a function (such as a factory or method)

// inject $q ...
const resolver = (asyncFunc) => {
    const deferred = $q.defer();
    asyncFunc()
      .then(deferred.resolve)
      .catch(deferred.reject);
    return deferred.promise;
}

Now, you can incorporate this function into your services, for example.

getUserInfo = () => {

  return resolver(async() => {

    const userInfo = await fetch(...);
    const userAddress = await fetch (...);

    return {userInfo, userAddress};
  });
};

This method is just as efficient as using AngularJS $q, but with less code involved.

Answer №4

If you want to see the desired behavior in action, check out this fiddle I created: Promises with AngularJS. It includes Promises that resolve after 1000ms, an async function, and a Promise.race, all achieved with just 4 digest cycles (check the console).

Summarizing the goal:

  • The aim was to utilize async functions without relying on third-party libraries like $async
  • To automatically trigger the minimum required digest cycles

How did we accomplish this?

In ES6, the introduction of Proxy brought about valuable new possibilities. This allowed us to wrap a Promise within a Proxy object, which triggers a digest cycle only when necessary upon resolution or rejection.

This innovative approach integrates seamlessly as a runtime change within AngularJS:

function($rootScope) {
  function triggerDigestIfNeeded() {
    $rootScope.$applyAsync();
  };

  Promise = new Proxy(Promise, {
    construct(target, argumentsList) {
      return (() => {
        const promise = new target(...argumentsList);

        promise.then((value) => {
          triggerDigestIfNeeded();

          return value;
        }, (reason) => {
          triggerDigestIfNeeded();

          return reason;
        });

        return promise;
      })();
    }
  });
}

By leveraging Promises at the core of asynchronous functions, our objective was met succinctly through minimal code snippets. Moreover, one can now seamlessly incorporate native Promises into AngularJS!

Update: The use of Proxy is not mandatory, as observed here where the same outcome is achievable using plain JavaScript:

Promise = ((Promise) => {
  const NewPromise = function(fn) {
    const promise = new Promise(fn);

    promise.then((value) => {
      triggerDigestIfNeeded();

      return value;
    }, (reason) => {
      triggerDigestIfNeeded();

      return reason;
    });

    return promise;
  };

  // Clone the prototype
  NewPromise.prototype = Promise.prototype;

  // Copy all writable instance properties
  for (const propertyName of Object.getOwnPropertyNames(Promise)) {
    const propDesc = Object.getOwnPropertyDescriptor(Promise, propertyName);

    if (propDesc.writable) {
      NewPromise[propertyName] = Promise[propertyName];
    }
  }

  return NewPromise;
})(Promise) as any;

Answer №5

According to @basarat, the native ES6 Promise does not have knowledge of the digest cycle.

To work around this, you can instruct Typescript to utilize the $q service promise instead of the native ES6 promise.

This method eliminates the need to call $scope.$apply()

angular.module('myApp')
    .run(['$window', '$q', ($window, $q) =>  {
        $window.Promise = $q;
    }]);

Answer №6

If you are transitioning from AngularJS to Angular using ngUpgrade (refer to https://angular.io/guide/upgrade#upgrading-with-ngupgrade):

Since Zone.js updates native Promises, it is advisable to convert all $q-based AngularJS promises to native Promises. This is because Angular automatically triggers a $digest when the microtask queue is cleared (such as when a Promise is resolved).

Even if your plan does not involve upgrading to Angular, you can still incorporate Zone.js into your project and set up a similar hook like ngUpgrade for improved functionality.

Answer №7

Is there a way to avoid having to constantly use $scope.$apply() in my code?

The reason for this is that TypeScript relies on the default browser implementation of native Promise, which Angular 1.x does not recognize. To ensure proper dirty checking, all asynchronous functions outside of Angular's control must manually trigger a digest cycle.

Answer №8

In agreement with @basarat, the traditional ES6 Promise does not have knowledge of the digest cycle. You need to use a promise in the following way:

async runAsyncTest() {
 await this.$timeout(2000).toPromise()
      .then(response => this.text = "Updated");
 }

Answer №9

Angular faces the challenge of not being able to detect when the native Promise is completed. In order to address this issue, all async functions generate a new Promise.

An effective workaround could involve:

window.Promise = $q;

This approach enables TypeScript/Babel to utilize angular promises instead. Is it a foolproof solution? I cannot fully guarantee its safety yet - ongoing tests are still in progress.

Answer №10

To kick things off, I suggest creating a converter function within a generic factory (please note that this code has not been tested yet):

function convertToAngularPromise(promise)
{
    var deferred = $q.defer();
    promise.then((data) => {
        $q.resolve(data);
    }).catch(response)=> {
        $q.reject(response);
    });

    return deferred.promise;
}

This is just a starting point and the actual conversion process might require more complexity than what is shown here...

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

How to Show or Conceal a list item depending on the value of another variable using angularjs

As someone new to Angular/Ionic, I'm exploring how to manipulate an Ionic list. Specifically, I am attempting to show or hide an item in the list based on a value stored in the item.display variable. The code snippet below demonstrates what I have so ...

Tips for validating Enum Strings using the newest version of Joi?

Is there a way to validate Enum String? In the past, I followed this advice from: https://github.com/hapijs/joi/issues/1449 enum UserRole { Admin = 'admin', Staff = 'staff' } const validator = { create: Joi.object().keys({ ...

"Typescript throws a mysterious 'Undefined value' error post-assignment

I'm currently working on a task to fetch my customer's branding information based on their Id using Angular. First, I retrieve all the customer data: this.subscription = this.burstService.getBurst().subscribe(async(response) => { if (r ...

Is it possible to display a modal view when clicking on a textfield without the keyboard automatically popping up?

As a beginner in the realm of AngularJS and the Ionic framework, I am currently working on developing a hybrid app utilizing PhoneGap and Ionic. I have encountered a scenario where, upon clicking a UITextField, a modal should appear with a list of account ...

What could be causing the drop-down values to fail to be saved?

I'm dealing with an address object that has nested objects for both permanent and postal addresses. Despite successfully saving the values of input boxes in a large form, I'm facing an issue with not being able to save the dropdown (select) value ...

Typescript - ensure only one specific value is in an array of length N

Is there a way to require the 'foo' literal, while allowing the array to have any shape (i.e. not using an X-length tuple with pre-defined positions)? type requireFoo = ??? const works: requireFoo = ['bar','foo'] //This shoul ...

Replacing URLs in Typescript using Ionic 3 and Angular

Currently facing some difficulties getting this simple task to work... Here is the URL format I am dealing with: https://website.com/image{width}x{height}.jpg My objective is to replace the {width} and {height} placeholders. I attempted using this func ...

Real-time data and dynamic checkbox functionality in AngularJS

I am working on an onclick function that involves data stored in objects. $scope.messages = [ {"id": "1"}, {"id": "2"}, {"id": "3"}, {"id": "4"}, ]; $scope.selection = { ids: {} }; $scope.sendMe = function(message) { //send the data with `id` and ...

What is the best way to transfer the value of one directive attribute to another directive in AngularJS?

For instance: <custom-main> <custom-sub1 att-name="test"></custom-sub1> <custom-sub2></custom-sub2> </custom-main> JavaScript Source Code bosAppModule.directive('custom-main',[&apos ...

When working with the "agora-rtc-sdk-ng" package, an error may be thrown by Next.js stating that "window is not defined"

Currently, I am in the process of incorporating the "agora-rtc-sdk-ng" package for live streaming with next.js and typescript. import AgoraRTC from 'agora-rtc-sdk-ng'; However, when I try to import it, an error is thrown as shown below: https:/ ...

Angular tabs display the initial tab

I attempted to implement the Tabs feature from angular material by following the provided documentation. However, I encountered an issue where the first tab does not display upon page load; I have to manually click on it to view its content. For more info ...

TypeScript conditional return type: effective for single condition but not for multiple conditions

In my code, I have implemented a factory function that generates shapes based on a discriminated union of shape arguments. Here is an example: interface CircleArgs { type: "circle", radius: number }; interface SquareArgs { type: "square" ...

Buttons for camera actions are superimposed on top of the preview of the capacitor camera

I am currently using the Capacitor CameraPreview Library to access the camera functions of the device. However, I have encountered a strange issue where the camera buttons overlap with the preview when exporting to an android device. This issue seems to on ...

Exploring Data and Models within AngularJS

I am working on a WebApp that has a unique HTML layout Nav-column-| Center Column---- | Right Column Link1---------|Data corresponding|Data Corresponding to Link1-A Link2---------| to Nav-column------| (ie based oon Center Column Link) Link3----- ...

Encountering an error when utilizing Firefox version 35 with protractor

When running my Angular app scenarios with Chrome, everything runs smoothly. However, I encounter a halt when trying to run the scenarios on Firefox version 35.0b6. Any help would be greatly appreciated. Thank you in advance. I am currently using Protract ...

Unlimited template loading with AngularJS

I am encountering an issue with AngularJS while utilizing routeprovider. When I reload the page, it triggers an infinite load. Interestingly, upon initially loading the main page, everything works as expected. Below is the code snippet: var app = angula ...

Struggling to implement sparklines for real-time data in the AngularJS adaptation of the SmartAdmin template

Currently, I am embarking on a project that involves utilizing the AngularJS version of the SmartAdmin Bootstrap template foundhere. Within this project scope, I am required to integrate sparklines into various pages. I have successfully implemented them ...

Creating a unique AngularJS custom directive that utilizes ng-options can be achieved by populating

I've encountered a major roadblock in my project, and despite my efforts to troubleshoot, I can't seem to find the solution. The issue revolves around populating options within an ng-options directive through a service. This directive is nested i ...

Guidelines for forming a composite type with elements?

Imagine having a convenient function that wraps a generic component with a specified constant. function wrapComponent(ComponentVariant: ComponentVariantType) { return ( <Wrapper> <ComponentVariant> <InnerComponent /> ...

Incorporating Angular into the script code of the extension content

I am looking to customize text fields on a website using a chrome-extension. However, the website is built with Angular, so I need to modify the text fields using Angular code. To incorporate Angular in my extension's scope, I am attempting to declar ...