Navigation Lifecycle using Vaadin Router and LitElement
Originally published at labs.thisdot.co
Sep 21, 2020 · 6 minutes read · Follow @luixaviles
In a previous post, I explained the main aspects to add a Routing Management with LitElement and TypeScript to build a SPA(Single Page Application). It’s important to note that a good understanding of the routing library you selected to work through can help you to have more control over the navigation lifecycle.
Navigation Lifecycle
Creating and configuring routes into your Web Application is only one part of the Routing Management story.
As you can see, an instance of the class will be created first(using the constructor). Let’s walk around the Lifecycle functions we can use to have full control over navigation using Vaadin Router and LitElement for Web Components.
onBeforeEnter
Let’s suppose we have an Admin page, which is displayed only for authorized users. Our Web Component will look as follows:
import {
PreventAndRedirectCommands,
Router,
RouterLocation,
} from '@vaadin/router';
import { LitElement, html, customElement } from 'lit-element';
@customElement('lit-admin')
export class Admin extends LitElement {
render() {
return html`
<h2>Admin</h2>
<p>Only for authorized users</p>
`;
}
}
However, this page will remain accessible to everyone unless you define a rule to avoid loading it for restricted users.
If you’re working with Vaadin Router and Web Components, you don’t need to implement or extend from any class. It’s enough to define a function name that matches to any lifecycle functions.
So, it’s time to implement a route guard in order to restrict the access to this page and do a redirect instead:
public onBeforeEnter(
location: RouterLocation,
commands: PreventAndRedirectCommands,
router: Router
): RedirectResult | undefined {
if(!this.isAuthorized()) {
return commands.redirect('/'); // Redirects to home page
}
}
private isAuthorized() {
// Logic to determine if the current user can see this page
return false;
}
In this case, the onBeforeEnter function returns a RedirectResult as a router command. That means the router will perform a redirection to /
(the home page) before loading the current path.
If you need to perform any async action in this context, then you can return a Promise instead:
public onBeforeEnter(
location: RouterLocation,
commands: PreventAndRedirectCommands,
router: Router
): Promise<unknown> | undefined {
if (!this.isAuthorized()) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(commands.redirect('/'));
}, 2000);
});
}
}
In the previous example, the router will wait until the Promise is resolved before continuing to load the page.
Find below the complete TypeScript class implementation:
import {
PreventAndRedirectCommands,
RedirectResult,
Router,
RouterLocation,
} from '@vaadin/router';
import { LitElement, html, customElement } from 'lit-element';
@customElement('lit-admin')
export class Admin extends LitElement {
render() {
return html`
<h2>Admin</h2>
<p>Only for authorized users</p>
`;
}
public onBeforeEnter(
location: RouterLocation,
commands: PreventAndRedirectCommands,
router: Router
): Promise<unknown> | RedirectResult | undefined {
console.log('onBeforeEnter');
if (!this.isAuthorized()) {
// sync operation
// return commands.redirect('/');
// async operation
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('Not authorized, redirect to home page');
resolve(commands.redirect('/'));
}, 2000);
});
}
console.log('You can see this page');
}
private isAuthorized() {
// Logic to determine if the current user can see this page
return false;
}
}
onAfterEnter
This function can be used to process the URL params or even initialize the page. Any value you may return will be ignored.
@customElement('lit-admin')
export class Admin extends LitElement {
@property({ type: String }) username!: string;
render() {
return html`
<h2>Admin</h2>
<p>Welcome ${this.username}</p>
<p>Only for authorized users</p>
`;
}
public onAfterEnter(
location: RouterLocation,
commands: PreventAndRedirectCommands,
router: Router
): void {
console.log('onAfterEnter');
// Read params from URL
const section = location.params.section; // path: 'admin/:section'
const username = new URLSearchParams(location.search).get('username');
console.log('section', section);
console.log('username', username);
// Assign the username value from the URL
this.username = username || 'user';
// No need to return a result.
}
}
As you can see, this function will be in charge of all the needed initialization of the page.
As the first step, it will read the parameters from the URL: http://localhost:8000/admin/profile?username=luixaviles
. Then the username
value will be assigned to an existing property.
Here you have the output screen:
onBeforeLeave
This function will be executed once the current path doesn’t match anymore. That means the page or component is about to be removed from the DOM.
public onBeforeLeave(
location: RouterLocation,
commands: PreventAndRedirectCommands,
router: Router
): PreventResult | undefined {
console.log('onBeforeLeave');
const leave = window.confirm('Are you sure to leave this page?');
if (!leave) {
return commands.prevent();
}
}
In this example, the application will show a confirm popup before leave the current page. If the user decides to cancel the action, the function will return a PreventResult
object due to return commands.prevent()
call.
In case you need to perform an async operation, the function can return a Promise as follows:
public onBeforeLeave(
location: RouterLocation,
commands: PreventAndRedirectCommands,
router: Router
): PreventResult | Promise<unknown> | undefined {
console.log('onBeforeLeave');
const leave = window.confirm('Are you sure to leave this page?');
if (!leave) {
// sync operation
// return commands.prevent();
// async operation
return new Promise((resolve, reject) => {
setTimeout(() => {
// console.log('Not authorized, redirect to home page');
console.log('resolved');
resolve(commands.prevent());
}, 2000);
});
}
}
onAfterLeave
This is pretty similar to the previous one and it will be executed once the current path doesn’t match anymore AND the process to remove it from the DOM has been started. Any value returned by this function will be ignored.
public onAfterLeave(
location: RouterLocation,
commands: PreventAndRedirectCommands,
router: Router
): void {
console.log('onAfterLeave');
alert('Just wanted to say goodbye!');
}
As you can see in this screenshot, the URL has been changed already and the function will run as a last action from the previous view.
The Final Component
Pay attention to all imports and types used here:
import {
PreventAndRedirectCommands,
PreventResult,
RedirectResult,
Router,
RouterLocation,
} from '@vaadin/router';
import { LitElement, html, customElement, property } from 'lit-element';
import { router } from '../index';
@customElement('lit-admin')
export class Admin extends LitElement {
@property({ type: String }) username!: string;
render() {
return html`
<h2>Admin</h2>
<p>Welcome ${this.username}</p>
<p>Only for authorized users</p>
<p>Go to <a href="${router.urlForPath('/about')}">About</a></p>
`;
}
public onBeforeEnter(
location: RouterLocation,
commands: PreventAndRedirectCommands,
router: Router
): Promise<unknown> | RedirectResult | undefined {
if (!this.isAuthorized()) {
// sync operation
// return commands.redirect('/');
// async operation
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(commands.redirect('/'));
}, 2000);
});
}
}
public onAfterEnter(
location: RouterLocation,
commands: PreventAndRedirectCommands,
router: Router
): void {
// Read params from URL
const section = location.params.section; // path: 'admin/:section'
const username = new URLSearchParams(location.search).get('username');
console.log('section', section);
console.log('username', username);
// Assign the username value from the URL
this.username = username || 'user';
// No need to return a result.
}
public onBeforeLeave(
location: RouterLocation,
commands: PreventAndRedirectCommands,
router: Router
): PreventResult | Promise<unknown> | undefined {
const leave = window.confirm('Are you sure to leave this page?');
if (!leave) {
// sync operation
// return commands.prevent();
// async operation
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(commands.prevent());
}, 2000);
});
}
}
public onAfterLeave(
location: RouterLocation,
commands: PreventAndRedirectCommands,
router: Router
): void {
alert('Just wanted to say goodbye!');
}
private isAuthorized() {
// Logic to determine if the current user can see this page
return true;
}
}
Source Code Project
If you work through the source code of this project, use this URL for testing the lifecycle callbaks: http://localhost:8000/admin/profile?username=luixaviles.
Find the complete project in this GitHub repository: https://github.com/luixaviles/litelement-website. Do not forget to give it a star ⭐️ and play around with the code.
You can follow me on Twitter and GitHub to see more about my work.