IRouteConfig interfacePlease let me know if there are any use cases that are missing from this specification.
Route configuration can be done one of three ways: Up-front (no definition yet), using the @route decorator, or by defining a static routes property. For clarity, I've included the most terse definition with the decorator approach and the most verbose definition with the static routes approach for each example. This will demonstrate what they will look like in a typical development case as well as an explicit definition what they imply. I've also commented out implicit or conventional values that will be set by Aurelia under the hood.
IRouteConfigThe names of properties are up for grabs. I have chosen names that I believe are most illuminating and most standard.
interface IRouteConfig {
// A string or array of strings of matching route paths.
// (Renamed from route in vCurrent)
path: string | string[],
// A constructor function for the view-model to attach for this route.
// For route decorator and static routes approaches, Aurelia will set this
// value under the hood.
// (Repurposed from moduleId in vCurrent)
component?: IComponent,
// A uniquely identifiable name for the route, for canonical navigation.
// For route decorator and static routes approaches, Aurelia will try to
// set this value by convention if not specified explicitly.
name?: string,
// Optional, the name of the viewport to attach the controller to. If not
// specified, the default viewport will be used.
viewport?: string,
// Optional, the name of the parent route or routes, matched by the `name` property.
parent?: string | string[],
// Optional, flag to opt the route out of the navigation model. Defaults
// to true.
nav?: boolean,
// Optional, an object with additional information available to the
// view-model throughout the activation lifecycle.
// (Renamed from settings in vCurrent)
meta?: any
}
Defining a basic route is dead simple.
@route('home')
export class MyViewModel {
static routes = [
{
path: 'home',
//, name: 'my'
//, component: this
}
];
}
Multiple routes can redirect to the same route, creating a canonical url for that page.
@route('home')
@route({ path: '', redirect: 'home' })
export class MyViewModel {
static routes = [
{
path: 'home'.
//, name: 'my'
//, component: this
},
{
path: '',
redirect: 'home'
}
]
}
Alternatively, multiple routes can load the same page without redirecting the route.
@route(['', 'home'])
export class MyViewModel {
static routes = [
{
path: ['', 'home'],
//, name: 'my'
//, component: this
}
]
}
If a page has multiple viewports defined, each route should specify which viewport the route targets. Multiple route configurations can target the same paths.
<template>
<viewport name="left"></viewport>
<viewport name="right"></viewport>
</template>
@route({ path: 'forwards', viewport: 'left' })
@route({ path: 'backwards', viewport: 'right' })
export class FirstViewModel {
static routes = [
{
path: 'forwards',
viewport: 'left',
//, name: 'first'
//, component: this
},
{
path: 'backwards',
viewport: 'right'
//, name: 'first'
//, component: this
}
]
}
@route({ path: 'forwards', viewport: 'right' })
@route({ path: 'backwards', viewport: 'left' })
export class LastViewModel {
static routes = [
{
path: 'forwards',
viewport: 'right',
//, name: 'last'
//, component: this
},
{
path: 'backwards',
viewport: 'left'
//, name: 'last'
//, component: this
}
]
}
If two configurations target the same paths and viewports, a warning should be raised.
@route('home')
export class HomeViewModel { }
@route('home') // Warning! Duplicate route definition for path "home" in viewport "default".
@route({ path: 'home', viewport: 'side' }) // OK.
export class NotHomeViewModel { }
NOTE I've done my best to get various points of views concerning the importance of child routes. I'm still looking for use cases that may not be covered here.
A route can be defined as a child route by specifying its parent by name. This allows (a) creating a child viewport that is refreshed without touching any other viewports and (b) defining a more flexible navigation model (below) without having to maintain a separate route configuration table.
// 'parent' loads parent only into the default viewport
@route({ path: 'parent' })
export class ParentViewModel { }
<template id="parent">
<viewport></viewport>
</template>
// 'child' loads child only into the default viewport
@route({ path: 'child' })
// 'parent/child' loads child into the default viewport of parent
@route({ path: 'child', parent: 'parent' })
export class ChildViewModel {
static routes = [
{
path: 'child',
//, name: 'child'
//, component: this
},
{
path: 'child',
parent: 'parent',
//, component: this
}
]
}
Paths defined with a leading forward slash match the app's base url. This is particularly useful when you want a child route to match a path independent of its parent's path.
// 'special-child' loads child into the default viewport of parent
@route({ path: '/special-child', parent: 'parent' })
export class ChildViewModel {
static routes = [
{
path: '/special-child',
parent: 'parent',
//, component: this
}
]
}
Parameterized routes define dynamic sections of the path (parameters) that are made available throughout the activation lifecycle. If a type is specified, the route will only match values that can be coerced to the specified type, and the value will be available as the specified type throughout the activation lifecycle.
// matches 'foo/bar1!@#$%'
@route('foo/{name}')
// matches 'foo/123' and 'foo' but not 'foo/bar'
@route('foo/{id?:number}')
export class MyViewModel {
static routes = [
{
path: 'foo/{name: string}',
//, component: this
},
{
path: 'foo/{id?: number}',
//, component: this
}
];
}
In addition to native JavaScript types, custom types can be registered with the router.
router.registerParameterizer('Guid', (param: string) => {
if (/^[0-9a-f]{8}-?(?:[0-9a-f]{4}-?){3}[0-9a-f]{12}$/i.test(param)) {
return param;
}
// If no value is returned, the route does not match.
});
// matches 'foo/6e380650-8b50-4cb4-8a09-a5449abf597b' but not 'foo/123'
@route('foo/{id: Guid}')
export class FooViewModel { }
Special metadata can be specified on each route that is made available throughout the activation lifecycle. This is particularly useful for defining implicit or hidden parameters when a user navigates through a particular route.
@route('')
@route({ path: 'other', meta: { referral: true })
export class MyViewModel {
static routes = [
{
path: '',
//, component: this
},
{
path: 'other',
meta: { referral: true },
//, component: this
}
]
activate(params, config) {
config.meta.referral === true;
}
}
Very nice and appealing declaration. One thing that could be hard with this per child declaration is the ability to handle unknown route. What options do we have if we go for this path ?
I like the direction this is heading. Here are a few quick thoughts:
viewport, I'd like everything to work through the core compose element, if it can. So, what about a custom attribute approach, like this?<template>
<compose viewport="left"></compose>
<compose viewport="right"></compose>
</template>
?.@route('foo/{id?:number}')
When it comes to the route configuration, "controller" is a bit of an odd word, given how vNext works. There's no such thing as a controller. Literally, there are only components and views (referred to as "renderables"). The compose element can handle a component constructor, a view or anything that can create a view (such as an IViewFactory). So, for view-models, those are really treated as components. In JIT mode, they will need to be decorated with the customElement decorator and have their view specified. In AOT mode, the compiler will evaluate conventions and auto-generate the decorators with the view hookups (making the dev experience just like today).
I'm a little confused about how child routes will work, based on the explanation above. Can you help me understand that better?
Have you thought about what you might be able to do with AOT? For example, could you search the code for all @route decorators and generate the route config file ahead of time? This could allow the routes themselves to be lazy-loaded while still enabling the router to have all information up front.
Will there be child router instances or just one central router that handles everything? If you think it's possible to have a single router with child routes rather than a bunch of child routers, I believe that's preferable.
- Instead of introducing a new custom element viewport, I'd like everything to work through the core compose element, if it can. So, what about a custom attribute approach, like this?
<template> <compose viewport="left"></compose> <compose viewport="right"></compose> </template>
<viewport> element is that it is clear and declarative. It says what it does and does what you'd expect. I agree that under the hood it should just be syntactic sugar over a <compose> element.Do you think there is an advantage to using not just the same component but also the same API?
Yes.
controller > component
The route configuration allows specifying a parent by name. Consider:
@route({ path: 'employee', redirect: 'employee-new' })
@route({ path: 'employee/0', name: 'employee-new' })
@route({ path: 'employee/{empId: number}', name: 'employee-edit' })
export class EmployeeEditComponent { }
// these are available child routes on any of the above routes
@route({ path: 'resume', parent: ['employee-edit', 'employee-new'], redirect; 'resume-new' })
@route({ path: 'resume/0', name: 'resume-new', parent: ['employee-edit', 'employee-new'] })
@route({ path: 'resume/{resId: number}', name: 'resume-edit', parent: ['employee-edit', 'employee-new'] })
export class ResumeEditComponent { }
Navigating to employee/1/resume (1) redirects to employee/1/resume/0, (2) loads EmployeeEditComponent into the default viewport, (3) loads ResumeEditComponent into the default viewport in the EmployeeEditView.
Yes. That's one of the unchecked todos.
I'm not sure if it is possible. I'm just dreaming up what is preferable and ideal here. I agree, child routes are great but child routers are a maintenance nightmare.
This looks like it's really starting to shape up. One thing I can't see in this configuration proposal is support for layouts which we have in vCurrent:
For example: layoutViewModel: PLATFORM.moduleName('layout');
Is there a plan at this stage to port the router's layout functionality into the vNext configuration as well?
I'm wondering if there's a way to do it with the normal primitives in vNext. For example, the compose element dynamically creates elements and can project content into slots. So, maybe we can manage something like this:
<compose subject.bind="someLayoutComponent">
<compose viewport="left" slot="sidebar"></compose>
<compose viewport="right" slot="main"></compose>
</compose>
The idea is that someLayoutComponent produces a custom element that defines the layout with slots for the sidebar and main areas. Then the other two compose elements pipe the router's left viewport into the sidebar slot and the right viewport into the main slot.
I'm not sure if we can make it work this way or not yet, but it's worth thinking about before adding a router-specific feature for layout I think.
As @EisenbergEffect said, <compose> now support slots, which is going to check all the boxes that layouts previously checked.
To enhance his example a bit: Imagine that you have two routes that represent the same view with a different layout. You can add this extra data to the meta property (formerly settings) and handle that directly in your view via a compose element.
foo.ts
@route({ path: 'x', meta: { layout: 'layouts/horizontal' } })
@route({ path: 'y', meta: { layout: 'layouts/vertical' } })
class FooComponent {
activate(params, meta) {
this.someLayoutComponent = () => import(meta.layout);
}
}
Currently, the layout feature is part of the router but has distinct logic. There are two problems with this. From the ivory tower, layouts have nothing to do with routes. The router is designed to interface between Aurelia and the browser's URL bar. If layouts do belong, they don't really belong in the router. From the ground, it is extra complexity, extra logic, and extra code to maintain.
Layouts behave identically to compose components, and the existing meta and <compose> with slot features can achieve the same result.
@freshcutdevelopment I'd really love to hear a strong defense of the layout feature. I've never understood what problems it solved that couldn't be solved before.
@davismj @jods4 is a user of layout I believe. Would be nice if @jods4 could also input something too
Thanks everyone for the input, I think I see where you're going with this, and can definitely see the benefit in reducing the concept count if this is no longer required.
@davismj , to give you some context, what I typically use layouts for is to have a separate 'layout' for the login page (where I don't need a nav) as compared to the regular application pages (which would have a nav). Other things I might include in the main layout would be things like breadcrumbs etc.
So this ends up looking like:
<!-- app.html -->
<router-view></router-view>
<!-- login-layout.html-->
<slot></slot>
<!-- main-layout.html-->
md5-12f590c052df2dd7f07d091124503799
<slot></slot>
The way layouts currently work feels very similar to the way ASP.NET MVC Razor layouts work, which is likely what attracted me to them.
If we can achieve this behavior dynamically using <compose> in vNext then awesome 馃憤
@davismj
This looks pretty neat, just some quick thoughts, Is it possible to have access to the children routes of a parent route in vNext routing mechanism, and how does authorization steps will fit in?
And on thing more, I just saw au-viewport, viewport, router-view and compose , in routing all looks to do the same job, am I right? Will they in the future become one thing at least in routing module?
I think we've settled on <au-viewport>, which is just a compose that is controlled by the router.
Sorry to provide feedback this late, but here are my thoughts on the proposal.
The decorator based API for specifying routes looks very elegant, easy to understand, as well as familiar (referring to ASP.NET WebApi) 馃憤 Same goes for route parameter constraints.
Apart from that, I would like to point out a potential problem with the proposal of specifying the child routes. I will try to explain.
In vCurrent, the child routes are typically defined in child/index vm, without any explicit knowledge of the invoking parent. The parent route configuration comes from one level higher (typically from similar parent/index vm), with the path fragment defined for the parent.
This disassociation lets us put the complete child module (routes, vms, views, etc.) in a different node package altogether. And the child module doesn't need to know what the parent route is. The "main" app can then register this child module as a plugin and act as a hosting app that hosts multiple child modules. The main app can flexibly decide under which parent path fragment it wants host a particular child module. IMO this reduces code duplication, and provides better separation of concerns.
I can imagine that the AOT compiler can process such metadata (as proposed) to create the routing table. However, my first impression after seeing the child route specification was "why the child vm need to know about parent vm?"
Do you think this is a valid concern? Are there more robust alternative solutions for this scenario?
Most helpful comment
As @EisenbergEffect said,
<compose>now support slots, which is going to check all the boxes that layouts previously checked.To enhance his example a bit: Imagine that you have two routes that represent the same view with a different layout. You can add this extra data to the
metaproperty (formerlysettings) and handle that directly in your view via a compose element.foo.ts
Currently, the layout feature is part of the router but has distinct logic. There are two problems with this. From the ivory tower, layouts have nothing to do with routes. The router is designed to interface between Aurelia and the browser's URL bar. If layouts do belong, they don't really belong in the router. From the ground, it is extra complexity, extra logic, and extra code to maintain.
Layouts behave identically to compose components, and the existing
metaand<compose>with slot features can achieve the same result.@freshcutdevelopment I'd really love to hear a strong defense of the layout feature. I've never understood what problems it solved that couldn't be solved before.