We're currently getting data from our rooms, but its just the room list for the purpose of our navigation items. We will need to bring the data into our room component so we can utilize it in our room list and room form. This requires two stages: First we have to get the data, and then secondly we have to make sure that the data is kept in such a way that we can accomplish everything we want to. I know that second part sounds weird, but it will make sense later. Let's focus on the first problem:
The easy answer would be to use the OnInit for our room component, but there's a problem with that. Each room we click on uses the same component. The only difference is in the data we're getting. Because of this, if we clicked on the Starfox room from our welcome screen, we would get our data as expected. But if we then clicked on the Zelda room, the ngOnInit wouldn't fire because we're staying in the same component.
So how do we solve this problem. We would need a subscription, that much we can probably deduce, but is there any subscription we have that fires any time the address changes, even going from room to room?
We do, our subscription to the params that we did a while back. Remember that observable triggers off of any address change, we can make our data call right there.
Now that we know where to place the call for our data, we need to make a method that returns the data. Since the data will vary based on the parameter that we're getting from our paramMap, we know this method will have to take in an id, and return an observable that has our data. Let's get crackin'.
Let's start by naming our method getRoomByID since that what we want the method to do. It's going to take in a string that should be our room id. We will then need to grab our list from the database list, and then do two seperate maps.
Could we do everything in one map? Sure, but it makes the code a little harder to look at and for the sake of this demonstration, we'll be using the one that looks easier. When you get more comfortable with using map, feel free to try to get everything done at once.
Let's start by tackling the first part. We're going to make our method, and inside we will return the observable that we called earlier to get our list of rooms. We then tack on a .pipe to get ready to work our magic.
public getRoomById(id:string) {
return this._db.list<Room>('rooms).valueChanges().pipe()
}
Nothing too crazy so far. I mentioned earlier how in services you generally don't assign the value of a service to a variable and then jjust give anyone access to it; We usually call a method and just return the value that we want. That's what we're doing here.
Also the _db refers to the AngularFireDatabase I Injected via the constructor a couple lessons ago. Your variable name may.. vary.
So we've got our pipe ready to go. Our first map will pass in an array of rooms. We want to return just one room out of that. We can use .find to help us out here.
public getRoomById(id:string) {
return this._db.list<Room>('rooms).valueChanges().pipe(
map( roomsFromDB => {
return roomsFromDB.find(room => room.id === id)
}),
)
}
We used arrow notation inside our .find to pass each room and it only returns the resulting room if the property id of our room matches the id we pass in via our method.
I know it's not likely to happen, but what if I passed in the value 'Zelda' and I actually had two rooms in the database named 'Zelda'. Would that not return an array and mess up the rest of this mapping business?
A solid question, and the answer is that .find() only returns the first matching result. So in your example, a second Zelda room would simply never be found.
Now we've got the one room we want. Sweet. The result is an object, and that's fine, but lets look at how the data is stored in firebase.
Hopefully our issues become more apparent looking at this: Our reservations list is not an array: It is an object that has a property for each firebase generated key, and inside that is an object with all our room data. We can't use a for loop on data like this, we need our reservations list to be an array.
Thats the first issue, but look at how the firebase-assigned id is stored: It's not a property lined up with all our other reservation data. We want a property in our reservation list named id and it needs to contain that key.
Lets start off our second map and begin by creating a blank array that will eventually hold our reservations:
public getRoomById(id:string) {
return this._db.list<Room>('rooms).valueChanges().pipe(
map( roomsFromDB => {
return roomsFromDB.find(room => room.id === id)
}),
map ( (selectedRoom:Room) => {
const reservationList:Reservation[] = [];
})
)
}
We began second map and passed in the result of our first map, now calling it selectedRoom. We gave it a type of Room, since thats what it should be. We also typed our blank array as an array of Reservations. You will need to import the reservation interface we made earlier, and if you didn't make one, you will need to do so before continuing.
So how can we go through all these reservations? Fortunately for..in can help us out here. Here is an explanation of how for..in works. The short version is that it allows us to iterate over object properties.
public getRoomById(id:string) {
return this._db.list<Room>('rooms).valueChanges().pipe(
map( roomsFromDB => {
return roomsFromDB.find(room => room.id === id)
}),
map ( (selectedRoom:Room) => {
const reservationList:Reservation[] = [];
for (let reservationKey in selectedRoom.reservations) {
}
})
)
}
We now have a loop that will go over each key in our reservation list. Great. Now bear with me as this is where things get a little dicey. I will show how we got our array and then explain in further detail:
public getRoomById(id:string) {
return this._db.list<Room>('rooms).valueChanges().pipe(
map( roomsFromDB => {
return roomsFromDB.find(room => room.id === id)
}),
map ( (selectedRoom:Room) => {
const reservationList:Reservation[] = [];
for (let reservationKey in selectedRoom.reservations) {
const reservation:Reservation = selectedRoom.reservations[reservationKey];
reservation.id = reservationKey;
reservationList.push(reservation);
}
})
)
}
Oof. So first, we create a new variable called reservation. We give it a type Reservation, which should make sense. We then assign its value to the reservation that corresponds to they key that is being iterated.
So that means reservation now holds email, endTime, reason, and all that business. That's cool, but now we need to give it a property id. Remember when we created our reservation interface that we added an optional property: id? This is what is allowing the next line to happen. We take that new reservation we made, and assigned its property id to be the value of the key we're using to iterate over our room list. If we did not designated the id field, we would not be able to do this. In fact, if we didn't give our variable reservation a type Reservation our linter would probably get mad at us because it doesn't recognize the property id. This is why types are our friends!
Now that we have our reservation object and we added an id, we push it to that blank reservation array we created earlier. Since this happens inside our for..in loop, once the loop finishes our reservationList will now be an array of reservations, complete with an id. Superb!
Now we can just overwrite the old reservations property value which previously held an object with our shiny new array, and return the resulting room.
public getRoomById(id:string) {
return this._db.list<Room>('rooms).valueChanges().pipe(
map( roomsFromDB => {
return roomsFromDB.find(room => room.id === id)
}),
map ( (selectedRoom:Room) => {
const reservationList:Reservation[] = [];
for (let reservationKey in selectedRoom.reservations) {
const reservation:Reservation = selectedRoom.reservations[reservationKey];
reservation.id = reservationKey;
reservationList.push(reservation);
}
selectedRoom.reservation = reservationList;
return selectedRoom;
})
)
}
That.. is a lot of code. But now with this method, we pass in the id of a room, and we get the corresponding rooms data all nicely formatted.
Here's a look at my final result. As usual, your variable names may be different:
I added a couple of typings to make sure the data is exactly what I think it should be. I didn't put the returning :Observable<Room> in our initial explanation because the entrire function would have red bars for days because our linter is expecting us to return an Observable. Now that our method is finished, go ahead and add that on. If you are receiving errors, your data result may not be what you think it is.
So now that we have our method, where do we call it? We discussed this at the beginning of the lesson: We will put this in our paramMap subscription since we know thta code will be fired every time we change address, even if it's from room to room.
Let's now head into our room component and look for where we are subscribing to our paramMap. I'm going it in the constructor, yours may be inside an ngOnInit.
We will need to do two things:
Yeah, we will be doing a subscription inside a subscription. This may seem a little dirty at first, and there are better ways to handle multiple subscription. If you start to have 3 or more, I would suggest looking into rxjs forkJoin or something similar. We could also just not subscribe to it in our component and use the async pipe on the observable instead, which would be fine. I'm choosing not to do that because I want to console log the info.
Here's how my paramMap subscription looks now:
I noticed that I was tacking on a string to my paramData earlier in a map function. This is not only unnecessary, but would break our functionality, so if your map is tacking on a string like ' is the parameter we are on!', then remove it.
So now when we click on a room, we should have all the information from the database on that room being console logged:
Hooray! We got our room info! Our reservation list is an array! And it has the firebase as a key! Yeeeeeah! And no, that is not my email. Thats a certain student being funny.
If you've been ignoring styling your project (like I have), now is a good time to start on it, particularly as it applies to your room component. If you didn't get the pictures made by Mark Rogers, they are posted in the class slack channel. If you'd rather use your own, great! Just make sure that the name matches up. Try displaying the user's google photo somewhere. Also don't forget your layout for your room component: That router-outlet will contain either a list or a form. I ended up putting the room info on the left half and the router outlet on the right half, but do what works for you. Next up, we'll start tackling that room list. I will let you decide what, if anything, you want to display from that room data.