Navigation Lifecycle using Vaadin Router and LitElement

Originally published at labs.thisdot.co

Sep 21, 2020 · 6 minutes read · [TypeScript] [WebComponents] [LitElement]

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.

LitElement - 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.

LitElement - onBeforeEnter callback example

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:

LitElement - onAfterEnter callback example

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();
    }
  }
LitElement - onBeforeLeave callback example

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!');
  }
LitElement - onAfterLeave callback example

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.

tweet Share