Openhouse Add Favorites Functionality to the Show Page
Learning objective: By the end of this lesson, students will be able to implement functionality for favoriting a resource.
User story
With our listingSchema updated, we should be ready to handle the following user stories:
As a user, I want to see the number of favorites a listing has received, to gauge its popularity and potential demand. As a user, I want to favorite a listing, so I can easily find and review it later.
Conceptualizing the route
Our route should listen for POST requests on /listings/:listingId/favorited-by/:userId.
| Action | What It Does | HTTP Verb | Route |
|---|---|---|---|
| Create | Favorites a listing | POST |
/listings/:listingId/favorited-by/:userId |
🧠 We could technically omit
:userIdfrom our route since our auth middleware already gives us access touserthroughreq.session. However, the pattern you see here is common in development and worth practicing.
Building the UI
Next let’s add a ‘Favorites’ section to views/listings/show.ejs.
In this section, we’ll display the number of people that have favorited a listing, and a button for marking the listing as a favorite.
We’ll need to include two pieces of data in our POST request:
listing._iduser._id
Add the following to views/listings/show.ejs:
<!-- views/listings/show.ejs -->
<h2>Favorites</h2>
<p>Favorited by <%= listing.favoritedByUsers.length %> people.</p>
<form
action="/listings/<%= listing._id %>/favorited-by/<%= user._id %>"
method="POST"
>
<button type="submit">Favorite it!</button>
</form>
Scaffolding the function
Next, let’s build the scaffolding for our controller function. With this we’ll confirm that both the listingId and the userId are accessible through req.params.
Add the following to controllers/listings.js:
// controllers/listings.js
router.post('/:listingId/favorited-by/:userId', async (req, res) => {
try {
console.log('userId: ', req.params.userId);
console.log('listingId: ', req.params.listingId);
res.send(`Request to favorite ${req.params.listingId}`);
} catch (error) {
console.log(error);
res.redirect('/');
}
});
In your browser, click on the ‘Favorite it!’ button and check your terminal for the logged data.
Writing the controller action
Now that we have confirmed access to req.params.listingId and req.params.userId, let’s complete the controller action.
Our controller action will update a listing by adding a userId to the favoritedByUsers array. For this we’ll utilize the findByIdAndUpdate() method.
The findByIdAndUpdate() accepts two arguments:
- An
ObjectIdfor locating the document. - An object containing data to update the document with.
The update object we use here will be different from what you’ve seen in the past, as we need to modify an array inside the listing document. For this we’ll make use of MongoDB’s $push operator.
The $push operator is used to add a new value to an array. The operator requires that we specify the target array and the value to add.
When used inside an update object, we would see something like the following syntax:
const updateObject = { $push: { targetArray: newValue } };
For our purposes, we’ll be adding req.params.userId to favoritedByUsers.
Update the function as shown below:
// controllers/listings.js
router.post('/:listingId/favorited-by/:userId', async (req, res) => {
try {
await Listing.findByIdAndUpdate(req.params.listingId, {
$push: { favoritedByUsers: req.params.userId },
});
res.redirect(`/listings/${req.params.listingId}`);
} catch (error) {
console.log(error);
res.redirect('/');
}
});
At this point, you should be able to favorite a listing!
Modifying the view
Now that we can favorite a listing, let’s touch up our show page with some conditional rendering.
Once a user favorites a listing, they should no longer see the ‘Favorite it!’ button. We can determine if a user has favorited a listing by checking if their ObjectId is inside the favoritedByUsers array.
This determination can actually be handled in the corresponding controller action by creating a boolean called userHasFavorited. We can then pass this boolean to the show view, and conditionally render the ‘Favorite it!’ button based on its value.
Update the ‘show’ controller as demonstrated below:
// controllers/listings.js
router.get('/:listingId', async (req, res) => {
try {
const populatedListings = await Listing.findById(
req.params.listingId
).populate('owner');
const userHasFavorited = populatedListings.favoritedByUsers.some((user) =>
user.equals(req.session.user._id)
);
res.render('listings/show.ejs', {
listing: populatedListings,
userHasFavorited: userHasFavorited,
});
} catch (error) {
console.log(error);
res.redirect('/');
}
});
💡 The
some()array method returnstrueif at least one element passes the test in the provided callback. In this example, if at least oneObjectIdin the array matches that of the current user, the value ofuserHasFavoritedwill betrue. If there is no matchingObjectId,userHasFavoritedwill have a value offalse.
Next we’ll update our view with an if...else block:
<!-- views/listings/show.ejs -->
<h2>Favorites</h2>
<p>Favorited by <%= listing.favoritedByUsers.length %> people.</p>
<% if (userHasFavorited) { %>
<p>You've favorited this listing!</p>
<% } else { %>
<p>You haven't favorited this listing.</p>
<% } %>
🏆 Notice how concise our conditional rendering is here. This is a direct result of the additional work we did in our controller action. While it’s possible to achieve this functionality entirely in the view, it is better practice to handle complex logic like this in controllers.
Finally, move the <form> and <button> into the else block:
<!-- views/listings/show.ejs -->
<h2>Favorites</h2>
<p>Favorited by <%= listing.favoritedByUsers.length %> people.</p>
<% if (userHasFavorited) { %>
<p>You've favorited this listing!</p>
<% } else { %>
<p>You haven't favorited this listing.</p>
<form
action="/listings/<%= listing._id %>/favorited-by/<%= user._id %>"
method="POST"
>
<button type="submit">Favorite it!</button>
</form>
<% } %>
In your browser, try favoriting a listing. Verify that the UI changes as expected!