Navigation

Certainly navigation is one of the most common features that we have to implement while creating a web application. In this section, we’ll consider one of the possible ways to do it.

The implementation is based on two components: app-route and iron-lazy-pages.

  • app-route is used to analyze URL currently opened in the browser.

  • iron-lazy-pages manages what page with which content should be opened.

Below is a simple example demonstrating how the result of using these elements might work. It’s put in <iframe/> because we have to change current location in order to show how navigation works.

Navigation example

Code in iframe

app-with-navigation.html
<html>
<head>
  <link rel="import" href="src/recipes/navigation/thermodynamic-laws.html">
  <script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
<thermodynamic-laws></thermodynamic-laws>
</body>
</html>

Polymer element implementing navigation

src/recipes/navigation/thermodynamic-laws.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<!-- app-location listens to changes in current location of the user, including hash -->
<link rel="import" href="../../../bower_components/app-route/app-location.html">
<!-- app-route can analyze the result produced by app-location and determine which page should be opened now -->
<link rel="import" href="../../../bower_components/app-route/app-route.html">
<!-- iron-lazy-pages is a collection of pages. -->
<!-- At any particular moment only one page inside iron-lazy-pages will be shown. -->
<link rel="import" href="../../../bower_components/iron-lazy-pages/iron-lazy-pages.html">

<dom-module id="thermodynamic-laws">
  <template>

    <!-- use-hash-as-path means that this element will analyze only hash, i.e. the text in URL that follows # sign. -->
    <!-- app-location analyzes currently opened URL and stores the result of the analysis in the object property 'route'. -->
    <app-location route="{{route}}" use-hash-as-path></app-location>
    <!-- app-route accepts 'route' produced by app-location -->
    <!--  and using attribute 'pattern' parses it into 'routeData' and 'tail' objects  -->
    <app-route route="[[route]]"
               pattern="/:page"
               data="{{routeData}}"
               tail="{{subRouter}}"></app-route>

    <div>
      <!-- The currently opened URL. Watch how it changes when you click on navigation links. -->
      Current location (click it to open the component in a new tab):
      <br/>
      <a href="[[_location]]" target="_blank">[[_location]]</a>
    </div>

    <h3>Laws of thermodynamics</h3>

    <!-- Navigation block. Each item leads to a different page. -->
    <ul>
      <li><a href="#/0th_law">Zeroth law</a></li>
      <li><a href="#/1st_law">First law</a></li>
      <li><a href="#/2nd_law">Second law</a></li>
      <li><a href="#/3d_law">Third law</a></li>
    </ul>

    <!-- A list of pages -->
    <iron-lazy-pages selected="[[routeData.page]]" attr-for-selected="data-route">

      <!-- Each child block of iron-lazy-pages must have 'data-route' attribute. -->
      <!-- The block will be shown only when opened page matches the value of 'data-route'. -->

      <!-- This particular div will be displayed when a current url hash has '#/0th_law' value. -->
      <div data-route="0th_law">
        If two systems are in thermal equilibrium with a third system, they are in thermal equilibrium with each other.
      </div>

      <div data-route="1st_law">
        When energy passes, as work, as heat, or with matter, into or out from a system, the system's internal energy
        changes in accord with the law of conservation of energy. Equivalently, perpetual motion machines of the first
        kind (machines that produce work without the input of energy) are impossible.
      </div>

      <div data-route="2nd_law">
        In a natural thermodynamic process, the sum of the entropies of the interacting thermodynamic systems increases.
        Equivalently, perpetual motion machines of the second kind (machines that spontaneously convert thermal energy
        into mechanical work) are impossible.
      </div>

      <div data-route="3d_law">
        The entropy of a system approaches a constant value as the temperature approaches absolute zero.
        With the exception of non-crystalline solids (glasses) the entropy of a system at absolute zero
        is typically close to zero, and is equal to the natural logarithm of the product of the quantum ground states.
      </div>

    </iron-lazy-pages>

  </template>
  <script>
    class ThermodynamicLaws extends Polymer.Element {
      static get is() {
        return 'thermodynamic-laws';
      }

      static get properties() {
        return {
          _location: {
            type: String,
            // 'route' is passed to the '_getLocation' method to force it to re-calculate each time 'route' changes
            computed: '_getLocation(route)'
          }
        }
      }

      _getLocation() {
        return window.location.href;
      }
    }

    customElements.define(ThermodynamicLaws.is, ThermodynamicLaws);
  </script>
