This document describes how the routing in Aurelia v2 differs from the routing in v1 (and other frameworks which uses a url-based routing). If you're new to client side routing, Routing with Aurelia might be a better starting point.
In addition to having a new way of dealing with routing, the v2 router also supports the more traditional, url-based routing of v1. This document will however focus solely on the new component-based routing.
The main difference between v1 and v2 routing is that in v1 you _compose a url string_ to tell the router, via an alias (a configured route), which components you want to load. In v2 you tell the router directly _which components to load_ and the router will take care of the rest, including creating the url string.
The url-based routing of v1 requires some kind of configuration to connect url strings to sets of components in viewports. The component-based routing of v2 doesn't require any "connect configuration" since we're working directly with the end result: which components to load.
View code example
app.ts - configure the route/url string users to load Users component
public configureRouter(config: RouterConfiguration, router: Router) {
config.map([
{ route: 'users', name: 'users', moduleId: './components/users', },
]);
}
app.html - link with route/url string above
<a href="#/users">Show users</a>
<router-view name="main"></router-view>
app.ts - nothing is needed in app.ts
app.html - link directly with the name of the component to load
<a href="users">Show users</a>
<au-viewport name="main"></au-viewport>
How does the router know where the component is?
In Aurelia 2, all component dependencies must be registered either globally or on the component that has the dependency. This is a general principle and not just something for routing. The router uses the dependency registration to match component names to components. The actual location, the file, of the component is therefore never needed. You can read more about registering dependencies here.
While we've tried to avoid breaking changes, the differences in behavior have merited some name changes.
View list of commonly used names that have changed
In html
router-view -> au-viewport
In javascript/typescript
canActivate -> canEnter
activate -> enter
canDeactivate -> canLeave
deactivate -> leave
navigate -> goto
navigateTo -> goto
href -> au-href
Might be confusing. It really hasn't changed. And route-href isn't au-href either. So maybe leave this part off for now.
@davismj You're right; removed it. Thanks!
While I really like the new v2 mental model for it's simplicity, We used the old configuration model for more than just pointing route X to component X, 3 things from the top of my head:
{
route: ["task-update"],
moduleId: "app/tasks/task-update",
title: "Edit task",
name: "task-update",
settings: {
roles: [Roles.Register, Roles.Planner, Roles.Administrator]
},
},
With the new model, where would you put stuff like this? One solution would be to decorate your components with this security metadata, which seems fine. But you would still lose the flexibility of defining 2 routes pointing to the same component, where the authorization of route 1 differs from route 2 (for example, a create route and an update route, pointing to the same component)
{
route: ["task-update"],
moduleId: "app/tasks/task-update",
title: "Edit task",
name: "task-update",
activationStrategy: activationStrategy.replace,
},
The same argument as above, you can also configure this on your component but you would be out of luck when you want to do one or the other depending on a specific route
I would like to repeat I really like the new model, but we need at least 2 of the things I mentioned on each project we do, so I'm interested in hearing your opinions on how you would tackle these scenario's in the new model.
The old model will also still be available, and to a certain extent the two will be usable in conjunction. This might be one of those cases where that would be the way to go.
The old model will also still be available, and to a certain extent the two will be usable in conjunction. This might be one of those cases where that would be the way to go.
I am fully aware of that, but my point implies we can never use the new model if stuff like this isn't possible, and I'm quite sure many others have the same requirements.
Also, I am looking into making the aurelia openid connect plugin compatible with V2, and right now I don't see a way to incorporate the new router into it. It would be a shame to require users to always use the configuration model if they want to use the OIDC plugin IMO
What I meant by
and to a certain extent the two will be usable in conjunction
Is that you could do stuff like this:
@route({ settings: { roles: [Roles.Register, Roles.Planner, Roles.Administrator] } })
export class TaskUpdate {
}
```ts
@route({ activationStrategy: activationStrategy.replace })
export class TaskUpdate {
}
To associate additional settings with a particular component via configuration, while relying on the new model to figure out the rest (path, name, etc) by convention.
The general idea behind the new model is that most configurable properties can be specified in the path / instruction without having to configure them. For some configurable properties this is either tricky to do, or for various reasons just not desirable (e.g. you might want to keep the url clean / looking more like traditional urls).
To address this point:
> But you would still lose the flexibility of defining 2 routes pointing to the same component, where the authorization of route 1 differs from route 2 (for example, a create route and an update route, pointing to the same component)
This could be addressed in several ways. The first one, being to define this on the component itself with two separate route configs:
```ts
@route({ path: ':id', settings: { roles: [Roles.Editor] } })
@route({ path: 'new', settings: { roles: [Roles.Anonymous] } })
export class EditThing {
}
This is an example of the old and new model working in conjunction. Only the path is specified along with custom settings to be able to differentiate the two, but this is on the component itself. There is still no configuration on the "parent" side.
That said, there could be configuration on the "parent" side regardless, even combined with some config on the component itself:
{
path: ':id',
settings: { roles: [Roles.Editor] },
component: EditThing,
},
{
path: 'new',
settings: { roles: [Roles.Anonymous] }
component: EditThing,
},
// ...
// Not differentiated by path, so applicable to all paths
@route({ activationStrategy: activationStrategy.replace })
export class EditThing {
}
@arnederuwe The new router comes with _routing hooks_. There'll be several of them, covering different parts of "centralized configuration". Currently, the api is
router.addHook(callbackMethod, options);
where callbackMethod is a method you provide with the logic you want for the specified hook. Parameters into and return value from the callback method varies depending on which hook type it is. The options in the options parameter also varies depending on hook type, but one is consistent: type, that specifies which hook type it is.
So, for your first two use cases I'd say the BeforeNavigation hook could be useful. For example
this.router.addHook((instructions: NavigationInstruction[]) =>
{
for (const instruction of instructions) {
if (!this.verifyAccess(instruction.componentName)) {
return false; // Prevents the navigation, it's also possible to redirect
}
}
return true; // Proceed
},
{
type: HookTypes.BeforeNavigation, // Default, can be omitted in this case
exclude: [ '', 'login' ], // Exclude public components from this hook. There's also an `include` option
});
checkAccess(component) {
const requirements = {
'task-view': [Roles.View],
'task-update': [Roles.Register, Roles.Planner, Roles.Administrator],
}[component];
for (const requirement of requirements) {
if (user.roles.some(role => role === requirement)) {
return true;
}
}
return false;
}
Okay, pretty ugly code, but hopefully it illustrates the possibilites regarding centralized configuration.
As for the navigation, any list structure will suffice to iterate through.
<nav>
<ul>
<li repeat.for="link of [{ link: 'tasks', title: 'Tasks' }, { link: 'closed-tasks', title: 'Closed tasks' }]">
<a goto="${link.link}">${link.title}</a>
</li>
</ul>
</nav>
There's also an au-nav custom element that lets you do this in code
<au-nav name="tasks-menu"></au-nav>
router.setNav('tasks-menu', [{ component: 'tasks', title: 'Tasks' }, { component: 'closed-tasks', title: 'Closed tasks' }]);
The au-nav also supports child menues (children) and styling options. You might want to keep full control for complex menues and use cases, but for basic use cases it's a way to quickly and easily get a navbar (or tab list!) going.
Bottom line is that the goal is that if you're able to configure it centrally in v1, you should be able to do it with the new routing approach as well (where it makes sense).
Thank you @fkleuver and @jwx !
You provided me with everything I need in order to start putting something together, it seems I was missing some critical information about the new model, all cleared up now :)
To be clear, the APIs I mentioned don't exist yet. That's what I'll be working on towards the end of this month and beginning of January.
As mentioned on Discord, That's fine :)
Give me a ping when this is ready to testdrive
Im also trying to put some routing example together, but it seems none of the is already available? Or am I looking in the wromg place?
I've never liked how popular routers required me to manually specify url strings in config files and then remember and refer to them in HTML. Those urls had to be maintained manually and generally had no support for intellisense or strong typing for correctness in IDEs.
I think the new approach is a good step forward (or a step towards what non-web development had in early 2000's :smiley: )
Some live coded routing can be found here.
Most helpful comment
Some live coded routing can be found here.