This month at the St. Louis Angular Lunch I spoke about Angular’s upcoming “component router”. Originally created for Angular 2 and to-be-back-ported to Angular 1.x, the component router serves as a replacement to the current routing options. While still too early to use in production it looks like a promising alternative. In this talk (video below) I outline where the component router stands and a brief example of how it can be used.
Here is the talk, transcribed to text. This is a lightly edited, draft transcription, so any errors are probably from that process.
Paul Spears: Well, welcome. Today we're going to talk about the new router. I am Paul Spears. I work at Oasis Digital. And that's me, you know, third.
Before we get too far with this, I'm actually kind of interested: is anyone actually using the new router yet? Anything real? Anyone using it at all? Even playing with it. All right. So then, we have a real, fresh group here.
Speaker from the audience: Anybody using the old router? Anybody using any router at all? Define the old router.
Paul: All right. First thing: what's it called, and why is it called that? Up until recently, it's always been referred to as the new router; the new router; the new router. And finally, we got around to creating an issue for it and saying, "Are we actually going to give this any name?" And the community bolstered and leading the front was the name Component Router. So from here on now, I'm going to refer to it as a Component Router.
Why Component Router? Well, the biggest idea behind this router is not so much the syntax or how it works, so much as the idea it is trying to convey. They're backporting more than just the tool for the router of 2.0. They're actually backporting the idea of routing to components in your application; the idea that your 2.0 applications are actually going to be clusters of visual components that you provide routes. So if that idea is what we're actually bringing back here with the Component Router, then it's going to help us on our way to 2.0 if we're using the Component Router in a way that’s akin to how it was originally intended.
Real quick, we're going to just run through the most simple steps that you can go through in setting up a router. Then when we're done with that, we are looking at some heavier code that I've put together, experimenting with it and seeing what it takes to actually put this guy in place. So I'm going to try and real quick get to these slides because I have some really cool stuff at the end that you might want to get into. Hopefully, we can get to it. If you have any questions, feel free and stop me, though.
All right. First things first. Where are we going to get this thing? Well, they have both an npm and a Bower repo where you can go out and get angular-new-router. When I signed up for the talk, "I know some things about the new router. I can put it in-depth more and give a talk on that." One of the first things that I discovered was that it was actually pulled from the 1.4 release. They kept promising that, "1.4 is going to have the new routers. It's going to have new the routers." And days before the release, "Oh, yes, by the way, the Component Hierarchy Router is not going to be in it. It's not quite finished."
The Component Router, we get it using angular-new-router, or you can compile directly from source. You actually have two different options for this, as well. You can grab it straight from the router's repo, or you can grab it out of the Angular 2.0 repo. And as of last night, anyway, the Angular 2.0 repo had more work towards the finished product than the router repository itself.
All right. Once we got it downloaded, I'll call them in, nothing surprising here. Include the source script, then you have to depend upon ngNewRouter in your module. Pretty simple stuff.
Now get access to it. First thing we have to do is either create or identify some sort of application-level controller. It's only recently have we started actually wrapping our entire application in a controller. We normally have – each of our components have their own individual controller and there needs to be any kind of communication. But outside of that channel, it goes to services or directives. Recently, though, we've actually been putting in a controller around entire thing. That's actually paying off for us because we actually need this for the new router.
With your controller in place, we'd inject the $router. $router, you'd it get from ngNewRouter, and that's what we have to set up. So unlike the old router and ui-router, you could actually set both of those during the config block of your application. This router, you'd actually set it up during the run phase of Angular. So you could actually add routes dynamically from it. Just kind of a cool bonus.
But, with that in mind, where you set up your router matters. The reason that we actually want an applicational controller is because we want that at the highest point in our hierarchy, so that controller is actually the first. And as a result, over else within a – our base routes, at least, are all established for our application. Yes.
Speaker from the audience: Did you do that as part of the run block?
Paul: I thought about that last night, like, "Ahhh, I don't have time to try that out". But I would imagine you can. That's part of the first thing I'm going to experiment, put it in place to see if I could get that done in the run block.
Yes, you want it at the highest level controller you can to make sure those routes are instantiated before you try and execute other controllers around the application.
Now we have our applicational controller and the router injected, we're going to go ahead and set that aside, and go create a component. So this is the idea that we're kind of backporting. A lot of people already thought about this concept and have been applying it already, but it becomes very explicit and in code for the new router. Components consist of a few things: template and a controller. That's it. Pretty simple. But if you're using this router, your template must reside:
Absolute there. Will not work any other way. Your controller must be named name-of-my-componentController. Will not work any other way. There is a way to get around this, but you're really swimming upstream if you have to go that route. So let's do that.
Here I have my homeController. And, let's see, here's the actual body of it. Something real simple. I'm just placing a message on it just to make sure that's coming around. And here I'm binding to that message. You notice I'm using the name of the component as the alias for the controller. So before, you would have some sort of controller as, stick it in somewhere in your code, and start pushing that. Here, that's automatically inferred for you from the name of your component. So we have our component. Let's route to it.
Speaker from the audience: Paul, back to this previous slide. That function in the bottom is the constructor. Is that right?
Paul: Right. Just like with controller as, this is the constructor function.
Speaker from the audience: I'm shaking my head because it starts smaller case.
Speaker from the audience: So in ES6, you'll probably write that as a class.
Speaker from the audience: And then the template, it's still in lowercase, right?
All right. Let's talk to this guy. So that's our application-level controller. We're going to call $router.config, and that accepts an array of the routes that we're going to try and navigate to. And, for this case, I'm going to slash, then redirectTo the only component that I have; so redirects to the /home path.
And this home, right here, is what determined how I make this, or I got that, how I named my controller. This one word — the name of my component — is what dictated how I wrote all of these.
So we have a way to route to it.
Now we have to add a viewport. Not to be confused with an Angular 2.0 viewport. Two complete different things. They're really trying hard to make that happen. They've created something called a viewport. And they already have something in Angular 2.0 called a viewport. These are not the same. So if you dig in to Angular 2.0, this may or may not be the same thing that you're thinking of.
And for this — before, we had ng-view or ui-view — you have ng-viewport. That's changing. Changing very soon, in fact. Soon it’s going to actually need ng-outlet, that you’d use here. Others are changing so soon, in fact, that if you use Bower install or npm install to go get this new router, you have to use ng-viewport. You grab it from the source and compile it. You have to use ng-outlet. So that's how soon this is coming down the pipeline.
That's it. Congratulations. You have your new router set up. Woohoo! Let's go to a bar and grab a Choice beverage; you earned it. But obviously, we still have a lot of things that we're usually missing from a real application here. Such as Resolve, Event Handlers, Nested Views, Controller As.
Let's take a look at resolve. We'll add another component. I want to call this component user. You want to route to it by going /user/:some-user-id. You'd use either of the two routers. The syntax are here. You know exactly what that is. That's going to be a parameter for our route.
Now how do we navigate to this other than typing an URL? Well, we have to add an ng-link directive which – it's a string that has the user or the component name. And if you have parameters, you actually execute this like a function, and pass in the parameters in an object whose keys are your primaries. Clicking on that, this will actually be interpolated and then shown as a fully-qualified path.
You have to add the actual component itself. I created components/user/user.html. I just have a simple binding to the user component — username property. Where it comes from, it's not so much important. Just know that that's coming from data from somewhere.
And then in our userController, we are going to inject $routeParams to get our routing parameters. And then a userService is going to be responsible for actually consuming the route parameters. And so here, I'm just saying, "Hey, userService, give me the user details. I'm passing in the id from $routeParams at this point." Using a promise there, when it comes back, setting a property on a control.
That's still not quite resolved. What if we actually wanted this to be required? Like, "I need you to have this fetched and available before you attempt to route."
Well, here is one way you could do that. Here, I'm saying, "In my userController, what if and instead of simply making this call and assigning the data into the then function, might restore the promise on the controller object that's associated with this call?” And now we're going to use the new router's lifecycle hook functions. We can actually extend our controller object canActivate function. And this is what the new router will look at to determine whether or not you can route to this location. Yes.
Speaker from the audience: This looks a lot harder than the old resolve.
Paul: It did. And if have time, I've got something really cool that kind of lazes out in a different direction. I agree. I first thought, "This seem a long step backwards." But what I realized is that it actually feels less like step backwards and more like, "Here we've actually given you more flexibility but it's up to you to come up with the solution for your problem." So things like, "Can I go to this route based on some arbitrary set of data that's already in a scope?" Or, "Can I go to this route based upon some other random state of a service?"
Rather than try and find the right event handler, just use promises here well, and you're in good shape. And I agree they could add some additional helpers that you might use to handle specific cases. And I wouldn't be surprised if that's coming. Because right now it's the biggest gripe point about the new router in the issue tracker. It's like, "We don't have event handlers; we really need event handlers." "Well, what do you trying to do, well, you can do it this way." Or, "What do you trying to do, you can do it this way." And it comes down to, "Do you want the API for the new router to handle all these cases for you, or you can learn promises just a little bit better and you can implement this that you need right here."
What is happening is because we're actually extending the userController, we actually have the same context for this, available here as we did up here. So now, I say, return this.dataPromise. I've returned the same dataPromise I stored up here. And I've returned that and say, "If this resolves successfully, then that means you can activate this route." If it fails to resolve, or just never resolves, or rejects, then it will not activate that route. But activating the route and running the controller are technically two different things now. So now, so long as I go through the route associated with this component, this controller bot will always execute. But whether or not it will route to it and utilize the execution of that depends upon the return of this canActivate function.
Let's look at some more code. Before we drop out of this, any questions about this? Yes.
Speaker from the audience: This canActivate function, do you have dependency injection available?
Paul: You do.
Speaker from the audience: Okay.
Paul: The idea behind the resolve is, we would say you have a template with all kinds of bindings on it, and those bindings are tied to some data that you're going to fetch from an async call. That's what basic – you know, reason why you would do this. But if you make that async call from your controller, what's going to happen is your views are going to instantiate; your controllers are going to run; templates are going to be showing up with these empty bindings, until that async call finishes and populates your data. Then everything refreshes. That's a long-running call. Your users are going to be staring at one page or a partially complete page for the duration of that. With resolve, that will actually block the navigation of the application from one route to the next, until all of the data points that you've put in that resolve function have had a chance to finish and succeeded at executing a list of asynchronous calls that you provide. It's about three different data points that you need to fetch. You can list them out. It won't route till they're all finish. Instead of seeing a blank page, you are seeing it hanging out on the old page until that's done; and then it move wholesale from one complete page to the next.
The resolve can also be used because of the fact that it will refuse to wrap them away from the previous route, unless a successful promise has returned. It has handled things like, "Hey, I need to look at the parameters associated with this route. And verify that they've even entered in something correct; if not, fail on routing; may redirect somewhere else, etc. So we get that kind of functionality out of it, as well. Yes.
Speaker from the audience: All right. For this path here, the module that's instantiating the home and user components, those two have to be injected into the module from myApp module. Correct?
Paul: Right. Up above is actually where I’ve got my mainAppModule. "Yes, I also need the — whatever I called it — my home module and then my user module.”
Speaker from the audience: I see pretty often what people will – use the router inside of a config block per module to do a standalone page that's reusable for a whole of applications. So you'd inject that one module –
Paul: – Sure.
Speaker from the audience: – and they'll create a route and stayed up talking to your application?
Okay? I have a few lessons left. I want to try and show you, guys, some code.
I made a real simple app just to play around with this. This is my Harry Potter Wizardary, not Wizardry. It's about wizards of Harry Potter. And what I started off doing was saying, "I'm actually going to make this app using the old router first. And then go through the pain of converting it so I see what happens along the way." So I applied the same kind of practices I would perform if I were ramping up to turn this into a real application. Just to walk you through this real quick so you have some context for what you're looking at. Really simply, you click Start here. You view a list of wizards. You sort through. Find the one you want. Click on it. Here's some placeholder text. All done. But you can see we have taken advantage of a bunch of different async calls to fetch data, taken advantage of the $routeParams up here. So got a little bit of stuff going on on the hood.
Let's go ahead and take a look at the code. This is my index html. We have covered the basics where you set up your router, you route, like, including it, and you establish a viewport.
We've established my application-level controller. So we have the basics in place. Now what's really interesting — and unfortunately, I don't think you can see it that well — is actually my folder structure. We know a list of our modules and our application and our files, as well, is similar to having you set up a Java application.
So I have my root-level app. And inside that, I've got my dataServices; my homeComponent; I've got a wizard directory which itself contains a wizard list and the wizard detail directory, which contain their own set of controllers and templates associated with just a piece of functionality. This is very typical of how we would start to build an application. And I didn't want to have to rewrite my file structure and folder structure to make this conversion.
Let's see what we have to do to actually make this work. I want to start by digging right into appjs. And here we have my mainAppModule and I have a config block. I've also got my AppCtrl in my function right here where I use $router.config. Now, before, this is where we supply that array of all the route objects.
What I've done is I've actually extracted that out and place it down here level to this feed block. So now, if you take this route list, put it straight in there – and I've cheated. There is no such thing as a template or controller property for $router.config. But it doesn't use it, so it doesn't care. So I've kind of went ahead and augmented the route object a little bit for any purposes later on. So if you need to use a component in some way other than that default manner, you need to establish how a controller is named and how a template is named.
The new router provides you with this $componentLoaderProvider that you can use at config time by injecting $componentLoaderProvider. And from there, you can call setCtrlNameMapping. It takes a function which takes a name. This name is your component name. This guy is going to pass in the name for your component that you're trying to route to. You have to say, "Based on this name, what is name of my controller?" So, if for example, a lot of people will name their controllers myComponentCtrl instead of the full word 'controller'. Even that doesn't work unless you do a few something like this.
So what I do instead of making this long, complicated rejects function that tries and maxes out, or tries to figure out the next instruction in my application, I say, "Okay, here I'm taking advantage of load that and simply find so item in my routeList that matches the name. So actually refers back to the routeList I've created down here. It grabs up controller name. So essentially, I've recreated the old router route object list. And I'm manually making that association right here by saying, "Okay, forward this route for home; here's my controller." I explicitly say that rather than trying to figure it out on the fly.
Likewise, with my template, I find the one with that name. I say, "Give me the template." So now this looks a whole like what we're used to with $router.configs. And now it doesn't matter where my components live, what they're called. So long as I make that essential relationship here, we're all set.
A little caveat: the reason why I make this little defensive copy here is that when you pass this routeList into config, it actually will go inside; it can manage it in a way that you actually don't have access to this component anymore. So I just made it a test copy so everything have theirs, I can have mine, and we can both live happily. A lot of heavy stuff here.
Paul: If I were starting from scratch, I would just go with the dice, make a components folder, puke my hundreds of components that are actually going to be in an enterprise application into that one folder, and promise I'll sit there every night that I'm using the new router.
Paul: That is like right up there with the "there are no event handlers" in terms of the amount of grief that they're getting.
Paul: Yes. I agree. It is complete garbage. And this is my way of saying, "You know what? I'm just going to recreate the old routing look and feel and go on my merry way."
Paul: I'm sure there's plenty of material for ng 1.2. They're also saving up for ng 1.3, as well.
Paul: Yes, there is a great thread that you, guys, really should check out the –. You want a good laugh. Go to the issue list for this router. There's chock-full of stuff you're just going to start chuckling at.
Paul: Let's go take a look at my wizDetail component here. You were saying that "isn't this a step backwards?" But, I think, it's more of a step forward and moving away from controllers that don't exist in 2.0. So this is now what controllers with the new router – I think this is what they're going to boil down to when people start using this for real.
This guy is going to go fetch some data about the wizard I've selected. I've got my controller, WizDetailCtrl. I've made it properly with capital W. And the only thing I've put in my controller is an empty variable. An empty variable that just is for the purpose of documentation, more than anything. The fact that, at some point in time, we're going to have access to some guy called wizardDetails. I, then, have my canActivate block, it goes out, and establishes some wizardDetailsPromise, and saves it. It returns that promise because we don't want to be able to activate this route once it succeeds. Then I used the activate lifecycle hook that runs once both of these have finished and succeeded.
And here is where I actually moved the bulk of my controller work.
Now, in here, this is where John Papa recommends an activate function activity in your controller.
You move all of that functionality in here. I think this is literally going to become a list of declarations of: these are the things you can start to see on a scope, none of them are populated yet, we’re going to see how that becomes.
I've stopped concerning myself about all the ways I've been threatened that I was going to get whipped if I did controllers the wrong way. I stopped thinking about that. Stop worrying about that for a minute. Think about just this as a pattern of: you add a controller, here's what has to be fetched before you can use this controller, and here's what we do with the data once it all comes back and is populated.
And it is actually a really nice pattern, separates out all those concerns. It can do nice chunks of: "each thing has a purpose and here's where it lies."
Going back to the big "What?" This will actually broke if you tried to inject scope instead of a watch right now. Whatever.
It's the state we're in today. Quite a really nifty stuff ,and start thinking about 2.0. But the only reason I would recommend using this today is if you know for a fact you're converting your app to 2.0; and you are okay with moving away from this ad-hoc stuff that I've done; and want to put in, identify your components; move to that component folder; and start working ahead of where they're at now and be prepped for 2.0.
But I would also wait until, at the very least, that they officially backport it. Because I'm guessing a lot of those gripes that we've even talked about — no event handlers, no official way to do a resolve, no official way to roll back a route that you can achieve with these — I think those are still coming down the pipeline. So my advice in general is to just wait a little bit longer. Not quite there yet.
That is all I have. Any more questions? Yes.
Speaker from the audience: Is there any particular reason that you're not using ES6 classes here?
Paul: If I'm not using ES6 classes – the reasoning? Yes, I'm just not using transpiler. And largely, if I've got an existing app like this, like what we have –. We haven't made that full jump to ES6 yet. Generally speaking, nothing. A lot of people just haven't made that jump yet.
Speaker from the audience: So the things you're doing — the whole wdc variable — will just go away?
Paul: Yes. I think that actually points to another good reason why you should probably wait in terms of –.
Because when you start using TypeScript, when you start using ES6, a lot more of these boilerplate falls away also.
Speaker from the audience: Like you, I was surprised when they dropped this from 1.4. How close to really shipping are they?
Paul: I'm not hopeful for coming soon. And especially since – considering that they've now stopped working on it in the actual router repo. They've gone back to doing work in the 2.0 repo, and they needed rethink some things there before rethinking how they're going to backport it to 1.x.
Paul: I would like to say, yes, there's all kinds of opportunity. I've been starting to make some noise on the issue list myself. There's actually an issue out there that's pretty much like pinging the Angular team, like, "Are you, guys, still around?" It's got, like, two months of discussion from nothing but community members with not a single comment from the Google team itself. Yes, their biggest concern is that they're not interested in improving a 1.x backport of something that you started in 2.0. They are trying to figure out how to make the 2.0 thing work, and then backport it.
Paul: Right. You're not going to see them accepting forks and full requests for a 1.x path of things. They're going to be forward thinking in that, "No, you need to find a way to write this ES6 and TypeScript that is compatible with 2.0.
So, anyone contributing, they're probably not going to go anywhere unless they come up with that solution that works both ways.
Speaker from the audience: Yes, there's still plenty of it, but you have to be deep into TypeScript and ES6 and the 2.0 code-base before you probably are to get too far.
Speaker from the audience: You might pull that website.
Paul: They have more open than closed right now. And there's all kinds of stuff that you'd expect to be solved –. Oh, something I forgot to mention early. Nested routes and sibling routes. It actually comes in the box. Sibling routes, you can have any number of them. You just kind of change the way you declare those routes by specifying that clone A and clone B mapped to viewport A and viewport B. Again, you have to name some parameters there. But nested routes, they're like, "Oh, yes, you can go to nested views. That's not a problem." And then, people are opening an issue track. It's like, "No, it doesn't work. See I can't; it doesn't." "It works."
Paul: Yes. But, I think, if you take anything away from this, the thing I think is the most important is starting to think about your applications in terms of components, what they're composed of; and how you can just derive those isolations as best as you possibly can. That is, regardless of how this ends up, that's what's going to get sucked in from 2.0.
Paul: Here we go. This is the one I was referring to. So it's only 20 days but I feel like, to be like there is no one. "Hey, guys, anyone there?" Yes, nothing.
Paul: Oh, yes, for clicking a home button. Yes, I think –
Speaker from the audience: – Is that actually triggering a refresh?
Paul: I think that's actually true of being resolved, I mean, the other mechanisms now. I think you're better suited to take advantage of the other ones that we didn't discuss such as canDeactivate and deactivate. You could actually only fire that process off for some of those data. If you really have one really big thing that you don't want to fire off unless you're really sure you're going to land on that route, you put it on a canDeactivate. And that will only happen as it tries to navigate away or, like, some combination that I'm not sure exactly. But there are lifecycles I think we could utilize there to achieve what you're trying to do.
Any other questions? Yes.
Speaker from the audience: I’ve been hoping that this Component Router would really let me finally embrace that, so that I was kind of routing to this feature level but I could –. It looks like they're almost there, but I don't have to have a template and a controller. Oh, I have those things inside of a directive. Now I kind of route straight through directive; or do I really still have to wrap this thing up? Like, at this point, my use of ngRoute is this very thin routes essentially by drawing this template that is its direct but never-before route.
Paul: Yes, I 've not seen it anywhere yet. But I would expect that the biggest hang up for that is the vast difference between Angular 2.0 directives and our directives.
Speaker from the audience: Yes. I would say that it makes sense that they did it this way, given what you say about Angular 2.0 and all that stuff.
Speaker from the audience: Yes, and I get that.
Paul: All right, guys. Thanks.
Audience: Thank you.