5.1 The Project Component
It’s time to create an individual project page. This will introduce us to getting a parameter out of the route, so that we can display the correct project.
1.Introduction5 lessons, 29:28
2.The Projects List6 lessons, 39:35
3.The Users5 lessons, 31:28
4.The New Project Form4 lessons, 30:48
5.The Project Page4 lessons, 49:46
6.Conclusion2 lessons, 04:08
5.1 The Project Component
As we saw at the end of the last lesson, we can successfully create new projects now. But we don't actually have an individual projects page where we can view these individual projects. So that is what we're going to work on next. So let's create the project component where we will view individual projects. Let's begin once again in our app module file. The reason we're beginning here is because we're going to need a route for this new project. And it's going to be a little bit different from any of the routes that we have looked at so far. First of all, let's import the component. I'm just going to copy the line for the projects component and change that to the project component. And now let's create a new route. I'm going to duplicate the last one here. But instead of the path being projects/new, it's going to be projects/:id. And that's going to be a token which will be the id value for the current project, or for whatever project we're currently trying to view. This is how we will figure out which project we want to view. All right, so then the component will be the ProjectComponent. And of course, we'll leave the canActivate guard in place. So let's just finish this off by adding the ProjectsComponent to our list of declarations. And now we can go ahead and open up our new file. So that will be project.component.ts. Once again, we'll start with some imports. Besides Component and OnInit, we're going to need the ProjectsService as well as the AuthService, and finally, the Project class as well. All right, so let's use our Component decorator here. We'll set our selector to be project. We will set our templateUrl to be templates/project.component.html. And we will also set our providers list here to include the ProjectService. And finally, let's add our class, ProjectComponent. And now let's work on our class here. For the constructor, we will need our AuthService. We're also going to need our ProjectsService. All right. And now we'll write the ngOnInIt function. And now this is where things start getting interesting. Because this class needs to be a little bit different than any of the classes we've worked with so far. And here's why. As we've seen, this is going to be a top-level component which will be rendered directly by the router. To decide what we need to render inside of our component, that is, which project we need to show, we need to get an ID value out of the URL. And so basically, what we need access to inside of this component is the activated route. And to get access to this, we need to import something from the router. So let's go ahead and import two classes here. We'll start with the ActivatedRoute. And we'll also import the Params class just for the sake of TypeScript. And of course, both of these come from @angular/router. So we'll need to inject the activated route. So let's go ahead and inject a route here. And this is going to be ActivatedRoute. And this route object here is going to have kind of all the information we might need about the route that the user is currently on. Now, there's one property in specific that we're interested in. And that is this.routes.params. This is going to be any parameters that are inside this route. Now, parameters in a route are any of the tokens that we've put in. In our case, we're using the ID parameter, which is the ID of the project we want to view. Now, the routes.params object is actually an observer. So you might think we want to call subscribe here to get access to that params object. However, we're not going to subscribe. Instead, we're going to map. Now, remember a few lessons ago when we were talking about observables. We were comparing them to arrays in that they're both containers for another object. And you might remember that we said subscribe is more like for each, because it gives you the inner object without actually returning anything. However, when we use map, what we want to do is change that inner object and return a new observable with a different value inside of it. And so that's what we're going to do here. So we're going to get the params as an attribute here. And I'm going to use the params class for TypeScript to type this as a Params object. And what we want to return here is just params['id']. So we're just going to return the value of the id parameter. So what we return for map here is a new observable object that, instead of wrapping all the params, just wraps that one id. Okay, let's map this again. And now we get that id, what are we going to do with it? Well, what we can do is call this.service.getProject. And we can pass it that id. And remember, service here is our ProjectsService. And do we have a getProject function? I don't think so. So let's check that out. Let's go to the ProjectsService. And we don't actually have an individual getProject function. So let's go ahead and write this. I will put it right above getProjects. It takes an ID, which is a string. And it's going to return an observable that wraps a Project object. Now, if you've been thinking about observables, you might notice already that there's going to be a little bit of a problem. But we'll get back to that in a second. So let's just return this.http.get /api/projects/, and then we'll add that id on the end there. And then we can map that to this.extractData. And that should return the project from the server, the individual project. Okay, so if we come back here, let's look at what we've got going on here. We start with an observable of parameters. We map that to an observable that just has the string id. Then we map from the observable that wraps the string id to an observable that wraps whatever we get back from our getProject function. But as we just saw, getProject returns an observable that wraps a project. So that means after this last map here, we have an observable that wraps an observable that wraps a project. Now, we don't need that double observable wrapping. So what we're going to do instead of calling just map, we're going to call flatMap. And basically, flatMap will flatten our observable. In the same way that flattening an array converts nested arrays to just objects inside an array, flat map converts observables inside an observable into just the single observable outer layer with the one project, in our case, on the inside. Okay, so now our observable is an observable wrapping the project object that we need to display on this page. So it's at this point that we're going to call subscribe. And what we're going to get here is our Project. And we'll tell TypeScript that we expect this to be a Project object. And so now, in here, we could say, this.project = project so that we can display it on the page. And let's just add our project property up here. So we'll say project, should be of type Project. That's good. Let's do one other thing here. We want to have a property called canWork. And that is going to be a boolean which will default to false. And basically, this will return true if the user is involved in this project and false if they are not. If they aren't involved, they can view it, but they can only read it. If they are involved, they basically have write permissions. So to figure this out, we'll need to look at the users array on our Project object. But first, we need to use our auth service to get the current user. So we'll say this.auth.currentUser.subscribe. We will get the user. And we can say this.canWork = project.users.indexOf(user.username) should be greater than -1. Okay, so this will now give us the Project object and a canWork property to work with inside of our template. So let's go ahead and open up templates. And we'll create a new file called project.component.html. Let's start with an md-toolbar at the top. This toolbar will only be displayed if the user can work. So we'll say, ngIf canWork. And there's only one tool to show here. Let's have a button. We'll give it a routerLink. And this routerLink will be a relative link to conversations/new. And we'll eventually include this form where we can create a new conversation. For now, we'll just include this button. So that would be an md-raised-button, and the color will be primary. And then we'll have the text New Conversation. Okay, underneath this toolbar, let's create our md-card. And we'll start with the md-card-title element, which is, of course, going to be project.name. Underneath that, we will have the md-card subtitle, which will be project.description. Then let's have our md-card content element. And this is pretty simple. It's just going to be an array of anchor elements. And these anchor elements are going to be for each one of the conversations that we have in our project. So, let's do *ngFor and we'll say let conv of project.conversations. And let's give this anchor element a class of conv. And let's also give this a routerLink attribute. And we will bind that to an array which has ./conversations. And then we will also include conv, to get the current conversation, .id. Okay. And so that will give us a link to the individual conversation page. Again, we haven't made that yet, but we're going to in a couple of lessons. And so then the text here will just be conversation.name. All right, so this should be pretty much everything we need to display our project. But there's going to be a small problem. So let's head back to the browser here. And I will click on our first project here. And we have our problem. And at first, it may not seem entirely obvious what's going on. We have an error in the template, Cannot read property 'conversations' of undefined. So what's the problem here? Well, down here, we can't read the conversations attribute of an undefined object. So basically, what we see here is that project is undefined. What's going on here is that the project component is trying to render before we actually get our project from the server. And this is an issue you'll run into several times within Angular. When you're trying to get some asynchronous data, initially, of course, that data is not there. So Angular expects you to write your templates in such a way that they can render both before and after the data appears. Basically, you need to be ready to show something before there is any content. And so what we're going to do is just wrap everything here in a div. And we're going to give this div a *ngif. And we're just going to say if project. So if there is no project, then this template will not render. And we'll just get a blank page for a split second while we load that from the server. So let's come back to the browser and click our component again. And there you go. We have our toolbar at the top for our new conversation. And underneath, we have a list of the conversations that are part of this project. If we go to another project, for example, Annual Company Dinner, which we are not a part of. Notice that we do not have a toolbar at the top, because we don't have permission to work inside of that project. But if we come back to our Foobar App, we do have a toolbar, because we are part of this project. Okay, that's pretty good. There's one other thing I want to add to our project template here. And that's right at the bottom underneath everything else. Let's add a router-outlet object. Now, you might remember, the router-outlet object is what we use in our app component to allow us to render nested templates inside of this. So why are we putting one here in our project component? Well, the reason is that we're going to have templates rendered inside of this. Two templates in particular. First of all, there'll be a small form. When we click New Conversation, we want that to display within the context of this template. Also, when we click one of these links to view a conversation, we want that to display within the context of this template as well. So we'll be looking at those in the next couple of lessons.