Creating a Messages Service to Interact with Firestore
Once again, our usual approach is to start with the “main” feature of the application. Although we will eventually have things like login and account creation, the main purpose of our application is creating and displaying messages. Let’s start there.
Create an Interface for messages
export interface Message { author: string; content: string; created: string;}Creating the Message Service
import { Injectable, computed, inject, signal } from '@angular/core';import { Observable, merge } from 'rxjs';import { collection, query, orderBy, limit } from 'firebase/firestore';import { collectionData } from 'rxfire/firestore';import { map } from 'rxjs/operators';import { connect } from 'ngxtension/connect';
import { Message } from '../interfaces/message';import { FIRESTORE } from '../../app.config';
interface MessageState { messages: Message[]; error: string | null;}
@Injectable({ providedIn: 'root',})export class MessageService { private firestore = inject(FIRESTORE);
// sources messages$ = this.getMessages();
// state private state = signal<MessageState>({ messages: [], error: null, });
// selectors messages = computed(() => this.state().messages); error = computed(() => this.state().error);
constructor() { // reducers const nextState$ = merge( this.messages$.pipe(map((messages) => ({ messages }))) );
connect(this.state).with(nextState$); }
private getMessages() { const messagesCollection = query( collection(this.firestore, 'messages'), orderBy('created', 'desc'), limit(50) );
return collectionData(messagesCollection, { idField: 'id' }).pipe( map((messages) => [...messages].reverse()) ) as Observable<Message[]>; }}Once again we have our basic state management setup, except this time we are
using the connect function that we covered in the advanced state management
module.
To quickly recap, this is essentially the same idea as our normal reducers that we have been creating:
constructor() { // reducers const nextState$ = merge( this.messages$.pipe(map((messages) => ({ messages }))) );
connect(this.state).with(nextState$); }Except rather than subscribing and calling state.update with the source
values, instead we map those values and return an object with whatever values
we want to set in the state. In this case, we return { messages } because we
want to update the messages property in our state signal with the messages
emitted on the messages$ stream.
Notice that we are also already integrating with Firestore now and making
use of our injection token by injecting FIRESTORE as this.firestore:
private getMessages() { const messagesCollection = query( collection(this.firestore, 'messages'), orderBy('created', 'desc'), limit(50) );
return collectionData(messagesCollection, { idField: 'id' }).pipe( map((messages) => [...messages].reverse()) ) as Observable<Message[]>; }We are using some functions from both the firebase and rxfire libraries in
order to retrieve documents from a specific collection in our Firestore
database. Let’s walk through what is happening step-by-step here:
The goal here is that we want this to return an observable stream of the last
50 messages that have been added to our database. First, we could just do this
to get a reference to a specific collection:
const messagesCollection = collection(this.firestore, 'messages')This will give us a reference to the messages collection of documents in our
Firestore database (we haven’t actually added any collections or documents to
the database, we will do that shortly).
But, we don’t just want to get everything from that collection, we want to query
for specific documents, which is why we use a query instead:
const messagesCollection = query( collection(this.firestore, 'messages'), orderBy('created', 'desc'), limit(50) );Now, we will only return 50 documents which are ordered by their created
date in descending order. Creating this query won’t actually give us the stream
of data we want though, we need to supply it to the collectionData method and
return that:
return collectionData(messagesCollection, { idField: 'id' }).pipe( map((messages) => [...messages].reverse()) ) as Observable<Message[]>;This collectionData method is from rxfire not firebase — we want this data
returned as an observable stream, which is why we use the rxfire method.
We also supply a couple of configurations for this — we want to get the
unique id from our documents so we supply the idField configuration, and we
also want the messages in reverse order (the latest should be at the bottom of
the screen) so we map and reverse them.
Now we can just call our getMessages method and we will get a stream of the
latest 50 messages from our Firestore database that will automatically
update in real time whenever a new message is added to the database!
Displaying Messages
Now that we have a way to get messages, we need a way to display them. We should be getting back to familiar territory now, as we will be creating a simple dumb component to display the data we get back from the database.
Go ahead and see if you can create this component — it should accept an array of
messages that matches the Message interface we created and it should render
out an item for each one. We will add some specific stuff for styling in
a moment, just try to get the basic skeleton set up.
Click here to reveal solution
Solution
import { Component, input } from '@angular/core';import { Message } from '../../shared/interfaces/message';
@Component({ selector: 'app-message-list', template: ` <ul class="gradient-bg"> @for (message of messages(); track message.created){ <li> <div class="avatar animate-in-primary"> <img src="https://api.dicebear.com/7.x/bottts/svg?seed={{ message.author.split('@')[0] }}" /> </div> <div class="message animate-in-secondary"> <small>{{ message.author }}</small> <p> {{ message.content }} </p> </div> </li> } </ul> `,})export class MessageListComponent { messages = input.required<Message[]>();}One fun thing we are doing here is this:
<img src="https://api.dicebear.com/7.x/bottts/svg?seed={{ message.author.split('@')[0] }}" />We are using the dicebear API here to return funky random avatars for us. The
idea is that you just pass a unique id to the API and it will return an avatar
for that id. We are just using the author’s email address to identify them for
the avatar.
Now we are going to use our MessageListComponent in our HomeComponent to
display a list of data from Firebase. See if you can do that.
Click here to reveal solution
Solution
import { Component, inject } from '@angular/core';import { MessageListComponent } from './ui/message-list.component';import { MessageService } from '../shared/data-access/message.service';
@Component({ selector: 'app-home', template: ` <div class="container"> <app-message-list [messages]="messageService.messages()" /> </div> `, imports: [MessageListComponent],})export default class HomeComponent { messageService = inject(MessageService);}This should all be working at this point… only we don’t have any data yet! Let’s add some test data to our emulators to see if it is all working.
Adding Data to the Emulator
First, make sure you run the start script:
npm startThis will ensure that the emulators are started before serving your application. Next, you can head over to:
http://localhost:4000From there, click on the Firestore tab. You should see an option that says
+ Start collection. Click that, and create a collection with a Collection ID
of messsages.
This will automatically create our first document for us, but we need to make sure to fill out its fields. Make sure to add the following fields and values when creating a document:
author | string | Joshcontent | string | hellocreated | string |You can just leave the created blank — we will create actual dates later but
that isn’t really required for testing. If you have your application open in
another window, you should see this new data instantly pop in when you add it.
The styling is a bit funky right now (you know… on account of the whole giant robot head thing) — we will work on that later. If you like, you can add some additional documents as well if you want — just keep in mind that when you stop the emulators all of the data will be deleted!