Lesson 8

Creating the profile page

25

⚠️ This tutorial uses an old version of Ember. Some things might have changed!

In this chapter, we'll put together all the knowledge that we have gained about templating, routes, components 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


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
app/styles/app.scss


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

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

app/mirage/fixtures/users.js

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


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
app/user/template.hbs


Data is there. Sweet!


Let's move on to our subroutes: chirps, following 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

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


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


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

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

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

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
app/users/followers/route.js

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

app/mirage/config.js

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
app/user/following/template.hbs


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

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

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

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

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

app/mirage/fixtures/users.js


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.

Comments

Stuart Quinn

At this point, I got the error "Error while processing route: user.index Assertion Failed: You looked up the 'chirps' relationship on a 'user' with id 1 but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (DS.hasMany({ async: true }) )", which was fixed by amending app/user/model.js as follows: chirps: DS.hasMany('chirp',{async:true})

profile/avatar/default
Chris

@corinnebrady: Thanks! Without your comment finding a solution would have been much more painful. I think the author should update the tutorial to reflect this.

profile/avatar/default
Chris Morris

I also ran into this issue and the author really should include this into the tutorial or else reading the comments becomes *required* to proceed.

profile/avatar/default
Corinne Brady

@stuartquinn: I got this error as well, just add {async:true} to hasMany in user/model.js as discussed in earlier comments

Tristan Edwards

@Chris @ckmorris Thanks for the input guys. I've updated that part of the lesson now. The reason I left out the { async: true }-part is because it's the new default in the stable version of Ember 2.0. However, since there's still a gap between the releases of Ember CLI and the releases of Ember.js (the Ember team should really work on that), it might still be necessary to include it since it's not the default in 1.13. Thanks again for the feedback!

Daniel Jeffery

Image appears to be on the home page, according to the link ... but it is definitely the profile page. Also, on the web site the dates are rendering in the long machine format. You may know the latter already!

Tristan Edwards

@danieljeffery: You're right. This image was taken directly from Sketch, where the header stays the same whatever the design is. So I just copied that because I'm lazy. :) But yeah, it should be fixed to avoid confusion.

Theodoros Delis

For some strange reason, ember doesn't 'recognize' the new profile.scss, even though i have added it to app.scss. I have restarted the app, delete and create again profile.scss, but nothing, the profile page is rendered without any css styling.

profile/avatar/default
Ian Lenehan

I was struggling to understand why you needed an inverse relationship, and the fact it comes from the same model and refers to the other field makes it even more confusing. After reading this: http://stackoverflow.com/a/25700619/256272


it became clearer if you imagine two user objects, where one has the followers and the other has the followees field and you're drawing that edge between the two users.

Daniel Jeffery

Curious as to why it isn't making a single request to /chirps rather than multiple requests to /chirps/:id?

Daniel Jeffery

Actually I think I can figure it out ... it's going to see an array [1,2] etc, and so it is going to make individual requests.


If you had an array with fifty different entries, would it still make fifty requests for the information?

profile/avatar/default
Shannon Swanson

I'm on version 2.5 of Ember and when I got to this step I did not get the error messages shown here (/api/chirps/1). I feel I have followed all the step correctly because I have recheck all of them. Do you know if there are changes to Ember Data in 2.5 that has affected this example?

profile/avatar/default
Jerimy

If I put this in as you have it here, I get the following error... "Uncaught TypeError: items.slice is not a function". But, if I try to enter it the same as the home template, I get the else block and my chirps don't appear?

Daniel Jeffery

File name app/users ... should be app/user!

profile/avatar/default
Tim

Is there a benefit to these methods vs. just calling {{model.user.followees.length}} in the user/template.hbs ?

Daniel Jeffery

Something I saw a few days back ...


this.get('followees').get('length');


can be ...


this.get('followees.length');


Which I think is a little neater, simpler!

Tristan Edwards

@timherpderp: In this case, you're right, it would work the same. :)


