Skip to content

Adding Live Chat Messages

Currently, the only way we can add new messages to our Firestore database is to do it manually through the emulator interface. In this lesson, we are going to provide a way to do it through the application.

Update the MessageService

import { Observable, defer, merge } from 'rxjs';
import { collection, query, orderBy, limit, addDoc } from 'firebase/firestore';
private addMessage(message: string) {
const newMessage: Message = {
author: '[email protected]',
content: message,
created: Date.now().toString(),
};
const messagesCollection = collection(this.firestore, 'messages');
return defer(() => addDoc(messagesCollection, newMessage));
}

An important thing to note here as that we are just using [email protected] as the author since we don’t have a login system/user authentication yet — we will need to come back to this method later to use the user’s actual email.

Just like with the getMessages example, we create a reference to the collection we are interested in:

const messagesCollection = collection(this.firestore, 'messages');

But this time, we use the addDoc method from firebase to add the new document we just created to the messages collection. Note that we do not have to supply an id for our documents as this is created automatically by Firestore.

Also notice that we are wrapping the call to addDoc in defer. This is because addDoc returns a Promise. We want to convert this into an Observable. We can use from from RxJS to do this, but the benefit of using defer is that addDoc will be executed lazily.

The general idea is that Promises are executed eagerly/immediately. Even if you are not using the result, the code in a Promise (e.g. adding a document to Firebase in this case) will be executed immediately. This is different to an observable which is lazy — an observable won’t execute any code until we subscribe to it.

You might think that since from can convert a Promise to an Observable that it would make it lazy. However, if we use from the Promise will still be executed immediately, even if we don’t subscribe to it. Often this won’t cause any problems, but just to be safe, we will generally always use defer so that it will function in the way we expect: no code gets executed until we subscribe to the observable.

Now we are going to add our source for handling adding messages.

add$ = new Subject<Message['content']>();

We’re using that TypeScript trick again here so that we can use the type of whatever content is in our Message interface for our source.

Now we are going to make use of that concept we talked about in the advanced state management module.

constructor() {
// reducers
const nextState$ = merge(
this.messages$.pipe(map((messages) => ({ messages }))),
this.add$.pipe(
exhaustMap((message) => this.addMessage(message)),
ignoreElements(),
catchError((error) => of({ error }))
)
);
connect(this.state).with(nextState$);
}

Remember how we aren’t actually interested in setting the values from the add$ source in our state. The whole point of the add$ source is just to trigger the addMessage method. It is the addMessage method that will cause data to be added to Firestore, which will then also automatically cause our messages$ source to emit with the new data, and that is how the data gets set in our state.

We call the this.addMessage method by switching to it in the stream, and we use ignoreElements so that our stream does not actually emit any values — we are not interested in them. We are however interested in errors, which ignoreElements will not prevent. If we get an error we want to set that error into our state, but we don’t want the error to break our stream. So, we use catchError to prevent the stream from breaking, and return the error value as a normal stream emission by using of. Since this data emission is created after our ignoreElements operator it will not be prevented from emitting by ignoreElements.

The end result is that our add$ source will call addMessage and emit no values, except for the value of any errors that might occur.

Create a Message Input Component

We are going to create another dumb component now for the HomeComponent called MessageInputComponent that will allow the user to enter a chat message. This component will use a form control with an <input>, and when the user clicks the send button it will emit the current value using an output and it will also reset the form control to clear the message.

If you want, this will be a good opportunity to try building an entire component by yourself. Give it a go, and I will have my full solution below.

import { Component, output } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
@Component({
selector: 'app-message-input',
template: `
<input
type="text"
[formControl]="messageControl"
placeholder="type a message..."
/>
<button
mat-button
(click)="send.emit(messageControl.value); messageControl.reset()"
>
<mat-icon>send</mat-icon>
</button>
`,
imports: [ReactiveFormsModule, MatButtonModule, MatIconModule],
styles: [
`
:host {
width: 100%;
position: relative;
}
input {
width: 100%;
background: var(--white);
border: none;
font-size: 1.2em;
padding: 2rem 1rem;
}
button {
height: 100% !important;
position: absolute;
right: 0;
bottom: 0;
mat-icon {
margin-right: 0;
}
}
`,
],
})
export class MessageInputComponent {
send = output<string>();
messageControl = new FormControl();
}

Your solution will almost certainly be different to mine — that doesn’t mean it is wrong, there are many ways you could go about doing this. If you think your approach is generally correct feel free to stick with it instead of changing it to be the same as mine. If you do run into problems with it later, you can always come back and change it.

One thing you might want to change to match mine though is the extra styling and usage of the Angular Material components — this isn’t necessary, but it does play into the overall styling of the application if that is important to you.

Integrate the Message Input Component

Once again, see if you can get this component wired up in the HomeComponent so that when the user submits a message the add$ source from the MessageService is nexted with the value.

<div class="container">
<app-message-list [messages]="messageService.messages()" />
<app-message-input (send)="messageService.add$.next($event)" />
</div>

Now, as long as we have the emulators running with:

Terminal window
npm start

We should be able to add messages to our app using the app itself!

Again, it looks pretty terrible at the moment. We’re not really worried about that at the moment but… those giant robots are a bit ridiculous and make it pretty hard to use the application. Let’s deal with that now.

styles: [
`
ul {
height: 100%;
overflow: scroll;
list-style-type: none;
padding: 1rem;
padding-bottom: 5rem;
margin: 0;
}
li {
display: flex;
margin-bottom: 2rem;
}
.avatar {
width: 75px;
margin: 0 1rem;
height: auto;
filter: drop-shadow(2px 3px 5px var(--accent-darker-color));
}
.message {
width: 100%;
background: var(--white);
padding: 2rem;
border-radius: 5px;
filter: drop-shadow(2px 4px 3px var(--primary-darker-color));
}
`,
],

Much better! I mean it still looks pretty awful, but it is at least usable now.