Skip to content

Commit

Permalink
refactor(router-lite): transition plan
Browse files Browse the repository at this point in the history
  • Loading branch information
Sayan751 committed Dec 11, 2022
1 parent 4690cfa commit 186da90
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -384,17 +384,15 @@ Any attempt with a different casing is navigated to the `fallback`.

{% embed url="https://stackblitz.com/edit/router-lite-case-sensitive?ctl=1&embed=1&file=src/my-app.ts" %}

## Route configuration options
## Advanced route configuration options

There are few other routing configuration which aren't discussed above.
To understand these options fully, knowledge of other parts of the route would be beneficial.
Our assumption is that these options are more involved and might not be used that often.
Moreover, to understand the utility of these options fully, knowledge of other parts of the route would be beneficial.
Therefore, although this section briefly describes these options, it also provides links to the sections, describing these topics with detailed examples.

* `id` — The unique ID for this route. The router-lite implicitly generates a `id` for a given route, if an explicit value for this property is missing. This can be used to generate the `href`s in the view when using the [`load` custom attribute](TODO(Sayan): link to doc) or using the [`Router#load` API](TODO(Sayan): link to doc). Using this property is also very convenient when there are multiple aliases for a single route, and we need a unique way to refer to this route.
* `transitionPlan` — How to behave when this component is scheduled to be loaded again in the same viewport. Valid values for transitionPlan are:
* `replace` — completely removes the current component and creates a new one, behaving as if the component changed.
* `invoke-lifecycles` — calls `canUnload`, `canLoad`, `unload` and `load` (default if only the parameters have changed).
* `none` — does nothing (default if nothing has changed for the viewport).
* `id` — The unique ID for this route. The router-lite implicitly generates a `id` for a given route, if an explicit value for this property is missing. Although this is not really an advanced property, the fact that a route can be uniquely identified using this `id`, it can be used in many interested ways. For example, this can be used to generate the `href`s in the view when using the [`load` custom attribute](TODO(Sayan): link to doc) or using the [`Router#load` API](TODO(Sayan): link to doc). Using this property is also very convenient when there are multiple aliases for a single route, and we need a unique way to refer to this route.
* `transitionPlan` — How to behave when the currently active component is scheduled to be loaded again in the same viewport. For more details, please refer the [documentation](./transition-plans.md).
* `viewport` — The name of the viewport this component should be loaded into. This demands a full fledged documentation of its own. Refer to the [viewport documentation](./viewports.md) for more details.
* `data` — Any custom data that should be accessible to matched components or hooks. The value of this configuration property must be an object and the object can take any shape (that is there is no pre-defined interface/class for this object). A typical use-case for the `data` property is to define the permissions, required by the users, when they attempt to navigate to this route. Refer [an example](./router-hooks.md#example-for-handling-authorization) of this.
* `nav` - Set this flag to `false` (default value is `true`), to instruct the router not to add the route to the [navigation model](./navigation-model.md). This is typically useful to [exclude routes](./navigation-model.md#excluding-routes-from-the-navigation-model) from the public navigation menu.
Expand Down
189 changes: 189 additions & 0 deletions docs/user-docs/developer-guides/router-lite/transition-plans.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
---
description: Learn how Router-Lite handles the re-entrance of the same component and how to override the default behavior.
---

# Transition plan

The transition plan in router-lite is meant for deciding how to process a navigation instruction that intend to load the same component that is currently loaded/active.
Based on user-voice the router-lite uses a sensible default, and probably you never need to touch this area.
However, it is still good to know how to change those defaults, whenever you are in need (and we all know that such needs will arise from time to time).

Transition plan can be configured using the `transitionPlan` property in the [routing configuration](./configuring-routes.md#advanced-route-configuration-options).
The allowed values are `replace`, `invoke-lifecycles`, `none` or a function that returns one of these values.

- `replace`: This instructs the router to completely remove the current component and create a new one, behaving as if the component is changed. This is the default behavior if the parameters are changed.
- `invoke-lifecycles`: This instructs the router to call the lifecycle hooks (`canUnload`, `canLoad`, `unloading` and `loading`) of the component.
- `none`: Does nothing. This is the default behavior, when nothing is changed.

## How does it work

The child routes inherits the `transitionPlan` from the parent.

When the `transitionPlan` property in the [routing configuration](./configuring-routes.md#advanced-route-configuration-options) is not configured, router-lite uses a function as sensible default to select the transition plan when the current component is attempted to be loaded again.
The default behavior selects `replace` when the parameter changes and `none` otherwise.

{% hint style="info" %}
It might be normal to think that the default selection the `replace` transition plan when the parameter changes, to be an overkill and the default selection should have been `invoke-lifecycles` instead.
As a matter of fact that's the default option in Aurelia1 as well as in earlier versions of Aurelia2.
However, as understood from the user-voices that `replace` would in this case cause less surprises.
Hence the default behavior is changed to `replace`.
{% endhint %}

## Transition plans are inherited

Transition plans defined on the root are inherited by the children.
The example below shows that the `transitionPlan` on the root is configured to `replace` and this transition plan is inherited by the child route configuration.
This means that every time the link is clicked, the component is created new and the view reflects that as well.

```typescript
import { customElement } from '@aurelia/runtime-html';
import { IRouteViewModel, route } from '@aurelia/router-lite';

@customElement({ name: 'ce-one', template: 'ce1 ${id1} ${id2}' })
class CeOne implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
// Every instance gets a new id.
private readonly id1: number = ++CeOne.id1;
private id2: number;
public canLoad(): boolean {
// Every time the lifecycle hook is called, a new id is generated.
this.id2 = ++CeOne.id2;
return true;
}
}

@route({
transitionPlan: 'replace',
routes: [
{
id: 'ce1',
path: ['', 'ce1'],
component: CeOne,
},
],
})
@customElement({
name: 'my-app',
template: `<a load="ce1">ce-one</a><br><au-viewport></au-viewport>`,
})
export class MyApp {}
```

See this example in action below.

{% embed url="https://stackblitz.com/edit/router-lite-tr-plan-replace-inheritance?ctl=1&embed=1&file=src/my-app.ts" %}

## Use a function to dynamically select transition plan

You can use a function to dynamically select transition plan based on route nodes.
The following example shows just that where for every components, apart from the root component, `invoke-lifecycles` transition plan is selected.

```typescript
import { customElement } from '@aurelia/runtime-html';
import { IRouteViewModel, route } from '@aurelia/router-lite';

@customElement({ name: 'ce-one', template: 'ce1 ${id1} ${id2}' })
class CeOne implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
// Every instance gets a new id.
private readonly id1: number = ++CeOne.id1;
private id2: number;
public canLoad(): boolean {
// Every time the lifecycle hook is called, a new id is generated.
this.id2 = ++CeOne.id2;
return true;
}
}

@route({
transitionPlan(_current: RouteNode, next: RouteNode) {
return next.component.Type === MyApp ? 'replace' : 'invoke-lifecycles';
},
routes: [
{
id: 'ce1',
path: ['', 'ce1'],
component: CeOne,
},
],
})
@customElement({
name: 'my-app',
template: `<a load="ce1">ce-one</a><br><au-viewport></au-viewport>`,
})
export class MyApp {}
```

The behavior can be validated by clicking the link multiple times and observing that the `CeOne#id2` increases, whereas `CeOne#id1` remains constant.
This shows that every attempt to load the `CeOne` only invokes the lifecycle hooks without re-instantiating the component every time.
You can try out this example below.

{% embed url="https://stackblitz.com/edit/router-lite-tr-plan-function?ctl=1&embed=1&file=src/my-app.ts" %}

This can be interesting when dealing with [sibling viewports](./viewports.md#sibling-viewports), as you can select different transition plan for different siblings.

```typescript
import { customElement } from '@aurelia/runtime-html';
import { IRouteViewModel, route, RouteNode } from '@aurelia/router-lite';

@customElement({ name: 'ce-two', template: 'ce2 ${id1} ${id2}' })
class CeTwo implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeTwo.id1;
private id2: number;
public canLoad(): boolean {
this.id2 = ++CeTwo.id2;
return true;
}
}

@customElement({ name: 'ce-one', template: 'ce1 ${id1} ${id2}' })
class CeOne implements IRouteViewModel {
private static id1: number = 0;
private static id2: number = 0;
private readonly id1: number = ++CeOne.id1;
private id2: number;
public canLoad(): boolean {
this.id2 = ++CeOne.id2;
return true;
}
}

@route({
transitionPlan(current: RouteNode, next: RouteNode) {
return next.component.Type === CeTwo ? 'invoke-lifecycles' : 'replace';
},
routes: [
{
id: 'ce1',
path: ['ce1'],
component: CeOne,
},
{
id: 'ce2',
path: ['ce2'],
component: CeTwo,
},
],
})
@customElement({
name: 'ro-ot',
template: `
<a load="ce1@$1+ce2@$2">ce1@$1+ce2@$2</a>
<div id="content">
<au-viewport name="$1"></au-viewport>
<au-viewport name="$2"></au-viewport>
</div>
`,
})
export class MyApp {}
```

The example above selects `invoke-lifecycles` for the `CeTwo` and `replace` for everything else.
When you click the link multiple times, you can see that `CeOne` is re-instantiated every time whereas for `CeTwo` only the lifecycles hooks are invoked and the instance is reused.
You can see the example in action below.

{% embed url="https://stackblitz.com/edit/router-lite-tr-plan-function-sibling?ctl=1&embed=1&file=src/my-app.ts" %}
8 changes: 3 additions & 5 deletions packages/router-lite/src/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,9 @@ export interface IRouteConfig {
/**
* How to behave when this component scheduled to be loaded again in the same viewport:
*
* - `replace`: completely removes the current component and creates a new one, behaving as if the component changed.
* - `invoke-lifecycles`: calls `canUnload`, `canLoad`, `unloading` and `loading` (default if only the parameters have changed)
* - `none`: does nothing (default if nothing has changed for the viewport)
*
* By default, calls the router lifecycle hooks only if the parameters have changed, otherwise does nothing.
* - `replace`: completely removes the current component and creates a new one, behaving as if the component changed (default if only the parameters have changed).
* - `invoke-lifecycles`: calls `canUnload`, `canLoad`, `unloading` and `loading`.
* - `none`: does nothing (default if nothing has changed for the viewport).
*/
readonly transitionPlan?: TransitionPlanOrFunc | null;
/**
Expand Down

0 comments on commit 186da90

Please sign in to comment.