Navigation Lifecycle using Vaadin Router, LitElement and TypeScript

thisdotmedia_staff

This Dot Media

Posted on September 22, 2020

Navigation Lifecycle using Vaadin Router, LitElement and TypeScript

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.

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>
    `;
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode

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);
      });
    }
  }
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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.
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
  }
Enter fullscreen mode Exit fullscreen mode

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);
      });
    }
  }
Enter fullscreen mode Exit fullscreen mode

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!');
  }
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
thisdotmedia_staff
This Dot Media

Posted on September 22, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Testing Web Components with Cypress and TypeScript
litelementtypescript Testing Web Components with Cypress and TypeScript

November 6, 2020

Routing Management with LitElement and TypeScript
litelementtypescript Routing Management with LitElement and TypeScript

September 9, 2020