Multiple profiles
You might have noticed that our profile page still has a major problem: it shows only one profile!
Obviously we want to have **multiple users **on the site and be able to view their profiles as well, not just our own. In this chapter we're going to add a dynamic part to our profile URL (/profile/{username}
), and from that username, we'll load the requested user.
Let's first specify the new path in our Ember router-file with :username
(the colon specifies that the part is dynamic).
// app/router.js
this.route('user', { path: 'profile/:username' }, function() {
// ...
Then, we pick up the username-parameter in our route. Remember that when sending a query
-request, Ember will expect an array of values (even it if only contains 1 object), so we need to specify that we want to get only the first object in the array, which should be the only user that matches our username parameter.
// app/user/route.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function(params) {
return Ember.RSVP.hash({
user: this.store.query('user', { username: params.username }).then((users) => {
return users.get('firstObject');
})
});
}
});
Finally we need our REST API to support username querying, so let's just add that to our November API:
// app/controllers/user/list.js
// Place this right after the other if-statements in the same file:
if (req.query.username) {
findQuery = {
where: { username: req.query.username }
}
}
Boom! Now, instead of having your profile URL as localhost:4200/profile, it will be localhost:4200/profile/t4t5 (or whatever username you chose).
One thing left to fix is that we need to update all the link-to
tags that point to the profile page (you now need to supply them with the new username argument). Otherwise, you will get an error like this one when you try to click on any of the profile links:
Uh-oh.
Below are the changes you need to make to your markup files:
{{! app/application/template.hbs }}
{{#link-to "user" sessionAccount.currentUser.username}}
<img class="avatar" src="/images/avatar.jpg" />
{{!-- ... --}}
{{! app/components/profile-glance/template.hbs }}
{{#link-to "user" user.username}}
{{user.username}}
{{/link-to}}
{{! app/components/chirps-list/chirp-message/template.hbs }}
{{#link-to 'user' chirp.user.username}}
{{chirp.user.username}}
{{/link-to}}
To test that it works, you can log out, create a new user and go to the profile page. You should then be able to visit both /profile/t4t5 and /profile/bob
Adding a "follow"-button
Now that we have multiple users, who each have their own profile page, we can finally add some real following functionality! Let's make a component for our follow-button:
ember g component follow-button
The template for our follow-button will be very simple: if you're not yet following the user, we display a generic "Follow"-button, otherwise we show a red "Unfollow"-button.
{{! app/components/follow-button/template.hbs }}
{{#if isFollowing}}
<button {{action 'unfollow'}} class="red">Unfollow</button>
{{else}}
<button {{action 'follow'}}>Follow</button>
{{/if}}
The isFollowing property will be set by checking if any of the users in profile.followersmatches the logged-in user's id:
// app/components/follow-button/component.js
import Ember from 'ember';
export default Ember.Component.extend({
classNames: ['follow-container'],
sessionAccount: Ember.inject.service('session-account'),
isFollowing: Ember.computed(
'profile.followers',
'sessionAccount.currentUser'
, function() {
return this.get('profile.followers')
.isAny('id', this.get('sessionAccount.currentUser.id'));
}),
});
Let's now actually add this component to our profile page and specify that it should notbe visible on your own page (because you can't follow yourself dummy)!
{{! app/user/template.hbs }}
{{#link-to "user.followers" tagName="li"}}
<h4>Followers</h4>
<p>{{model.user.numberOfFollowers}}</p>
{{/link-to}}
<!-- This is the part to add -->
{{#unless userIsProfile}}
{{follow-button profile=model.user}}
{{/unless}}
ember g controller user
// app/user/controller.js
import Ember from 'ember';
export default Ember.Controller.extend({
sessionAccount: Ember.inject.service('session-account'),
userIsProfile: Ember.computed(
'model.user',
'sessionAccount.currentUser'
, function() {
return this.get('model.user.id') === this.get('sessionAccount.currentUser.id');
})
});
You'll notice now that, if you're logged in as bob, you won't see the follow-button on your own profile page, but you will see it if you visit *t4t5'*s (or any other user's):
There's our button!
To wrap it up, we need to code the follow
and unfollow
-actions for our button. Note that these request won't be fired through Ember Data, but are just standard jQuery AJAX calls. Therefore, we have to add some of the adapter functionality that we specified earlier manually (e.g. making sure the API host is localhost:9000 and appending the Bearer token as a request header).
// app/components/follow-button/component.js
// At this to the top of the page:
import config from '../../config/environment';
// Then inject the session service, and store the access token as a computed property that we can easily fetch
session: Ember.inject.service('session'),
sessionAccount: Ember.inject.service('session-account'),
accessToken: Ember.computed('session', function() {
return this.get('session.data.authenticated.access_token');
}),
// And finally add the actions:
actions: {
follow: function() {
Ember.$.ajax({
type: 'POST',
url: config.apiURL + '/follow',
headers: { "Authorization": "Bearer " + this.get('accessToken') },
dataType: 'json',
data: {
profileId: this.get('profile.id')
}
})
.done(() => {
// Add yourself to the profile's list of followers
this.get('profile.followers').pushObject(this.get('sessionAccount.currentUser'));
})
.fail(() => {
swal("Oops", "Couldn't follow user!", "error");
});
},
unfollow: function() {
Ember.$.ajax({
type: 'POST',
url: config.apiURL + '/unfollow',
headers: { "Authorization": "Bearer " + this.get('accessToken') },
dataType: 'json',
data: {
profileId: this.get('profile.id')
}
})
.done(() => {
// Remove yourself to the profile's list of followers
var myFollow = this.get('profile.followers').findBy('id', this.get('sessionAccount.currentUser.id'));
this.get('profile.followers').removeObject(myFollow);
})
.fail(() => {
swal("Oops", "Couldn't unfollow user!", "error");
});
}
}
As you can see, we use a custom AJAX URL in the same way as with the signup-action we created a few chapters earlier.
When clicking on the button, a request will either be sent to localhost:9000/follow or localhost:9000/unfollow.
Let's quickly generate the November actions for these requests:
november g action follow
november g action unfollow
// app/actions/follow.js
module.exports = function(req, res, render) {
if (!req.user) {
return render([412, "You need to be logged in to follow!"]);
}
if (!req.body.profileId) {
return render([412, "You need to specify who you want to follow!"]);
}
req.models.follow.create({
follower_id: req.user,
followee_id: req.body.profileId
})
.then(function(follow) {
res.json({});
})
.catch(function(err) {
render(err);
});
};
// app/actions/unfollow.js
module.exports = function(req, res, render) {
if (!req.user) {
return render([412, "You need to be logged in to unfollow!"]);
}
if (!req.body.profileId) {
return render([412, "You need to specify who you want to unfollow!"]);
}
req.models.follow.find({
where: {
follower_id: req.user,
followee_id: req.body.profileId
}
})
.then(function(follow) {
return follow.destroy();
})
.then(function(resp) {
res.status(204).json({});
})
.catch(function(err) {
render(err);
});
};
Now the Follow-button should work! (at least the first time)
Fixing the "following" and "followers" subroutes
The routes user.following
and user.followers
are right now always returning all of the existing users (and not just the followers or followees) because we're using this.store.findAll('user')
.
We want to change this to a request with a query that only returns the followers and followees.
There's obviously a mismatch at the moment. The stats say 1 follower, but the page shows 2 (all of the existing users)
We'll fix the queries by updating the route-files:
// app/user/following/route.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function(params, transition) {
// Fetch the username from the URL
var username = transition.params.user.username;
return Ember.RSVP.hash({
users: this.store.query('user', { follower: username })
});
}
});
// app/user/followers/route.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function(params, transition) {
var username = transition.params.user.username;
return Ember.RSVP.hash({
users: this.store.query('user', { followee: username })
});
}
});
Now we just need to add a few lines to our REST endpoint in November, right after the rest of the if-statements. We'll also update all of our if-statements to make sure that we sideload the chirp
, followees
and followers
relationships whenever we retrieve a user (so that their number of followers and followings gets computed correctly from the start).
// app/controllers/user/list.js
// Get the user from the token
if (req.query.me) {
findQuery = {
where: { id: req.user },
include: [
req.models.chirp,
{ model: req.models.user, as: 'followees' },
{ model: req.models.user, as: 'followers' }
]
}
}
if (req.query.username) {
findQuery = {
where: { username: req.query.username },
include: [
req.models.chirp,
{ model: req.models.user, as: 'followees' },
{ model: req.models.user, as: 'followers' }
]
}
}
if (req.query.followee) {
findQuery = {
include: [
{ model: req.models.user, as: 'followees', where: { username: req.query.followee } }
]
}
}
if (req.query.follower) {
findQuery = {
include: [
{ model: req.models.user, as: 'followers', where: { username: req.query.follower } }
]
}
}
Before and after sideloading. Now we get all of the relational data as well!
That's the right data!
Alright! Now our app is actually more or less finished. We can sign up, log in, post, view profiles and all the right data is loaded!
In the next chapter, we're going to look at another important best practice when developing an application, namely testing!