One could imagine that in a more optimized version of the app, you would want to send out numberOfFollowing and numberOfFollowers as values from the backend instead of dynamically generating them on the frontend (for performance reasons). In that case, it might be good to have them as separate properties.


But in this example, it does not actually matter as you pointed out!

Kirk Israel

Actually I don't think you do?

profile/avatar/default
Shannon Swanson

I didn't

Tristan Edwards

Turns our it's actually not necessary since it's stored as a global variable. Imo, it's still a good practice to be explicit though. :)

Daniel Jeffery

And here??

Daniel Jeffery

Although it looks like you put a curly brace instead of a square bracket! :P

Daniel Jeffery

Just a reminder, this is in the wrong place eh?


Should this be in the app/mirage/fixtures/users.js file, and without the 'user' key?

Kirk Israel

(yeah, ping on this)

Tristan Edwards

@kirkisrael: @danieljeffery: Yup, thanks guys. I had forgotten to click the "Publish"-button on this chapter. :P

Daniel Jeffery

:)

profile/avatar/default
Varun Sudhakar

I got an unexpected token error and had to remove "user:" to make it work.

Daniel Jeffery

I noted that also, it wasn't there in the previous chapter so I wasn't sure if it was meant to be there

Tristan Edwards

@danieljeffery: @varunsudhakar3: You're absolutely right. Thanks for the heads up. I fixed it!

Kirk Israel

The chirps line needs to be chirps: DS.hasMany('chirp',{ async: true }) -- or else


"Either make sure they are all loaded together with the parent record, or specify that the relationship is async (DS.hasMany({ async: true }) ) " happens in the next steps

Tristan Edwards

@kirkisrael: @paridin: Actually, if you're using Ember.js 2.0 or higher, you can leave out the { async: true } since it's the default option. However, if you're on 1.13.X you will still need to specify it. :)

profile/avatar/default
Roberto Estrada

After looking for the why it's necessary add {async: true}, I found this async-relationships, and it says, that is because since version of ember 1.18 you will need to be explicit with this param.

zheleznov

Hm. On this step I see <DS.PromiseManyArray:ember558> intstead of number of chirps

William Fleming

What is this reference? "// app/user/index/route.js" I dont see that anywhere. If I try to update the "app/user/route.js" file it breaks.

Tristan Edwards

@williamfleming: That's the default subroute for the user page where the chirps are displayed. It should have been created in the chapter "Routes" through "ember g route user/index".

William Fleming

Oi vey! For some reason I didn't run that. Ouch on the oversight! Thank you.

zheleznov

Also doesn't work. I see blank area where the data's should be

Shankar Dhanasekaran

I tried this


return Ember.RSVP.hash({


chirps: this.modelFor('user').user.get('chirps')


});


following the same logic as in home/route.js and modified the 'user/index/template' to use {{chirp-list chirps=chirps}} but it's not working?


When should one use RSVP.hash?

Tom Kay

Is this missing "import Ember from 'ember';"?

Tristan Edwards

@tomkay: Yes you're absolutely right. I forgot it even though I specifically mentioned it a few paragraphs higher up, lol.

Daniel Jeffery

Is there a reason {async: true} was taken out from followees and followers? Up the page they are included here they are not? :)

Tristan Edwards

@danieljeffery: Nope, seems to be an error on my part. Although it's worth noting that in the next Ember version (stable 2.0), { async: true } will be the default!

Daniel Jeffery

Which is due in very soon!

Daniel Jeffery

So this says 'update the route-file' here, but doesn't the route file stay the same as it is in the last chapter?

Tristan Edwards

@danieljeffery: This is the "user"-page's route file. The one in the previous chapter was for "home"!

Daniel Jeffery

Right you are sir!

Ed Prats

What does it mean to be "inverse"? Does that mean the opposite of a follower is a followee?

Tristan Edwards

@edprats: Exactly!

profile/avatar/default
Kprataps

How can we create a more realistic follower/following scenario using fixtures as an example. For instance what if we add one more user to our app/mirage/fixtures/users.js and then show the relationship of following and followers. How can we achieve that?