Now that we have the initial chats layout and its component, we will take it a step further by providing the chats data from a server instead of having it locally. In this step we will be implementing the API server and we will do so using Meteor with Mongo.
In Meteor, we keep data inside Mongo.Collections
.
This collection is actually a reference to a MongoDB collection, and it is provided to us by a Meteor package called Minimongo, and it shares almost the same API as a native MongoDB collection.
We can also wrap it with RxJS' Observables
using meteor-rxjs
.
That package has been already installed, it's a part of the boilerplate.
Let's create a Collection of Chats:
1
2
3
4
import {Chat} from "../models/chat.model";
import {MongoObservable} from "meteor-rxjs";
export const Chats = new MongoObservable.Collection<Chat>('chats');
And also for Messages:
1
2
3
4
import {MongoObservable} from "meteor-rxjs";
import {Message} from "../models/message.model";
export const Messages = new MongoObservable.Collection<Message>('messages');
Since we have Collections, we can now move on to fill them with data.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import {Chats} from "../../../both/collections/chats.collection";
import {Messages} from "../../../both/collections/messages.collection";
import * as moment from "moment";
export class Main {
start(): void {
if (Chats.collection.find().count()) return;
let chatId;
chatId = Chats.collection.insert({
title: 'Ethan Gonzalez',
picture: 'https://randomuser.me/api/portraits/thumb/men/1.jpg'
});
Messages.collection.insert({
chatId: chatId,
content: 'You on your way?',
createdAt: moment().subtract(1, 'hours').toDate()
});
chatId = Chats.collection.insert({
title: 'Bryan Wallace',
picture: 'https://randomuser.me/api/portraits/thumb/lego/1.jpg'
});
Messages.collection.insert({
chatId: chatId,
content: 'Hey, it\'s me',
createdAt: moment().subtract(2, 'hours').toDate()
});
chatId = Chats.collection.insert({
title: 'Avery Stewart',
picture: 'https://randomuser.me/api/portraits/thumb/women/1.jpg'
});
Messages.collection.insert({
chatId: chatId,
content: 'I should buy a boat',
createdAt: moment().subtract(1, 'days').toDate()
});
chatId = Chats.collection.insert({
title: 'Katie Peterson',
picture: 'https://randomuser.me/api/portraits/thumb/women/2.jpg'
});
Messages.collection.insert({
chatId: chatId,
content: 'Look at my mukluks!',
createdAt: moment().subtract(4, 'days').toDate()
});
chatId = Chats.collection.insert({
title: 'Ray Edwards',
picture: 'https://randomuser.me/api/portraits/thumb/men/2.jpg'
});
Messages.collection.insert({
chatId: chatId,
content: 'This is wicked good ice cream.',
createdAt: moment().subtract(2, 'weeks').toDate()
});
}
}
Quick overview.
We use .collection
to get the actual Mongo.Collection
instance, this way we avoid using Observables.
At the beginning we check if Chats Collection is empty by using .count()
operator.
Then we provide few chats with one message each.
We also bundled Message with a Chat using chatId
property.
This requires a small change in the model:
1
2
3
4
5
6
export interface Message {
_id?: string;
chatId?: string;
content?: string;
createdAt?: Date;
}
Since Meteor's API requires us to share some of the code in both client and server, we have to import all the collections on the client-side too.
We also want to provide that data to the component:
1
2
3
4
5
6
7
8
9
10
11
12
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import {Component, OnInit} from "@angular/core";
import template from "./chats.component.html"
import {Observable} from "rxjs";
import {Chat} from "../../../../both/models/chat.model";
import * as moment from "moment";
import style from "./chats.component.scss";
import {Chats} from "../../../../both/collections/chats.collection";
import {Message} from "../../../../both/models/message.model";
import {Messages} from "../../../../both/collections/messages.collection";
@Component({
selector: "chats",
...some lines skipped...
style
]
})
export class ChatsComponent implements OnInit {
chats: Observable<Chat[]>;
constructor() {
}
ngOnInit() {
this.chats = Chats
.find({})
.mergeMap<Chat[]>(chats =>
Observable.combineLatest(
...chats.map(chat =>
Messages.find({ chatId: chat._id }, { sort: { createdAt: -1 }, limit: 1 })
.startWith(null)
.map(messages => {
if (messages) chat.lastMessage = messages[0];
return chat;
})
)
)
).zone();
}
}
As you can see, we moved chats
property initialization to ngOnInit
, one of the Angular's lifehooks.
It's being called when Component is initalized.
Here comes a quick lesson of RxJS.
Since Chats.find()
returns an Observable
we can take advantage of that and bundle it with Messages.find()
to look for last messages of each chat. This way everything will work as a one body, one Observable.
So what's really going on there?
First thing is to get all the chats by using Chats.find({})
.
The result of it will be an array of Chat
objects.
Let's use map
operator to make a space for adding the last messages.
Chats.find({})
.map(chats => {
const chatsWithMessages = chats.map(chat => {
chat.lastMessage = undefined;
return chat;
});
return chatsWithMessages;
})
For each chat we need to find the last message.
We can achieve this by calling Messages.find
with proper selector and options.
Let's go through each element of the chats
property to call Messages.find
.
const chatsWithMessages = chats.map(chat => Chats.find(/* selector, options*/));
That returns an array of Observables.
We need to create a selector. We have to look for a message that is a part of required chat:
{
chatId: chat._id
}
Okay, but we need only one, last message. Let's sort them by createdAt
:
{
sort: {
createdAt: -1
}
}
This way we get them sorted from newest to oldest.
We look for just one, so selector will look like this:
{
sort: {
createdAt: -1
},
limit: 1
}
Now we can add the last message to the chat.
Messages.find(/*...*/)
.map(messages => {
if (messages) chat.lastMessage = messages[0];
return chat;
})
Great! But what if there are no messages? Wouldn't it emit a value at all?
RxJS contains a operator called startWith
. It allows to emit some value before Messages.find beings to emit messages.
This way we avoid the waiting for non existing message.
The result:
const chatsWithMessages = chats.map(chat => {
return Messages.find(/*...*/)
.startWith(null)
.map(messages => {
if (messages) chat.lastMessage = messages[0];
return chat;
})
})
Last thing to do is to handle the array of Observables we created (chatsWithMessages
).
Yet again, RxJS comes with a rescue. We will use combineLatest
which takes few Observables and combines them into one Observable.
It works like this:
const source1 = /* an Observable */
const source2 = /* an Observable */
Observable.combineLatest(source1, source2);
This combination returns an array of both results (result
). So the first item of that array will come from source1
(result[0]
), second from source2
(result[1]
).
Let's see how it applies to our example:
Observable.combineLatest(...chatsWithMessages);
We used ...array
because Observable.combineLatest
expects arguments, not a single one that with an array of Observables.
To merge that observable into Chats.find({})
we need to use mergeMap
operator instead of map
:
Chats.find({})
.mergeMap(chats => Observable.combineLatest(...chatsWithMessages));
In Whatsapp we used chats.map(/*...*/)
directly instead of creating another variables like we did with chatsWithMessages
.
With all this, we have now Chats with their last messages available in the UI view.
{{{nav_step prev_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/1.0.0/chats-page" next_ref="https://angular-meteor.com/tutorials/whatsapp2/meteor/1.0.0/messages-page"}}}