Skip to content

Navigation Lifecycle using Vaadin Router, LitElement and TypeScript

Navigation Lifecycle using Vaadin Router, LitElement and TypeScript

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

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.

Creating and configuring routes into your Web Application is only one part of the Routing Management story.

lifecycle-callbacks

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.

01-initial-screenshot

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:

02-on-after-enter

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();
    }
  }
03-on-before-leave

In this example, the application will show a confirm popup before leaving 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!');
  }
04-on-after-leave

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 callbacks: 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.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co