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: 'me@test.com', 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 me@test.com 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.
Click here to reveal solution
Solution
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.
Click here to reveal solution
Solution
<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:
npm startWe 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.