</dom-module>

Navigation can contain multiple levels. It can be achieved with the help of the same app-route and iron-lazy-pages components. In the example below, click on the "Paper Elements" link and you will be presented with the second level of navigation.

2-level navigation

Source code:

src/recipes/navigation/polymer-elements-registry.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/app-route/app-location.html">
<link rel="import" href="../../../bower_components/app-route/app-route.html">
<link rel="import" href="../../../bower_components/iron-lazy-pages/iron-lazy-pages.html">

<link rel="import" href="paper-elements-registry.html">

<dom-module id="polymer-elements-registry">
  <template>
    <style>
      .catalog-root {
        display: flex;
      }
    </style>

    <app-location route="{{route}}" use-hash-as-path></app-location>
    <app-route route="[[route]]"
               pattern="/:page"
               data="{{routeData}}"
               tail="{{subRouter}}"></app-route>

    <div>
      Current location (click it to open the component in a new tab):
      <br/>
      <a href="[[_location]]" target="_blank">[[_location]]</a>
    </div>

    <h3>Polymer Elements Catalog</h3>

    <div class="catalog-root">
      <ul>
        <li><a href="#/iron">Iron Elements</a></li>
        <li><a href="#/paper">Paper Elements</a></li>
      </ul>
      <iron-lazy-pages selected="[[routeData.page]]" attr-for-selected="data-route">

        <ul data-route="iron">
          <li>app-layout</li>
          <li>app-pouchdb</li>
          <li>app-route</li>
          <li>app-storage</li>
        </ul>

        <!-- We pass an unused part of a hash to the next component with navigation -->
        <paper-elements-registry route="[[subRouter]]" data-route="paper"></paper-elements-registry>

      </iron-lazy-pages>
    </div>

  </template>
  <script>
    class PolymerElementsRegistry extends Polymer.Element {
      static get is() {
        return 'polymer-elements-registry';
      }

      static get properties() {
        return {
          _location: {
            type: String,
            computed: '_getLocation(route)'
          }
        }
      }

      _getLocation() {
        return window.location.href;
      }
    }

    customElements.define(PolymerElementsRegistry.is, PolymerElementsRegistry);
  </script>
</dom-module>
src/recipes/navigation/paper-elements-registry.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/app-route/app-route.html">
<link rel="import" href="../../../bower_components/iron-lazy-pages/iron-lazy-pages.html">

<dom-module id="paper-elements-registry">
  <template>
    <style>
      .paper-elements-root {
        display: flex;
      }
    </style>

    <!-- We don't need app-location here because we got 'route' from a root navigation component -->
    <app-route route="[[route]]"
               pattern="/:page"
               data="{{routeData}}"
               tail="{{subRouter}}"></app-route>

    <div class="paper-elements-root">
      <ul>
        <li><a href="#/paper/input">Input Elements</a></li>
        <li><a href="#/paper/overlay">Overlay Elements</a></li>
        <li><a href="#/paper/ui">Ui Elements</a></li>
      </ul>

      <iron-lazy-pages selected="[[routeData.page]]" attr-for-selected="data-route">

        <ul data-route="input">
          <li>paper-checkbox</li>
          <li>paper-dropdown-menu</li>
          <li>paper-input</li>
          <li>etc.</li>
        </ul>

        <ul data-route="overlay">
          <li>paper-dialog</li>
          <li>paper-dialog-behavior</li>
          <li>paper-dialog-scrollable</li>
          <li>paper-fab</li>
        </ul>

        <ul data-route="ui">
          <li>paper-badge</li>
          <li>paper-button</li>
          <li>paper-card</li>
          <li>etc.</li>
        </ul>

      </iron-lazy-pages>
    </div>

  </template>
  <script>
    class PaperElementsRegistry extends Polymer.Element {
      static get is() {
        return 'paper-elements-registry';
      }
    }

    customElements.define(PaperElementsRegistry.is, PaperElementsRegistry);
  </script>
</dom-module>

The same method can be used to create any navigation tree.