Wei Guang's Blog

How to Wait for emit to Finish in Angular

In Angular, we use EventEmitter to communicate between components.

For example, we have a simple login form component:

// login.component.ts
import { Component, EventEmitter, Output } from '@angular/core';

@Component({
  selector: 'app-login',
  template: `
    <div>
      <label for="username">Username:</label>
      <input type="text" id="username" [(ngModel)]="username" />
    </div>
    <div>
      <label for="password">Password:</label>
      <input type="password" id="password" [(ngModel)]="password" />
    </div>
    <button (click)="onLogin()">Login</button>
  `,
})
export class LoginComponent {
  @Output() loginEvent = new EventEmitter<{ username: string; password: string }>();

  username = '';
  password = '';

  onLogin() {
    this.loginEvent.emit({ username: this.username, password: this.password });
  }
}

The loginEvent is an instance of EventEmitter. When the user clicks the "Login" button, it triggers the emit() method, which broadcasts an event to its subscribers. We can also pass values via emit(), like in the example where we passed the username and password as an object.

Here is the usage:

// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <h1>Login Page</h1>
    <app-login (loginEvent)="handleLogin($event)"></app-login>
  `,
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  handleLogin(credentials: { username: string; password: string }) {
    console.log('Received login credentials:', credentials);
    if (credentials.username === 'admin' && credentials.password === 'password') {
      console.log('Login successful');
    } else {
      console.log('Login failed');
    }
  }
}

We implemented a simple client-side authentication by comparing the entered username and password with fixed strings to check if the login is successful.

So far, everything looks good and easy, right?

But what if we want some delays? What if we want to perform the authentication on the backend side by calling an API? We may not have the actual API set up, but we can mock one.

Let's create a service file:

// app.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  login(username: string, password: string): Promise<boolean> {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (username === 'admin' && password === 'password') {
          resolve(true);
        } else {
          resolve(false);
        }
      }, 1000);
    });
  }
}

And call the service in handleLogin:

// app.component.ts
import { AuthService } from './app.service';

@Component({
  // ...
})
export class AppComponent {
  constructor(private authService: AuthService) {}

  handleLogin(credentials: { username: string; password: string }) {
    this.authService
        .login(credentials.username, credentials.password)
        .then((success) => {
          if (success) {
            console.log('Login successful');
          } else {
            console.log('Login failed');
          }
        })
        .catch((error) => {
          console.error('An error occurred during authentication:', error);
        });
  }
}

Instead of instantly getting the login result, we need to wait for 1 second to get the result from the "backend". Now, I want to add an indicator in the login component to represent the waiting process:

// login.component.ts
import { Component, EventEmitter, Output } from '@angular/core';

@Component({
  selector: 'app-login',
  template: `
    <div>
      <label for="username">Username:</label>
      <input type="text" id="username" [(ngModel)]="username" />
    </div>
    <div>
      <label for="password">Password:</label>
      <input type="password" id="password" [(ngModel)]="password" />
    </div>
    <button (click)="onLogin()">
      {{ isSubmitting ? 'Submitting...' : 'Login' }}
    </button>
  `,
})
export class LoginComponent {
  isSubmitting = false;
  @Output() loginEvent = new EventEmitter<{ username: string; password: string }>();

  username = '';
  password = '';

  onLogin() {
    this.isSubmitting = true;
    this.loginEvent.emit({ username: this.username, password: this.password });
    this.isSubmitting = false;
  }
}

We introduced a variable isSubmitting to indicate the process. Before emitting the loginEvent, we set isSubmitting to true to indicate that the submission is in progress. After the loginEvent is emitted, we set isSubmitting back to false.

But this is not ideal. If you run the code, you will find that it doesn't work as expected. Why?

Because emit() is synchronous, and all it does is broadcast an event. The subscribers' callbacks will be triggered after the broadcast, but we can't know what the callback does.

This is the signature of emit():

/**
 * Emits an event containing a given value.
 * @param value The value to emit.
 */
emit(value?: T): void;

So, after we set isSubmitting to false and emit the loginEvent, isSubmitting will immediately turn back to true, even if the AuthService call is not completed.

Then the question arises: How can we wait for emit() to finish? Is there a built-in approach to do it?

As far as I know, no. But I have come up with a workaround for this. If you have used Vue.js before, you will know that Vue also has a similar mechanism that uses an event emitter to communicate between components, but it's "thenable".

// ParentComponent.vue
<template>
  <div>
    <ChildComponent @custom-event="handleCustomEvent"></ChildComponent>
    <p>{{ message }}</p>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      message: ''
    };
  },
  methods: {
    handleCustomEvent(payload) {
      return new Promise((resolve) => {
        this.message = payload;
        resolve();
      });
    }
  }
}
</script>

// ChildComponent.vue
<template>
  <button @click="triggerCustomEvent">Click me</button>
</template>

<script>
export default {
  methods: {
    triggerCustomEvent() {
      this.$emit('custom-event', 'Custom event triggered!')
        .then(() => {
          // do something after emit() completed
        });
    }
  }
}
</script>

What if we implement this in Angular?

First, we create a new ThenableEventEmitter class which extends EventEmitter but overrides its emit() method. Instead of emitting just a value, we will pass an additional field called next, which the subscribers can use to notify the emitter when they have finished their callback:

// thenable-event.ts
import { EventEmitter } from '@angular/core';

export class ThenableEventEmitter<T> extends EventEmitter {
  constructor(isAsync?: boolean) {
    super(isAsync);
  }

  override emit(value?: T) {
    let next: (value: unknown) => void = () => {};
    const promise = new Promise((resolve) => {
      next = resolve;
    });

    super.emit({ value, next });

    return promise;
  }
}

Next, we replace the original EventEmitter with our new ThenableEventEmitter in the login form component and add a then handler for it:

// login.component.ts
import { Component, Output } from '@angular/core';
import { ThenableEventEmitter } from './thenable-event';

@Component({
  selector: 'app-login',
  template: `
    <div>
      <label for="username">Username:</label>
      <input type="text" id="username" [(ngModel)]="username" />
    </div>
    <div>
      <label for="password">Password:</label>
      <input type="password" id="password" [(ngModel)]="password" />
    </div>
    <button (click)="onLogin()">
      {{ isSubmitting ? 'Submitting...' : 'Login' }}
    </button>
  `,
})
export class LoginComponent {
  isSubmitting = false;
  @Output() loginEvent = new ThenableEventEmitter<{ username: string; password: string }>();

  username = '';
  password = '';

  onLogin() {
    this.isSubmitting = true;
    this.loginEvent.emit({ username: this.username, password: this.password })
      .then(() => {
        this.isSubmitting = false;
      });
  }
}

Finally, in the app component, instead of receiving a single value from the emitter, we receive a value and a next function. After the API call is completed, we call the next() function to notify the login component about it.

// app.component.ts
// ...
export class AppComponent {
  handleLogin({ value: credentials, next }: { value: { username: string; password: string }, next: (value?: unknown) => void }) {
    this.authService
      .login(credentials.username, credentials.password)
      .then((success) => {
        // Add this!!!
        next();

        if (success) {
          console.log('Login successful');
        } else {
          console.log('Login failed');
        }
      })
      .catch((error) => {
        console.error('An error occurred during authentication:', error);
      });
  }
}

This approach is not perfect, but it does solve the problem when you want to wait for an emit() operation.

You can find the full code here.