Discover Ember

Back to All Courses

Lesson 8

Creating the profile page

In this chapter, we'll put together all the knowledge that we have gained about templatingroutescomponents and Ember Data by creating the profile page!

This is what we want to build!

Templating with Handlebars and CSS

Let's start by creating the basic markup and style that we need based on our mockups. We'll use only static data for now.

For the HTML-page, we'll just add some layout-tags and add an aside-part with the user info.

{{! app/user/template.hbs }}

<div class="profile-cover cover-photo"></div>

<div class="stats-container">
  <div class="page-container">
    <img class="avatar" src="/images/avatar.jpg" />

    <ul class="profile-stats">

      {{#link-to "user.index" tagName="li"}}
        <h4>Chirps</h4>
        <p>0</p>
      {{/link-to}}

      {{#link-to "user.following" tagName="li"}}
        <h4>Following</h4>
        <p>0</p>
      {{/link-to}}

      {{#link-to "user.followers" tagName="li"}}
        <h4>Followers</h4>
        <p>0</p>
      {{/link-to}}

    </ul>
  </div>
</div>

<div class="page-container user-page">

  <aside>
    <h1>t4t5</h1>
    <p class="description">I like open-source software!</p>
    <p class="joined">Joined 5 minutes ago</p>
  </aside>

  <main>
  	{{outlet}}
  </main>

</div>

Well, that looks messy.

Let's fix it by sprinkling on some CSS magic. Create a new file called profile.scss in your app/styles-folder:

// app/styles/profile.scss

.profile-cover {
  height: 256px;
  background-color: gray;
}

.stats-container {
  $stats-height: 63px;
  background: #FFFFFF;
  border-bottom: 1px solid #DDDDDD;
  height: $stats-height;

  .avatar {
    border: 5px solid white;
    position: absolute;
    bottom: -80px;
    left: 47px;
    width: 156px;
    height: 156px;
    border-radius: 50%;
  }

  .follow-container {
    button {
      float: right;
      margin-top: 2px;
    }
  }

  ul.profile-stats {
    text-align: left;
    margin: 0;
    margin-left: 291px;
    padding-top: 12px;

    li {
      overflow: hidden;
      position: relative;
      height: $stats-height - 12px;
      padding: 0 20px;
      margin: 0;
      cursor: pointer;
      float: left;

      &::after {
        content: '';
        height: 5px;
        position: absolute;
        bottom: -5px;
        left: 0;
        right: 0;
        background-color: $link-color;
        transition: bottom 0.2s;
      }
      &:hover::after, &.active::after {
        bottom: 0;
      }

      h4 {
        font-size: 12px;
        color: #B5B5B5;
        line-height: 16px;
        font-weight: 400;
        text-transform: uppercase;
      }

      p {
        text-align: center;
      }
    }
  }
}

.page-container.user-page {
  main {
    margin: 40px auto;
  }

  main.expanded {
    margin-left: 300px;
    width: auto;
  }

  .profile-glance {
    width: calc(45%);
    width: -webkit-calc(50% - 13px);
    width: calc(50% - 13px);
    display: inline-block;
    vertical-align: top;
    margin: 10px 5px;
  }

  .about-description {
    margin: 10px 0 20px 0;

    button.small {
      padding: 4px 10px;
      font-size: 13px;
      margin-top: 6px;
    }
  }

  aside {
    width: 200px;
    margin-left: 30px;
    position: absolute;
    top: 0;

    h1 {
      font-size: 26px;
      color: #515151;
      line-height: 36px;
      font-weight: 500;
    }

    p.description {
      font-size: 15px;
      color: rgba(0,0,0,0.59);
      line-height: 22px;
      font-weight: 400;
      margin: 10px 0;
    }

    p {
      font-size: 15px;
      color: #8B8B8B;
      font-weight: 300;
    }

    p.joined::before {
      content: "";
      width: 20px;
      height: 20px;
      background-image: url("/images/clock-icon.svg");
      display: inline-block;
      position: relative;
      margin-bottom: -5px;
      margin-right: 7px;
    }
  }
}
// app/styles/app.scss

@import "profile"; // <-- The line to add!

@import "pod-styles";

Ahhh. There we go.

Alright, that was just the basic HTML/CSS stuff. Now, on to the real fun: the Ember part.

Adding the profile data

Just like in "home" we want the user's data to appear on the profile page.

First of all, looking at our mockups, we can see that we lack two attributes in our user-model:

  1. The "about me" description

  2. The date the user joined the service

Let's add these! We'll also add a chirps-attribute as a relationship that references the ids of the user's chirps, so that we can easily access them from the user object.

// app/user/model.js

export default DS.Model.extend({
  username: DS.attr('string'),
  numberOfChirps: DS.attr('number'),
  numberOfFollowing: DS.attr('number'),
  numberOfFollowers: DS.attr('number'),
  // New attributes:
  aboutMe: DS.attr('string'),
  joinedAt: DS.attr('date'),
  chirps: DS.hasMany('chirp', { async: true })
});

Of course, we also need to update our mock data in that case:

// app/mirage/fixtures/users.js

export default [
  {
    id: 1,
    username: 't4t5',
    numberOfChirps: 2,
    numberOfFollowing: 5,
    numberOfFollowers: 5,
    // The new data:
    aboutMe: 'I like making stuff.',
    joinedAt: new Date('2015-06-08T09:30:26'),
    chirps: [1, 2]
  }
];

Okay, now we have the data there. Remember what we need to do in order to fetch the data? That's right,** update the route-file**!

// app/user/route.js

import Ember from 'ember';

export default Ember.Route.extend({
  model: function() {
    return Ember.RSVP.hash({
      user: this.store.findRecord('user', 1)
    });
  }
});

Again, if you visit the profile page, you should see a console message from Mirage informing you that the request was successful

Next, we update the Handlebars-file to render the data in our template.

Try to do this on your own first, and peek at the code below if you get stuck (note: we only show the parts of the file that need to be updated).

{{! app/user/template.hbs }}

{{!-- The profile stats --}}
<ul class="profile-stats">
  {{#link-to "user.index" tagName="li"}}
    <h4>Chirps</h4>
    <p>{{model.user.numberOfChirps}}</p>
  {{/link-to}}

  {{#link-to "user.following" tagName="li"}}
    <h4>Following</h4>
    <p>{{model.user.numberOfFollowing}}</p>
  {{/link-to}}

  {{#link-to "user.followers" tagName="li"}}
    <h4>Followers</h4>
    <p>{{model.user.numberOfFollowers}}</p>
  {{/link-to}}
</ul>
{{! app/user/template.hbs }}

{{! The profile intro }}
<aside>
  <h1>{{model.user.username}}</h1>
  <p class="description">{{model.user.aboutMe}}</p>
  <p class="joined">Joined {{model.user.joinedAt}}</p>
</aside>

Data is there. Sweet!

Let's move on to our subroutes: chirpsfollowing and followers.

Loading the referenced chirps

Let's start with the data that we want to load in the chirps-subroute. Here we can do something nifty. Since we can already access the user's chirps through the parent route's model (with model.user.chirps), we'll just use a method called modelFor() to fetch the user route's model, and then get the chirps from there:

// app/user/index/route.js

import Ember from 'ember';

export default Ember.Route.extend({
  model: function() {
    // Using the parent route's model
    return this.modelFor('user').user.get('chirps');
  }
});

Going back to the browser, you'll now see, uh-oh, an error message again!

Because we specified that our user owns the chirps with id 1 and 2, Mirage tries to fetch them through GET-requests.

As you can see, our GET-requests for fetching specific chirps haven't been defined in the mirage/config.js-file yet, which makes the app crash. No worries, just like we did with our user, we add this line of code to give Mirage the ability to fetch a specific chirp record:

// app/mirage/config.js

this.get('/chirps/:id');

Now the data is fetched just like it's supposed to!

Back to our template. Remember that we made a chirps-list-component for the Home-page before? One of the great things with components is that they are reusable. And since the list of chirps on the profile-page looks exactly the same as the list of chirps on the home-page, it makes sense to just use the same component to render both!

{{! app/user/index/template.hbs }}

<main>
  {{chirps-list chirps=model}}
</main>

Dang, that was easy!

Here we truly see the power of components. We only had to pass in our new route-data to the component, and the logic, markup and styling just comes for free thanks to our earlier work!

Let's move on to the following and followers subroutes.

Followees and followers

Right now in our Fixture data, we actually don't have anything for linking up a user's followers and followees. Let's add that.

Remember: a user can both follow many users, and be followed by many users. Therefore, we need two hasMany()-relationships in our user model.

// app/user/model.js

// The attributes to add:
followees: DS.hasMany('user'),
followers: DS.hasMany('user')

Since we only have one user right now, our mock data user will only follow (and be followed by) himself. Notice that when using hasMany() we have to use an array with the user-IDs.

// app/mirage/fixtures/users.js

export default [
  {
    id: 1,
    username: 't4t5',
    numberOfChirps: 2,
    numberOfFollowing: 5,
    numberOfFollowers: 5,
    aboutMe: 'I like making stuff.',
    joinedAt: new Date('2015-06-08T09:30:26'),
    chirps: [1, 2],
    // Our two new attributes:
    followees: [1],
    followers: [1]
  }
];

Back to the browser. Oh no. Blank page. What happened? Let's check the console.

Aha. Since we have two hasMany()-attributes linking to the same thing, Ember needs to know the inverse relationships.

The inverse relationship between our attributes in this case is pretty simple: the inverse of a follower is a followee, and the inverse of a followee is a follower:

// app/user/model.js

followees: DS.hasMany('user', {
  inverse: 'followers'
}),
followers: DS.hasMany('user', {
  inverse: 'followees'
}),

That fixed the error! Now we need to update our two subroutes to fetch that data.

Right now, we'll just make these routes fetch all the users we have in our fixture data (without filtering). Don't worry, we'll improve this once we build our real REST API. The goal at the moment is just to make sure that the templates and styles render well.

// app/user/following/route.js

import Ember from 'ember';

export default Ember.Route.extend({
  model: function() {
    return Ember.RSVP.hash({
      users: this.store.findAll('user')
    });
  }
});
// app/users/followers/route.js

import Ember from 'ember';

export default Ember.Route.extend({
  model: function() {
    return Ember.RSVP.hash({
      users: this.store.findAll('user')
    });
  }
});

Since we're using findAll(), we also need to add the corresponding REST URL to our Mirage config-file:

// app/mirage/config.js

this.get('/users'); // <-- Add this line!

And finally we update the route's template. Again, we'll use a component that we've already spent some time creating: the profile-glance-component.

{{! app/user/followers/template.hbs }}

{{#each model.users as |user|}}
  {{profile-glance user=user}}
{{/each}}
{{! app/user/following/template.hbs }}

{{#each model.users as |user|}}
  {{profile-glance user=user}}
{{/each}}

Now the profile glances of your followers/followees should appear.

Final touches with computed properties

One problem we still have with our current user model is that there's no relation between the chirps/followers/following and the number of them that we see in the profile glance.

For example, in our fixture data, we say that our user has 5 followers (in numberOfFollowers), although the actual relationship-data only contains one single user-ID (in followers).

Wouldn't it be great if our numberOfFollowers-property was automatically deduced from the length of the followers array? Well, once again, we can easily achieve that through computed properties.

With computed properties, you can create new attributes for your model based on one or many of the other attributes your model has. So let's replace the attributes numberOfFollowers and numberOfFollowing with computed properties!

// app/user/model.js

numberOfFollowing: Ember.computed('followees', function() {
  return this.get('followees').get('length');
}),

numberOfFollowers: Ember.computed('followers', function() {
  return this.get('followers').get('length');
}),

Also, since we're using the Ember-keyword, we need to import the Ember library at the top of the file with:

import Ember from 'ember';

The first argument of Ember.computed() tells Ember what property to observe in order to calculate the computed property. In this case for example, we've decided that as soon as followers changes, numberOfFollowers should change as well.

We can actually do the same thing for our chirps attribute! Since we already have a relationship between our user model and the chirp model, all we need is a computed property there as well.

Your final model-file should now look something like this:

// app/user/model.js

import DS from 'ember-data';
import Ember from 'ember';

export default DS.Model.extend({
  username: DS.attr('string'),
  aboutMe: DS.attr('string'),
  joinedAt: DS.attr('date'),

  followees: DS.hasMany('user', {
    inverse: 'followers'
  }),
  followers: DS.hasMany('user', {
    inverse: 'followees'
  }),
  numberOfFollowing: Ember.computed('followees', function() {
    return this.get('followees').get('length');
  }),
  numberOfFollowers: Ember.computed('followers', function() {
    return this.get('followers').get('length');
  }),

  chirps: DS.hasMany('chirp', { async: true }),
  numberOfChirps: Ember.computed('chirps', function() {
    return this.get('chirps').get('length');
  })
});

As you can see, we don't have any raw numbers anymore, everything is computed on the fly on the client side.

This has some great advantages. Let's look at what would happen if we were to post a new chirp for example. We'll simulate it by updating the fixture data for our chirp model:

// app/mirage/fixtures/chirps.js

// Add this record to your fixture data:
{
  id: 3,
  text: 'Hello one more time!',
  user: 1,
  createdAt: new Date('2015-06-08T09:30:28')
}

Also make sure that you reference that record (with ID 3) in your user fixture:

// app/mirage/fixtures/users.js

export default [
  {
    id: 1,
    username: 't4t5',
    aboutMe: 'I like making stuff.',
    joinedAt: new Date('2015-06-08T09:30:26'),
    followees: [1],
    followers: [1],
    chirps: [1, 2, 3] // <-- Add the "3"
  }
];

Boom!

You now see all three chirps in the user's profile and, lo and behold, the number of chirps has automatically been updated to 3!

It is important to note that, even though the whole page was refreshed to show the changes in this particular example (because we're using fixture data), this kind of change will be instant and reflected in your UI as soon as the model data changes.

It's really a great feeling not to have to worry about updating all your data in the UI manually as soon as some data changes!

Great, we now have two pages that look pretty good. In the next chapter we're going to take a closer look at how to import different kinds of third-party libraries and use them in our Ember app.