Manage Daily video call state in Angular (Part 2)
Tags: webrtc, angular, tutorial, programming
Published: 2023-10-17 22:01:25

[_By Jess Mitchell_](https://www.daily.co/blog/author/jess/)

In this series, we’re building a video call app with a fully customized UI using [Angular](https://angular.io/) and [Daily’s Client SDK for JavaScript](https://docs.daily.co/reference/daily-js). In the [first post](https://www.daily.co/blog/p/d0d9bc4f-06b7-4c50-b57b-3d48e983a6ce/), we reviewed the app’s core features, as well as the general code structure and the role of each component. (Instructions for setting up the demo app locally are also included there.)

In today’s post, we’ll start digging into the [Angular demo app’s source code](https://github.com/daily-demos/daily-angular) available on GitHub. More specifically, we will:

1. Build the [`join-form`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/join-form/join-form.component.ts) component to allow users to join a Daily room.
2. Show how the [`app-daily-container`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/daily-container/daily-container.component.ts) component works, including how it responds to the `join-form` being submitted.
3. See how the [`app-call`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts) component sets up an instance of the Daily Client SDK’s [call object](https://docs.daily.co/reference/daily-js/daily-iframe-class).
4. Review how joining a Daily call works, and how to handle the events related to [joining](https://docs.daily.co/reference/daily-js/events/meeting-events#joined-meeting) and [leaving](https://docs.daily.co/reference/daily-js/events/meeting-events#left-meeting) a call.

The next post in this series will cover rendering the actual video tiles for each participant, so stay tuned for that.

Now that we have a plan, let’s get started!

## `app-daily-container:` The parent component
As a reminder from our first post, [`app-daily-container`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/daily-container/daily-container.component.ts) is the parent component for the video call feature:

![Component structure in the Angular demo app.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ebz1eeutn2x99h0hfmf9.png)
_Component structure in the Angular demo app._

It includes the [`join-form`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/join-form/join-form.component.ts) component, which allows users to submit an HTML form to join a Daily call. It also includes the [`app-call`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts) component, which represents the video call UI shown after the form is submitted.

![Gif of call participant submitting the join form and joining the video call](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w00sdwn78txdkaauyvyu.gif)
_Submitting the form in `join-for`m will cause `app-daily-container` to render `app-call` instead._

In this sense, `app-daily-container` is the bridge between the two views because we need to share the form values obtained via `join-form` with the in-call components (`app-call` and its children).

💡 _`app-chat` is also a child of `app-daily-container`, but we’ll cover that in a future post._

Looking at the class definition for [`app-daily-container`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/daily-container/daily-container.component.ts), we see there are two class variables (`dailyRoomUrl` and `userName`), as well as methods to set or reset those variables.

```
import { Component } from "@angular/core";

@Component({
  selector: "app-daily-container",
  templateUrl: "./daily-container.component.html",
  styleUrls: ["./daily-container.component.css"],
})

export class DailyContainerComponent {
  // Store callObject in this parent container.
  // Most callObject logic in CallComponent.
  userName: string;
  dailyRoomUrl: string;

  setUserName(name: string): void {
    // Event is emitted from JoinForm
    this.userName = name;
  }

  setUrl(url: string): void {
    // Event is emitted from JoinForm
    this.dailyRoomUrl = url;
  }

  callEnded(): void {
    // Truthy value will show the CallComponent; otherwise, the JoinFormComponent is shown.
    this.dailyRoomUrl = "";
    this.userName = "";
  }
}
```

The [HTML](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/daily-container/daily-container.component.html) for `app-daily-container` shows how these variables and methods get shared with the child components:

```
<join-form
  *ngIf="!dailyRoomUrl"
  (setUserName)="setUserName($event)"
  (setUrl)="setUrl($event)"></join-form>
<app-call
  *ngIf="dailyRoomUrl"
  [userName]="userName"
  [dailyRoomUrl]="dailyRoomUrl"
  (callEnded)="callEnded()"></app-call>
```

We can see that the `join-form` and `app-call` components use the [`*ngIf`](https://angular.io/api/common/NgIf) attribute to determine when they should be rendered. They have opposite conditions, meaning they will never both be rendered at the same time. If the `dailyRoomUrl` variable is not set yet, it renders the `join-form` (the default view); otherwise, it renders the `app-call` component. This is because the `dailyRoomUrl` is set when the join form has been submitted, so if the value isn’t set, the form hasn’t been used yet.

`join-form` has two output properties: the `setUserName()` and `setUrl()` event emitters, which are invoked when the join form is submitted.

`app-call` receives the `userName` and `dailyRoomUrl` variables as input props, which automatically update when Angular detects a change in their values. It also has `callEnded()` as an output prop.

### Understanding input and output props in Angular
In Angular, props can either be an [input property](https://angular.io/api/core/Input) – a value passed down to the child – or an [output property](https://angular.io/api/core/Output) – a method used to emit a value from the child to the parent component. (Data sharing is bidirectional between the parent and child when both types are used.)

You can tell which type of property it is based on whether the child component declares the prop as an input or output. For example, in [`app-call`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L38) we see:

```
@Input() userName: string;
@Output() callEnded: EventEmitter<null> = new EventEmitter();
```

The `userName` prop is an input property, meaning the value will automatically update in `app-call` whenever the parent component updates it.

The `callEnded` prop is an output property. Whenever `app-call` wants to emit an update to the parent component (`app-daily-container`), it can call `this.callEnded(value)` and `app-daily-container` will receive the event and event payload.

## `join-form`: Submitting user data for the call

![Join form UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f9dchutzfd6nus2j3zdl.png)
_The demo app’s `join-form` component._

The [`join-form`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/join-form/join-form.component.ts) component renders an HTML form element. It includes two inputs for the user to provide the Daily room they want to join, as well as their name:

```
<form [formGroup]="joinForm" (ngSubmit)="onSubmit()" class="join-form">
  <label for="name">Your name</label>
  <input type="text" id="name" formControlName="name" required />
  <label for="url">Daily room URL</label>
  <input type="text" id="url" formControlName="url" required />
  <input type="submit" value="Join call" [disabled]="!joinForm.valid" />
</form>
```

💡_You can create a Daily room and get its URL through the [Daily dashboard](https://dashboard.daily.co/rooms/create) or [REST API](https://docs.daily.co/reference/rest-api/rooms/create-room)._

The [`onSubmit()`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/join-form/join-form.component.ts#L23) class method is invoked when the form is submitted ([`ngSubmited`](https://angular.io/api/forms/NgForm#properties)).

The input values are bound to the component’s [`joinForm`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/join-form/join-form.component.ts#L16) values, an instance of Angular’s [`FormBuilder`](https://angular.io/guide/reactive-forms) class:

```
// … See the source code for the complete component

export class JoinFormComponent {
  @Output() setUserName: EventEmitter<string> = new EventEmitter();
  @Output() setUrl: EventEmitter<string> = new EventEmitter();

  joinForm = this.formBuilder.group({
    name: "",
    url: "",
  });

  constructor(private formBuilder: FormBuilder) {}

  onSubmit(): void {
    const { name, url } = this.joinForm.value;
    if (!name || !url) return;

    // Clear form inputs
    this.joinForm.reset();
    // Emit event to update userName var in parent component
    this.setUserName.emit(name);
    // Emit event to update URL var in parent component
    this.setUrl.emit(url);
  }
}
```

When `onSubmit()` is invoked, the form input values are retrieved through `this.joinForm.value`. The `this.joinForm` values are then reset, which also resets the inputs in the form UI to visually indicate to the user that the form was successfully submitted.

Next, the `setUserName()` and `setURL()` output properties are used to emit form input values to [`app-daily-container`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/daily-container/daily-container.component.ts). (Remember: `app-daily-container` is listening for events already.)

Once these events are received by `app-daily-container`, the form values are assigned to the `userName` and `dailyRoomUrl` variables in `app-daily-container`, the latter of which causes the `app-call` component to be rendered instead of the `join-form`.

It’s a mouthful, but now we can move on to creating the actual video call!

## `app-call`: The brains of this video call operation
Let’s start with [`app-call`’s HTML](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.html) so we know what’s going to be rendered:

```
<error-message
  *ngIf="error"
  [error]="error"
  (reset)="leaveCall()"></error-message>

<p *ngIf="dailyRoomUrl">Daily room URL: {{ dailyRoomUrl }}</p>

<p *ngIf="!isPublic">
  This room is private and you are now in the lobby. Please use a public room to
  join a call.
</p>

<div *ngIf="!error" class="participants-container">
  <video-tile
    *ngFor="let participant of Object.values(participants)"
    (leaveCallClick)="leaveCall()"
    (toggleVideoClick)="toggleLocalVideo()"
    (toggleAudioClick)="toggleLocalAudio()"
    [joined]="joined"
    [videoReady]="participant.videoReady"
    [audioReady]="participant.audioReady"
    [userName]="participant.userName"
    [local]="participant.local"
    [videoTrack]="participant.videoTrack"
    [audioTrack]="participant.audioTrack"></video-tile>
</div>
```
There is an [error message](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.html#L4) that gets shown only if the `error` variable is true. (This gets set when Daily’s Client SDK’s [`”error”`](https://docs.daily.co/reference/daily-js/events/meeting-events#error) event is emitted.)

The `dailyRoomUrl` value is rendered in the UI so the local participant can see which room they joined. We also let the participant know if the room is private since we haven’t added a feature yet to join a private call. (You could, for example, add a [knocking](https://www.daily.co/blog/manage-call-permissions-with-dailys-knocking-feature/) feature).

Finally, the most important part: the video tiles. In Angular, you can use [`*ngFor`](https://angular.io/api/common/NgFor) attribute like a [`for of`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of) statement. For each value in the participants object (described in more detail below), a [`video-tile`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/video-tile/video-tile.component.ts) component will be rendered. We will look at how the `video-tile` component works in the next post so, for now, know that there’s one for each participant.

Next, let’s look at how we build our `participants` object in [`app-call`’s class definition](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts).

### Defining the `CallComponent` class
Most of the logic related to interacting with [Daily’s Client SDK for JavaScript](https://docs.daily.co/reference/daily-js) is included in the `app-call` component. The [`daily-js` library](https://docs.daily.co/reference/daily-js) is imported at the top of the file ([`import DailyIframe from "@daily-co/daily-js";`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L11)) which allows us to create the call for the local participant.

To build our call, we will need to:

1. Create an instance of the call object (i.e., the [`DailyIframe` class](https://docs.daily.co/reference/daily-js/daily-iframe-class)) using the [`createCallObject()`](https://docs.daily.co/reference/daily-js/factory-methods/create-call-object) factory method. (The [`getCallInstance()` static method](https://docs.daily.co/reference/daily-js/static-methods/get-call-instance#main) can be used to see if a call object already exists, too.)
2. Attach [event listeners](https://docs.daily.co/reference/daily-js/events) to the call object. `daily-js` will emit events for any changes in the call so we’ll listen for the ones relevant to our demo use case.
3. Join the call using the `dailyRoomUrl` and the `userName`, which were provided in the `join-form`. This is done via the [`join()`](https://docs.daily.co/reference/daily-js/instance-methods/join) instance method.

Since the call component is rendered as soon as the join form is submitted, we need to set up the call logic as soon as the component is initialized. In Angular, we use the [`ngOnInit()` lifecycle method](https://angular.io/api/core/OnInit) to capture this:

```
// … See source code for more detail

export class CallComponent {
  Object = Object; // Allows us to use Object.values() in the HTML file
  @Input() dailyRoomUrl: string;
  @Input() userName: string;
  // .. See source code

  ngOnInit(): void {
    // Retrieve or create the call object
    this.callObject = DailyIframe.getCallInstance();
    if (!this.callObject) {
      this.callObject = DailyIframe.createCallObject();
    }

    // Add event listeners for Daily events
    this.callObject
      .on("joined-meeting", this.handleJoinedMeeting)
      .on("participant-joined", this.participantJoined)
      .on("track-started", this.handleTrackStartedStopped)
      .on("track-stopped", this.handleTrackStartedStopped)
      .on("participant-left", this.handleParticipantLeft)
      .on("left-meeting", this.handleLeftMeeting)
      .on("error", this.handleError);

    // Join Daily call
    this.callObject.join({
      userName: this.userName,
      url: this.dailyRoomUrl,
    });
  }

 // … See source code
```

All three of the steps mentioned above happen as soon as the component is initialized. The second step – attaching Daily event listeners – is where most of the heavy lifting happens for managing the call. For each event shown in the code block above, an event handler is attached that will be invoked when the associated event is received.

As an overview, each event used above represents the following:
[`”joined-meeting”`](https://docs.daily.co/reference/daily-js/events/meeting-events#joined-meeting): The local participant joined the call.
[`”participant-joined”`](https://docs.daily.co/reference/daily-js/events/participant-events#participant-joined): A remote participant joined the call.
[`”track-started”`](https://docs.daily.co/reference/daily-js/events/participant-events#track-started): A participant's audio or video track began.
[`”track-stopped”`](https://docs.daily.co/reference/daily-js/events/participant-events#track-stopped): A participant's audio or video track ended.
[`”participant-left”`](https://docs.daily.co/reference/daily-js/events/participant-events#participant-left): A remote participant left the call.
[`”left-meeting”`](https://docs.daily.co/reference/daily-js/events/meeting-events#left-meeting): The local participant left the call.
[`”error”`](https://docs.daily.co/reference/daily-js/events/meeting-events#error): Something went wrong.

💡 _The next post in this series will cover the track-related events._

Once the call object is initialized and `join()`-ed, we need to manage the call state related to which participants are in the call. Once we know who is in the call, we can render `video-tile` components for them.

**Adding a new participant**
The `app-call` component uses the [`participants`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L42) variable (an empty object to start) to track all participants currently in the call.

```
export type Participant = {
  videoTrack?: MediaStreamTrack | undefined;
  audioTrack?: MediaStreamTrack | undefined;
  videoReady: boolean;
  audioReady: boolean;
  userName: string;
  local: boolean;
  id: string;
};

type Participants = {
  [key: string]: Participant;
};

export class CallComponent {
  // … See source code
  participants: Participants = {};
```

When the local participant or a new remote participant joins, we’ll respond the same way: by updating `participants`.

In both [`this.handleJoinedMeeting()`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L133) and [`this.participantJoined()`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L147) – the callbacks for [`”joined-meeting”`](https://docs.daily.co/reference/daily-js/events/meeting-events#joined-meeting) and [`”participant-joined”`](https://docs.daily.co/reference/daily-js/events/participant-events#participant-joined) – the `this.addParticipant()` method gets called to update `participants`. Let’s see how this works using `this.participantJoined()` as an example:

```
participantJoined = (e: DailyEventObjectParticipant | undefined) => {
  if (!e) return;
  // Add remote participants to participants list used to display video tiles
  this.addParticipant(e.participant);
};
```

With any Daily event, the event payload is available in the callback method (`e` in this case). The `participant` information is available in the event payload, which then gets passed to [`this.addParticipant()`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L102).

```
addParticipant(participant: DailyParticipant) {
  const p = this.formatParticipantObj(participant);
  this.participants[participant.session_id] = p;
}
```

Two steps happen here:

1. A reformatted copy of the participant object is made.
2. The reformatted participant object gets added to the `participants` object.

The participant object doesn’t need to get reformatted for this demo to work, but we do this to make the object easier to work with. The participant object that gets returned in the event payload contains a lot of nested information – much of which doesn’t affect our current feature set. To extract the information we do care about, we use [`this.formatParticipantObj()`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L85) to format a copy of the participant object, like so:

```
formatParticipantObj(p: DailyParticipant): Participant {
  const { video, audio } = p.tracks;
  const vt = video?.persistentTrack;
  const at = audio?.persistentTrack;
  return {
    videoTrack: vt,
    audioTrack: at,
    videoReady:
      !!(vt && (video.state === PLAYABLE_STATE || video.state === LOADING_STATE)),
    audioReady:
      !!(at && (audio.state === PLAYABLE_STATE || audio.state === LOADING_STATE)),
    userName: p.user_name,
    local: p.local,
    id: p.session_id,
  };
}
```

What we’re interested in here is:

1. The video and audio tracks.
2. Whether the tracks can be played in the demo’s UI, which is represented by `videoReady` and `audioReady`.
3. The user name of the participant, which we’ll display in the UI.
4. Whether they’re local, since local participants will have device controls in their `video-tile`.
5. The participant ID, so we can have a unique way of identifying them.
(Refer to our docs for an example of [all the other participant information](https://docs.daily.co/reference/daily-js/instance-methods/participants) returned in the event.)

Once the participant has been added to the `participants` object, a `video-tile` can be rendered for them, but more on that in our next post!

**Removing a remote participant from the call**
A call participant can leave the call in different ways. For example, they can use the “Leave” button in the [`video-tile`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/video-tile/video-tile.component.ts) component or they can simply exit their browser tab for the app. In any case, we need to know when a participant has left to properly update the app UI.

When any remote participant leaves the call, we need to update `participants` to remove that participant.

```
handleParticipantLeft = (
  e: DailyEventObjectParticipantLeft | undefined
): void => {
  if (!e) return;
  console.log(e.action);

  delete this.participants[e.participant.session_id];
};
```

In [`handleParticipantLeft()`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/call/call.component.ts#L160) (the handler for the [`”participant-left”`](https://docs.daily.co/reference/daily-js/events/participant-events#participant-left) event), we simply delete the entry for that participant in the participants object. This will in turn remove their video tile from the call UI.

**Removing a local participant from the call**
When the local participant leaves, we need to reset the UI by unmounting the `app-call` component and going back to rendering the `join-form` view instead, since the participant is no longer in the call. Instead of updating `participants`, we can just [`destroy()`](https://docs.daily.co/reference/daily-js/instance-methods/destroy) the call object and emit `callEnded()`, the output property passed from [`app-daily-container`](https://github.com/daily-demos/daily-angular/blob/8583a8688c1fd1773bb79175edaa6b6848861e06/src/app/daily-container/daily-container.component.ts).

```
handleLeftMeeting = (e: DailyEventObjectNoPayload | undefined): void => {
  if (!e) return;
  console.log(e);
  this.joined = false; // this wasn’t mentioned above but is used in the UI
  this.callObject.destroy();
  this.callEnded.emit();
};
```

When `callEnded()` is emitted, `app-daily-container` will reset the `dailyRoomUrl` and `userName` class variables.

```
// In DailyContainerComponent:

callEnded(): void {
    // Truthy value will show the CallComponent; otherwise, the JoinFormComponent is shown.
    this.dailyRoomUrl = "";
    this.userName = "";
  }
```

As a reminder, when `dailyRoomUrl` is falsy, the `join-form` component gets rendered instead of the `app-call`.

And with that, we have a Daily call that can be joined and left!

## Conclusion
In today’s post, we covered how participants can submit an HTML form in an Angular app to join a Daily video call, as well as how to manage participant state when participants join or leave the call.

In the next post, we’ll discuss how to render video tiles for each participant while they’re in the call, including our recommendations for optimizing video performance in an app like this.

If you have any questions or thoughts about implementing your Daily-powered video app with Angular, [reach out to our support team](https://www.daily.co/company/contact/support/) or head over to our [WebRTC community, peerConnection](https://community.daily.co/).