Preface

This tutorial will teach you how to use CUBA Polymer UI to develop front-end applications working with CUBA middleware. It’s designed primarily for developers who are not familiar with the Web Components technology and Google Polymer library, but have good knowledge of HTML, JavaScript and CSS.

The tutorial is divided into three chapters:

  • Polymer Core: an introduction to Polymer which contains everything that you should know to start building front-end applications using this technology.

  • CUBA Web Components: our Polymer components that facilitate creating applications based on the CUBA platform.

  • Common Tasks: a collection of our recipes to common problems a developer has to solve while creating web applications. For example: how to build a navigation block, implement dependency injection and so on.

The manual contains a lot of examples. For almost every topic we provide:

  • Commented source code of web components illustrating a subject;

  • index.html that represents the application entry point;

  • A result of executing this code.

All examples can be copy-pasted into your CUBA project in order to examine them closely. The simplest way to do it is to create a new project in CUBA Studio and then create a Polymer UI module using the Project Properties → Manage modules → Create polymer client module command. After that, deploy the application and run the server. You will have the deploy/tomcat/webapps/app-front folder with your front-end application, where you can copy our examples as is and just reload a web page to see them in action.

Polymer Core

Google Polymer is a JavaScript library that allows you to create reusable Web Components. CUBA Polymer UI module is built on it, so it’s important to understand basics of this framework.

Google Polymer has an official development guide. However, to facilitate the learning process we created our own quick manual that accents the most important features of the library and provides a number of examples for better understanding of how various features can be used.

The Polymer library currently has two major versions: 1.0 and 2.0, with very different syntax. All our examples use Polymer 2.0.

Polymer Core Basics

Polymer components are stored in HTML files. Each file contains one component, which consists of a template (HTML), styles (CSS) and logic (JavaScript).

Polymer components can import and use other Polymer components.

Simple Component

Let’s consider a very simple example. The project consists of two files: index.html and alert-button.html.

  • alert-button.html defines a Polymer component.

  • index.html uses this component.

See the source code and the result below.

Tip

You can play with the example in an application created and deployed by Studio by copying the below files to your deploy/tomcat/webapps/app-front folder keeping the relative paths. That is index.html should replace the existing file in the web application root, and alert-button.html must be created in the new src/polymer-basic/simple-component subdirectory.

index.html
<html>
<!-- index.html is an entry point of our application. -->
<!-- Usually it loads one root Polymer element which contains all other components. -->
<head>
    <!-- Import of a web component we want to use. -->
	<link rel="import" href="src/polymer-basic/simple-component/alert-button.html">
	<!-- Polyfills. -->
    <!-- Natively web-components work only in Google Chrome. -->
    <!-- For all other browsers polyfills are required. -->
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
  <alert-button>
    <!-- This text goes to the <slot/> element of the Polymer component. -->
    Our first simple component - alert button!
  </alert-button>
</body>
</html>
src/polymer-basic/simple-component/alert-button.html
<!-- polymer-element.html contains a base class for all our custom web components -->
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">

<dom-module id="alert-button">
  <template>
    <style>
      /* CSS put here is applicable only to this particular component. */
      /* Other HTML in the application won't be affected by it. */
      /* That is, if some other Polymer element will use .cuba-btn class, this CSS block won't affect it. */
      .cuba-btn {
        background: #006ba9;
        border: 1px solid #006ba9;
        border-radius: 5%;
        color: #ffffff;
        padding: 5px 10px;
      }
    </style>

    <h3>
      <!-- Here goes any HTML passed to Polymer component -->
      <slot></slot>
    </h3>

    <!-- onClick handler will call a function declared inside our Polymer element -->
    <button class="cuba-btn" on-click="greetUser">Push me please!</button>

  </template>
  <script>
    // Our web component has to extend Polymer.Element class
    class AlertButton extends Polymer.Element {
      // Return value of 'is' has to match the value specified in dom-module[id]
      static get is() {
        return 'alert-button';
      }

      // We can declare functions here and call them from handlers in HTML
      greetUser() {
        alert('Hello, User!');
      }
    }

    // customElements is a global object used to register all our custom web components
    customElements.define(AlertButton.is, AlertButton);
  </script>
</dom-module>

Result

Our first simple component - alert button!

So, alert-button is a component that is represented by a button and an optional caption. On click, the button shows the "Hello, User!" message. The code using this component provides a content in the <alert-button/> tag. This content is displayed by the slot element inside the component. However, the slot element is not required and can be omitted.

These are the basics that allow you to write and use simple Polymer components.

Tip

index.html in our example contains a polyfill script. This script checks what exactly our browser doesn’t support (HTML imports, shadow DOM, custom elements) and loads only polyfills that are really required. See details here.

What we have learned so far
  • Polymer components are declared in HTML files inside the dom-module tag.

  • Each Polymer component file can contain CSS (optional), HTML (optional) and JavaScript (mandatory).

  • A Polymer component is declared by creating a class that extends Polymer.Element and registering it with customElements object. Our web component must contain the is static property which has to match the id of the dom-module element. This id is used to refer to the component afterwards.

  • Polymer component class can contain an arbitrary number of functions that can be called in handlers from HTML.

  • CSS declared in Polymer components don’t affect the rest of the application.

  • Polymer components can import and use other Polymer components.

Properties

Properties are used for data binding, which is an essential part of any modern web framework.

Let’s consider a component that demonstrates binding abilities of Polymer. It is an input where users can type something, and all they are typing is duplicated below by using property binding.

Guesser component

Source code

index.html
<html>
<head>
	<link rel="import" href="src/polymer-basic/properties/name-guesser.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <name-guesser placeholder="Your name goes here"></name-guesser>
</body>
</html>
src/polymer-basic/properties/name-guesser.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<!-- iron-input is a Polymer element from standard Polymer library. -->
<!-- It enhances standard HTML input by providing an ability of two-way binding. -->
<link rel="import" href="../../../bower_components/iron-input/iron-input.html">

<dom-module id="name-guesser">
  <template>

    <h4>
      Please enter your name:
    </h4>
    <!-- iron-input works as a wrapper for a simple HTML input -->
    <iron-input bind-value="{{name}}">
      <input placeholder="[[placeholder]]" />
    </iron-input>
    <br/>
    <br/>
    <div>
      Your name is: [[name]]
    </div>

  </template>

  <script>
    class NameGuesser extends Polymer.Element {
      static get is() {
        return 'name-guesser';
      }

      static get properties() {
        // Properties configuration object contains a map of properties.
        // Key is a name of a property, value is a configuration.
        return {
          name: String,
          placeholder: {
            type: String,
            value: 'Your name please'
          }
        }
      }
    }

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

There are two types of binding:

  • [[ ]] - one-way binding. An enclosed component listens to changes of a property and refreshes its state. In other words, the data flows downward - from the host component to children components.

    In our example, the placeholder property uses one-way binding: we just pass its value down to the input component.

  • {{ }} - two-way binding. An enclosed component not only listens to property changes but can also change the property value itself. That is the data can flow both ways - from host to children and back.

    In our example, the name property uses two-way binding to receive a value from the iron-input component and display it in div.

In this example, we could use two-way binding for all properties and the component would still work as expected. But it’s important to choose a correct binding, because it increases code readability and simplifies refactoring. Always prefer one-way bindings and use two-way bindings only when necessary.

A Polymer component can configure all its properties in an object returned by the properties getter. It can just specify a type of a property (String, Boolean, Number, Object, Array, Date) or provide a number of different parameters. One of these parameters is value which specifies a default value for the property.

In our example, the placeholder property has default value "Your name please". But it’s overwritten with "Your name goes here" passed from the host name-guesser element defined in index.html. We could also provide a name, e.g.

<name-guesser name="Charlie"></name-guesser>

That would cause the input to be filled on initialization:

Apart from value, a property can have a number of other attributes (observer, notify, etc.) that partially will be reviewed further.

Pay attention to the following important detail: properties are named in CamelCase in JavaScript but in kebab-case in HTML. For example, the bind-value property of the iron-input component is defined in the source code of the component as follows:

...
properties: {

  /**
   * Use this property instead of `value` for two-way data binding.
   */
  bindValue: {
    type: String
  },
  ...

Also, there are some native properties like class that do not support property binding. You should use attribute bindings instead to interact with such properties.

What we have learned so far
  • Use [[ ]] for one-way binding.

  • Use {{ }} for two-way binding.

  • Describe your properties in an object returned by the properties getter.

  • For each property you can define type, default value and a number of other parameters.

  • You can provide properties from outside of a Polymer component by using HTML attributes.

  • iron-input is a Polymer component that allows you to use two-way binding in input element.

Changing Properties

Another interesting detail is how to make changes in properties correctly.

If the property is simple value you can use assignment, for example:

this.someStringProperty = 'value'; // works

If you deal with properties which are objects or arrays and mutate them (that is change their properties or add new elements to array) you should use special API in order to notify Polymer data system about the changes.

Example of changing an object:

this.user.name = 'John'; // does not work

this.set('user.name', 'value'); // works

Example of changing an array:

this.users.push({name: 'John'}); // does not work

this.push('users', {name: 'John'}); // works

Also, if changes are out of your control or you want to trigger property effects for a batch of changes there are notifyPath and notifySplices methods:

this.user.name = 'John';
this.user.surname = 'Smith';
this.notifyPath('user.*');

If in some cases you use "=", then a further refactoring can easily break the correct behavior by mixing up the rules. So there are two patterns to eliminate the problem:

  • avoid mutation of complex properties (i.e. using immutable data patterns) - each time you need to change complex object you should replace the whole object;

  • always use set() instead of a simple value assignment.

This will guarantee that you won’t forget the rules and won’t break anything later because of refactoring.

What we have learned so far
  • Changes in sub-properties are not propagated automatically.

  • Component properties should be mutated by using a set of Polymer methods.

Templating

Polymer provides convenient means for templating: a conditional template (dom-if) and a template repeater (dom-repeat).

The following example demonstrates these features. It’s a component named name-list, which allows a user to create a list of names. It provides an input where the user can enter some name and confirm it by pushing a confirm button. The confirmed name will be added to the list and displayed below.

To prevent a user from entering too many names there is a property maxNameLength. When a list size reaches some limit (3, by default) the input disappears.

Result

Source code

index.html
<html>
<head>
	<link rel="import" href="src/polymer-basic/templating/name-list.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <name-list></name-list>
</body>
</html>
src/polymer-basic/templating/name-list.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-repeat.html">
<link rel="import" href="../../../bower_components/iron-input/iron-input.html">

<dom-module id="name-list">
  <template>

    <!-- dom-if: conditional output. -->
    <!-- Polymer observes changes in the 'addingNameAvailable' property -->
    <!-- specified in the 'if' attribute and shows the element's content depending on it. -->
    <template is="dom-if" if="[[addingNameAvailable]]">
      <iron-input bind-value="{{newName}}">
        <input />
      </iron-input>
      <button on-click="addName">Add name</button>
      <br/>
    </template>

    <!-- dom-repeat: repeated output. -->
    <template is="dom-repeat" items="[[names]]">
      <br/>
      <!-- 'item' is an element of the 'names' array -->
      <div>[[item]]</div>
    </template>

  </template>

  <script>
    class NameList extends Polymer.Element {
      static get is() {
        return 'name-list';
      }

      static get properties() {
        return {
          names: {
            type: Array,
            // Value can be specified not only by literal but also by a function.
            // For properties of type Array or Object it's better to specify a function because
            //  otherwise the value can be shared between different instances of this Polymer element.
            value: function() {
              return [];
            }
          },
          newName: String,
          addingNameAvailable: {
            type: Boolean,
            value: true
          },
          // Nothing prevents us to change this value when we use this web-component.
          // E.g. <name-list max-name-length="5"></name-list> will do the trick.
          maxNameLength: {
            type: Number,
            value: 3
          }
        }
      }

      addName() {
        // We should change properties only by using Polymer mutator methods.
        // If we just use "this.names.push(this.name)" then the component won't be notified s
        // that the property change and UI won't be updated.
        // "push" method is used to add an element to an array.
        this.push('names', this.newName);
        // The same principle applies here.
        // If we just use "this.newName = ''" the component won't know
        //  that something has changed.
        this.set('newName', '');

        if (this.names.length >= this.maxNameLength) {
          this.set('addingNameAvailable', false);
        }
      }
    }

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

This example shows how to work with dom-if and dom-repeat templates.

What we have learned so far
  • dom-if template can be used when some content should be shown/hidden on some condition.

  • dom-repeat template can be used to display an array of elements.

  • The default values of object properties have to be specified with functions to avoid sharing a state between components.

Events Firing and Handling

Often child components must notify parent ones that something happened: a button was pushed, a form was confirmed, etc. In Polymer, such notification can be implemented using the standard observer pattern. A child component sends some event and a parent listens to it.

Let’s consider the following example: there is a simple form consisting of an input and a button. Users enter their name and the form notifies the parent component that the form was confirmed and passes the entered name to the parent.

The form

Source code

index.html
<html>
<head>
	<link rel="import" href="src/polymer-basic/events/event-manager.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <event-manager></event-manager>
</body>
</html>
src/polymer-basic/events/event-manager.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="participation-form.html">

<dom-module id="event-manager">
  <template>

    <!-- Here we expect that the component can send an event called 'form-submit'. -->
    <!-- So, the handler attribute is called 'on-form-submit'. -->
    <participation-form on-form-submit="formSubmitted"></participation-form>

  </template>
  <script>
    class EventManager extends Polymer.Element {
      static get is() {
        return 'event-manager';
      }

      formSubmitted(e) {
        // e.detail contains all properties that event sender has provided
        alert(e.detail.name + ', your name has been submitted!');
      }
    }

    customElements.define(EventManager.is, EventManager);
  </script>
</dom-module>
src/polymer-basic/events/participation-form.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/iron-input/iron-input.html">

<dom-module id="participation-form">
  <template>

    <iron-input bind-value="{{name}}">
      <input placeholder="Your name" />
    </iron-input>

    <!-- This is another example of how events work in Polymer. -->
    <!-- We just specify a handler in 'on-{event}' attribute. -->
    <button on-click="submitForm">Submit form!</button>

  </template>
  <script>
    class ParticipationForm extends Polymer.Element {
      static get is() {
        return 'participation-form';
      }

      static get properties() {
        return {
          name: String
        }
      }

      submitForm() {
        // Fire a custom event and pass a custom 'name' parameter in it.
        // Our parameter object has to be put inside the 'detail' property of a constructor argument.
        this.dispatchEvent(new CustomEvent('form-submit', { detail: { name: this.name } }));
      }
    }

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

If you have some experience with JavaScript events, you probably noticed that we didn’t put e.stopPropagation() expression into formSubmitted(e) method of the EventManager component. The reason we don’t stop propagation is that there is no propagation since CustomEvents by default do not cross shadow DOM boundaries.

For example, there are Component1, Component2 and Component3. Component1 contains Component2. Component2 contains Component3. Component3 sends some event. In this case Component2 will receive this event and Component1 won’t. This behavior is convenient in most cases but can be changed by using composed property. See more details in the official guide.

What we have learned so far
  • The dispatchEvent(event) method is used to send events. To create an event, we can use CustomEvent constructor which accepts as parameters the event name (mandatory) and a settings object (optional). We can put our custom parameters into the detail property of the settings object.

  • on-{eventName} attributes are used to listen to events.

  • Event parameters can be retrieved using the detail property of an event.

Polymer Elements Library

Polymer provides a large set of standard components that are grouped in collections: iron-elements, paper-elements, app-elements, gold-elements, etc.

The first two collections are the most basic and commonly used:

  • Iron elements provide some very basic components that are required for almost every project: input, label, etc.

  • Paper elements provide a set of UI components implementing material design: input, checkbox, slider, etc.

You already saw iron-input in our previous examples. Let’s check one of the paper elements: paper-checkbox. It is a flat design implementation of a simple checkbox. Below is a simple application that uses this element.

Music taste analyzer

It’s a component that can analyze person’s music preferences and draw a mental portrait based on it. Let’s check how it works under the hood.

Source code

index.html
<html>
<head>
	<link rel="import" href="src/polymer-basic/library/music-survey.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <music-survey></music-survey>
</body>
</html>
src/polymer-basic/library/music-survey.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-repeat.html">
<link rel="import" href="../../../bower_components/paper-checkbox/paper-checkbox.html">

<dom-module id="music-survey">
  <template>

    <div>
      Select music genres you like and we will tell you about your character.
    </div>
    <br/>
    <div>
      <template is="dom-repeat" items="[[genres]]">
        <paper-checkbox class="genre-checkbox">[[item]]</paper-checkbox>
      </template>

    </div>
    <br/>
    <div>
      <button on-click="analyze">Submit</button>
    </div>

  </template>
  <script>
    class MusicSurvey extends Polymer.Element {
      static get is() {
        return 'music-survey';
      }

      static get properties() {
        return {
          genres: {
            type: Array,
            value: function() {
              return ['Blues', 'Classical', 'Funk', 'Jazz', 'Rap', 'Rock'];
            }
          }
        }
      }

      analyze() {
        const selectedGenres = [];

        // An example of how we can find HTML elements matching some selector
        Polymer.dom(this.root).querySelectorAll('.genre-checkbox').forEach(function(checkbox) {
          if (checkbox.checked) {
            // selectedGenres is a local variable.
            // So, nothing prevents us from using a standard 'push' method instead of
            //  some Polymer special methods.
            selectedGenres.push(checkbox.textContent.trim());
          }
        });

        const genreAnalyze = selectedGenres.length === 0
          ? 'You don\'t seem to like any particular genre. '
          : 'You like ' + selectedGenres.join(', ') + '. ';

        alert(genreAnalyze + 'Probably you are a good and kind person.');
      }
    }

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

To learn more about the standard library of Polymer components visit https://www.webcomponents.org/collection/Polymer/elements

What we have learned so far
  • Polymer offers a number of ready-to-use components.

  • Iron elements are the most basic components from the standard library.

  • Paper elements collection provides a list of UI components implementing material design.

  • We can use Polymer.dom(this.root).querySelectorAll("some-selector-there") to find elements in our component.

Polymer Core Advanced

Despite the name of this section, techniques considered here are required to build almost any real web application that consists of more than a couple of components.

Accessing DOM Elements

The simplest and the most straightforward method to access an HTML element from JS code is to use id-s. Consider an example below.

Click on the button to see what it’s doing:

Source code

index.html
<html>
<head>
	<link rel="import" href="src/polymer-advanced/dom/colored-square-controller.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <colored-square-controller></colored-square-controller>
</body>
</html>
src/polymer-advanced/dom/colored-square-controller.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="colored-square.html">

<dom-module id="colored-square-controller">
  <template>

    <!-- We provided an id for this element -->
    <colored-square id="square"></colored-square>
    <br/>
    <button on-click="changeColor">Change color</button>

  </template>
  <script>
    class ColoredSquareController extends Polymer.Element {
      static get is() {
        return 'colored-square-controller';
      }

      changeColor() {
        // Accessing an HTML element by id
        this.$.square.changeColor();
      }
    }

    customElements.define(ColoredSquareController.is, ColoredSquareController);
  </script>
</dom-module>
src/polymer-advanced/dom/colored-square.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">

<dom-module id="colored-square">
  <template>
    <style>
      .square {
        background: white;
        border: solid 1px black;
        height: 20px;
        width: 20px;
      }

      .square[black] {
        background: black;
        border: solid 1px red;
      }
    </style>

    <!-- black$ instead of black is used in order to make it possible to use this attribute in CSS -->
    <div class="square" black$="[[black]]"></div>

  </template>
  <script>
    class ColoredSquare extends Polymer.Element {
      static get is() {
        return 'colored-square';
      }

      static get properties() {
        return {
          black: {
            type: Boolean,
            value: false
          }
        }
      }

      changeColor() {
        this.black = !this.black;
      }
    }

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

As you can see, Polymer has special shortcut — this.$, which can be used to retrieve an element by its id. In found element, you can change properties and call methods.

Tip

Please note that we used attribute black$ instead of black. What’s the difference?

If we used just black, then during the component initialization Polymer would or would not put black attribute in DOM on div based on if black property is true or false. And if the black property changed later Polymer wouldn’t add/remove this attribute from the div. Therefore, it would be impossible to use this attribute in CSS (.square[black]). But when we use black$, Polymer automatically updates HTML attribute black based on changes in the black property.

That is if in our example we used black instead of black$, then the square would always be white, no matter how much we press the button.

However, the this.$ syntax won’t always work. Let’s put the colored-square component inside an if-template. In this case this.$.square won’t work even if the condition is true. See more in the official guide.

src/polymer-advanced/dom/colored-square-controller-with-if.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<link rel="import" href="colored-square.html">

<dom-module id="colored-square-controller-with-if">
  <template>

    <template is="dom-if" if="[[someCondition]]">
      <colored-square id="square"></colored-square>
    </template>

    <br/>

    <button on-click="changeColor">Change color</button>

  </template>
  <script>
    class ColoredSquareControllerWithIf extends Polymer.Element {
      static get is() {
        return 'colored-square-controller-with-if';
      }

      static get properties() {
        return {
          someCondition: {
            type: Boolean,
            value: true
          }
        }
      }

      changeColor() {
        // this row is commented because it won't work anyway
        // this.$.square.changeColor();
      }
    }

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

In such cases you can use native DOM API to find a required element: this.shadowRoot.querySelector("selector"). This method will search for the element dynamically.

src/polymer-advanced/dom/colored-square-controller-with-if-fixed.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<link rel="import" href="colored-square.html">

<dom-module id="colored-square-controller-with-if-fixed">
  <template>

    <template is="dom-if" if="[[someCondition]]">
      <colored-square id="square"></colored-square>
    </template>

    <br/>

    <button on-click="changeColor">Change color</button>

  </template>
  <script>
    class ColoredSquareControllerWithIfFixed extends Polymer.Element {
      static get is() {
        return 'colored-square-controller-with-if-fixed';
      }

      static get properties() {
        return {
          someCondition: {
            type: Boolean,
            value: true
          }
        }
      }

      changeColor() {
        // Dynamically search for the element
        this.shadowRoot.querySelector('#square').changeColor();
      }
    }

    customElements.define(ColoredSquareControllerWithIfFixed.is, ColoredSquareControllerWithIfFixed);
  </script>
</dom-module>
What we have learned so far
  • this.$.{id} can be used to retrieve an element by id. However, it won’t work if this element is added/removed dynamically from DOM.

  • Native DOM API this.shadowRoot.querySelector("{selector}") can be used to find an element by CSS selector.

  • We can change properties on the found element and call its methods.

  • Use special syntax ("black$=") if you want to change attribute (not property). Mostly, it’s required when we want to use these attributes in our CSS.

Computed Properties

Sometimes you may need properties that depend on other properties. For example, you have properties firstName, lastName and also need property fullName that concatenates first and last names. Or you have a boolean property that defines if a button is enabled or disabled, and this property depends on a number of other properties.

Obviously, you shouldn’t manually change a value for such synthetic properties, it should be calculated automatically. Luckily, Polymer provides so called computed properties, and below is an example of using them.

Source code

index.html
<html>
<head>
	<link rel="import" href="src/polymer-advanced/computed-properties/service-agreement.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <service-agreement></service-agreement>
</body>
</html>
src/polymer-advanced/computed-properties/service-agreement.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
<link rel="import" href="../../../bower_components/paper-checkbox/paper-checkbox.html">

<dom-module id="service-agreement">
  <template>

    <template is="dom-if" if="[[!applicationConfirmed]]">
      <div>
        <label>
          Please enter your name and accept our agreement before continuing.
        </label>
      </div>
      <br/>
      <div>
        <iron-input bind-value="{{name}}">
          <input />
        </iron-input>
      </div>
      <br/>
      <div>
        <paper-checkbox checked="{{agreementConfirmed}}">I have read and accepted this agreement</paper-checkbox>
      </div>
      <br/>
      <div>
        <!-- continueEnabled is a computed property -->
        <!--  but it's used as any other property would be used -->
        <button disabled="[[!continueEnabled]]" on-click="continuePurchase">Continue</button>
      </div>
    </template>
    <!-- After a user enters all details and confirms a form we show him this confirmation message -->
    <template is="dom-if" if="[[applicationConfirmed]]">
      <h3>Thank you for your collaboration! We will analyze your application and contact you in 24 hours.</h3>
    </template>

  </template>
  <script>
    class ServiceAgreement extends Polymer.Element {
      static get is() {
        return 'service-agreement';
      }

      static get properties() {
        return {
          name: {
            type: String,
            value: ''
          },
          agreementConfirmed: {
            type: Boolean,
            value: false
          },
          // It's a computed property
          continueEnabled: {
            type: Boolean,
            // In 'computed' property we provide a name of a method to calculate a value
            //  and pass in it all properties this property depends on.
            // The property is re-calculated each time the properties it depends on change.
            computed: 'isContinueEnabled(name, agreementConfirmed)'
          },
          applicationConfirmed: {
            type: Boolean,
            value: false
          }
        }
      }

      isContinueEnabled(name, agreementConfirmed) {
        return name.length > 0 && agreementConfirmed;
      }

      continuePurchase() {
        this.set('applicationConfirmed', true);
      }
    }

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

Result

In this example, users cannot submit a form until they type in their name and accept an agreement.

It’s essential to specify what properties we depend on. You cannot just type computed: "isContinueEnabled()" and then use this.name and this.agreementConfirmed in the isContinueEnabled method, because then Polymer won’t know what properties we depend on and it won’t re-calculate a computed property when required.

What we have learned so far
  • We can use computed properties when we need some information that can be calculated based on other properties.

Observers

Sometimes, it is needed to listen to changes in some properties and react on them. For example, we may want to send some notifications to a server when users enter some information.

The example below demonstrates it. Basically, users can choose a type of their company and we save this information somewhere in a database.

Company type selector

Source code

index.html
<html>
<head>
	<link rel="import" href="src/polymer-advanced/observers/company-type-select.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <company-type-select></company-type-select>
</body>
</html>
src/polymer-advanced/observers/company-type-select.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<!-- Material design implementation of a radio button -->
<link rel="import" href="../../../bower_components/paper-radio-button/paper-radio-button.html">
<!-- PaperRadioGroup allows you to group radio buttons, so only one can be selected at any time -->
<link rel="import" href="../../../bower_components/paper-radio-group/paper-radio-group.html">
<!-- Decorative spinner that indicates that something is happening in the background -->
<link rel="import" href="../../../bower_components/paper-spinner/paper-spinner.html">

<dom-module id="company-type-select">
  <template>

    Please select a type of your company:
    <paper-radio-group selected="{{companyType}}">
      <!-- We are disabling radio buttons when we send a notification to a server just to simplify an application -->
      <!--  and prevent a user from rapidly changing settings -->
      <paper-radio-button name="ltd" disabled="[[persistenceInProgress]]">LTD</paper-radio-button>
      <paper-radio-button name="llp" disabled="[[persistenceInProgress]]">LLP</paper-radio-button>
    </paper-radio-group>

    <br/>

    <template is="dom-if" if="[[persistenceInProgress]]">
      <paper-spinner active></paper-spinner>
    </template>

  </template>
  <script>
    class CompanyTypeSelect extends Polymer.Element {
      static get is() {
        return 'company-type-select';
      }

      static get properties() {
        return {
          companyType: {
            type: String,
            observer: 'companyTypeChanged'
          },
          persistenceInProgress: {
            type: Boolean,
            value: false
          }
        }
      }

      // The function accepts 2 parameters. In our case we don't need them
      // so we could just write 'companyTypeChanged: function()'.
      // Based on radio-buttons we can say that newValue would be either 'ltd' or 'llp'.
      companyTypeChanged(newValue, oldValue) {
        this.set('persistenceInProgress', true);
        // It's a stub instead of sending some asynchronous request
        setTimeout(function() {
          this.set('persistenceInProgress', false);
        }.bind(this), 1000);
      }
    }

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

Observers can be much more complex. For example, you may want to monitor changes in several different properties.

Let’s enhance our previous example by adding a contact name input.

src/polymer-advanced/observers/company-type-select-enhanced.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
<link rel="import" href="../../../bower_components/paper-checkbox/paper-checkbox.html">
<link rel="import" href="../../../bower_components/paper-radio-button/paper-radio-button.html">
<link rel="import" href="../../../bower_components/paper-radio-group/paper-radio-group.html">
<link rel="import" href="../../../bower_components/paper-spinner/paper-spinner.html">

<dom-module id="company-type-select-enhanced">
  <template>

    <div>
      Please select a type of your company:
      <paper-radio-group selected="{{companyType}}">
        <paper-radio-button name="ltd" disabled="[[persistenceInProgress]]">LTD</paper-radio-button>
        <paper-radio-button name="llp" disabled="[[persistenceInProgress]]">LLP</paper-radio-button>
      </paper-radio-group>
    </div>
    <div>
      <paper-checkbox disabled="[[persistenceInProgress]]" checked="{{notificationSubscription}}">
        I want to get notifications
      </paper-checkbox>
    </div>
    <br/>
    <div>
      <template is="dom-if" if="[[persistenceInProgress]]">
        <paper-spinner active></paper-spinner>
      </template>
    </div>

  </template>
  <script>
    class CompanyTypeSelectEnhanced extends Polymer.Element {
      static get is() {
        return 'company-type-select-enhanced';
      }

      static get properties() {
        return {
          companyType: String,
          name: String,
          notificationSubscription: Boolean,
          persistenceInProgress: {
            type: Boolean,
            value: false
          }
        }
      }

      // This static getter is used to specify an array of observers
      static get observers() {
        return [
          'userInfoChanged(companyType, notificationSubscription)'
        ];
      }

      userInfoChanged(companyType, notificationSubscription) {
        this.set('persistenceInProgress', true);
        setTimeout(function() {
          this.set('persistenceInProgress', false);
        }.bind(this), 1000);
      }
    }

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

Enhanced company type selector

What we have learned so far
  • We can monitor changes in a single property by using an observer attribute. An observer function accepts 2 arguments: an old value and a new one.

  • We can monitor several properties at the same time by using observers array. But we lose information about old values in this case.

Lifecycle Callbacks

Polymer components allow you to define code that would be called in response to certain lifecycle events.

To use this feature, implement methods with the following names:

  • constructor - called when an element is created but before property values are set.

  • connectedCallback - called when an element is created and properties are set.

  • disconnectedCallback - called when an element is removed from a document.

For example, a user opens some profile page and we would like to load all required details from the server before showing them to the user.

index.html
<html>
<head>
	<link rel="import" href="src/polymer-advanced/lifecycle/profile-page.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <profile-page></profile-page>
</body>
</html>
src/polymer-advanced/lifecycle/profile-page.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<link rel="import" href="../../../bower_components/paper-button/paper-button.html">
<link rel="import" href="personal-details-page.html">
<link rel="import" href="preferences-page.html">

<dom-module id="profile-page">
  <template>

    <!-- Navigation section -->
    <paper-button on-click="openPersonalDetailsPage">Personal Details</paper-button>
    <paper-button on-click="openPreferencesPage">Preferences</paper-button>

    <hr/>

    <!-- dom-if by default doesn't remove it's content when condition evaluates to false, it just hides it. -->
    <!-- 'restamp' attribute allows you to override this behavior causing HTML element actually being added -->
    <!--  and removed each time a condition changes. -->
    <template is="dom-if" if="[[personalDetailsPageOpened]]" restamp>
      <personal-details-page></personal-details-page>
    </template>
    <template is="dom-if" if="[[preferencesPageOpened]]" restamp>
      <preferences-page id="test"></preferences-page>
    </template>

  </template>
  <script>
    class ProfilePage extends Polymer.Element {
      static get is() {
        return 'profile-page';
      }

      static get properties() {
        return {
          personalDetailsPageOpened: {
            type: Boolean,
            value: true
          },
          preferencesPageOpened: {
            type: Boolean,
            value: false
          }
        }
      }

      openPersonalDetailsPage() {
        this.set('personalDetailsPageOpened', true);
        this.set('preferencesPageOpened', false);
      }

      openPreferencesPage() {
        this.set('personalDetailsPageOpened', false);
        this.set('preferencesPageOpened', true);
      }
    }

    customElements.define(ProfilePage.is, ProfilePage);
  </script>
</dom-module>
src/polymer-advanced/lifecycle/personal-details-page.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<link rel="import" href="../../../bower_components/paper-spinner/paper-spinner.html">

<dom-module id="personal-details-page">
  <template>

    <h3>Personal Details</h3>

    <template is="dom-if" if="[[dataLoaded]]">
      <p>First name: [[details.name]]</p>
      <p>Family name: [[details.familyName]]</p>
      <p>Age: [[details.age]]</p>
    </template>
    <template is="dom-if" if="[[!dataLoaded]]">
      <div>
        <paper-spinner active></paper-spinner>
      </div>
    </template>

  </template>
  <script>
    class PersonalDetailsPage extends Polymer.Element {
      static get is() {
        return 'personal-details-page';
      }

      static get properties() {
        return {
          dataLoaded: Boolean,
          details: Object
        }
      }

      connectedCallback() {
        // When using callbacks we have to call a parent method first
        //  to ensure that all standard logic and logic from mixins (if they are used)
        //  is applied
        super.connectedCallback();

        this.set('dataLoaded', false);
        this.set('details', {});
        setTimeout(function() {
          this.set('details', {
            name: 'John',
            familyName: 'Black',
            age: 31
          });
          this.set('dataLoaded', true);
        }.bind(this), 1500);
      }
    }

    customElements.define(PersonalDetailsPage.is, PersonalDetailsPage);
  </script>
</dom-module>
src/polymer-advanced/lifecycle/preferences-page.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<link rel="import" href="../../../bower_components/paper-spinner/paper-spinner.html">

<dom-module id="preferences-page">
  <template>

    <h3>Preferences</h3>

    <template is="dom-if" if="[[dataLoaded]]">
      <p>Favourite color: [[preferences.color]]</p>
      <p>Favourite song: [[preferences.song]]</p>
    </template>
    <template is="dom-if" if="[[!dataLoaded]]">
      <div>
        <paper-spinner active></paper-spinner>
      </div>
    </template>

  </template>
  <script>
    class PreferencesPage extends Polymer.Element {
      static get is() {
        return 'preferences-page';
      }

      static get properties() {
        return {
          dataLoaded: Boolean,
          preferences: Object
        }
      }

      connectedCallback() {
        super.connectedCallback();
        this.set('dataLoaded', false);
        this.set('preferences', {});
        setTimeout(function() {
          this.set('preferences', {
            color: 'Aquamarine',
            song: 'My Sharona'
          });
          this.set('dataLoaded', true);
        }.bind(this), 500);
      }
    }

    customElements.define(PreferencesPage.is, PreferencesPage);
  </script>
</dom-module>
What we have learned so far
  • During their lifecycle, Polymer elements call a number of callback methods. We can use these methods to invoke our initialization logic.

Mixins

Inheritance in Polymer is implemented with so called mixins. A mixin is a set of methods, properties, observers and lifecycle callback methods that can be inherited by any Polymer component.

Each web component can use any number of mixins. Web components can use mixins' methods and properties as if they were their own. And mixins can use web components' methods and properties.

Below is an example demonstrating how to write and use mixins. It’s a spelling improvement program that offers a user to type a word. If the user fails to spell it correctly then the input will be highlighted with red. On any typing the highlighting will be removed. Logic for setting/removing error state in the component is implemented by a mixin called ValidatedElementMixin.

Spelling checker

Source code

index.html
<html>
<head>
	<link rel="import" href="src/polymer-advanced/mixins/spelling-checker.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <spelling-checker word="Elephant"></spelling-checker>
</body>
</html>
src/polymer-advanced/mixins/spelling-checker.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="input-with-validation.html">

<dom-module id="spelling-checker">
  <template>

    <input-with-validation id="wordInput" value="{{typedValue}}">
      Please try to enter word "[[word]]" without mistakes
    </input-with-validation>
    <button on-click="checkWord">Check</button>

  </template>
  <script>
    class SpellingChecker extends Polymer.Element {
      static get is() {
        return 'spelling-checker';
      }

      static get properties() {
        return {
          typedValue: String,
          word: String
        }
      }

      checkWord() {
        if (this.typedValue === this.word) {
          alert('Great! You did it!');
        } else {
          this.$.wordInput.setError('The word was not typed correctly. Please, try again!');
        }
      }
    }

    customElements.define(SpellingChecker.is, SpellingChecker);
  </script>
</dom-module>
src/polymer-advanced/mixins/input-with-validation.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
<link rel="import" href="validated-element-mixin.html">

<dom-module id="input-with-validation">
  <template>
    <style>
      .error-input-msg,
      input[error] {
        color: red;
      }
    </style>

    <label>
      <slot></slot>
    </label>

    <br/>
    <br/>

    <iron-input bind-value="{{value}}">
      <input error$="[[error]]" placeholder="[[placeholder]]" />
    </iron-input>

    <br/>
    <br/>

    <template is="dom-if" if="[[errorMsg]]">
      <div class="error-input-msg">[[errorMsg]]</div>
      <br/>
    </template>

  </template>
  <script>
    // Please note how we are inheriting our class from a mixin
    class InputWithValidation extends ValidatedElementMixin(Polymer.Element) {
      static get is() {
        return 'input-with-validation';
      }

      static get properties() {
        return {
          value: {
            type: String,
            // This attribute is required to enable 2-way binding.
            // If we don't set it then the client of the input-with-validation.html won't get notifications
            //  that the property has changed.
            notify: true
          }
        }
      }
    }

    customElements.define(InputWithValidation.is, InputWithValidation);
  </script>
</dom-module>
src/polymer-advanced/mixins/validated-element-mixin.html
<script>

  // Mixin can contain properties, observers, methods, lifecycle callback methods as a normal Polymer component
  ValidatedElementMixin = function(superClass) {
    // Actually a mixin function just returns a class extending a class passed to it
    return class extends superClass {
      static get properties() {
        return {
          error: Boolean,
          errorMsg: String
        };
      }

      static get observers() {
        return [
          '_resetError(value)'
        ];
      }

      setError(msgCode) {
        this.set('error', true);
        this.set('errorMsg', msgCode);
      }

      _resetError() {
        this.set('error', false);
        this.set('errorMsg', null);
      }
    }
  };

</script>

input-with-validation represents a common UI component that supports validation. ValidatedElementMixin can be used with other types of elements: comboboxes, text areas, radio-buttons, etc.

Tip

Please note what we marked the value property in input-with-validation.html with the notify attribute. It’s a necessary detail if want to allow clients of this element to use this property with 2-way binding.

In this example, we used just one mixin, but it’s possible to use any number of mixins by including them one into another. For example, we could create something like that:

class PowerfulInput extends ElementWithDebounceMixin(SelfPersistedElementMixin(ValidatedElementMixin(Polymer.Element)))
Tip

Please note that prior to version 2.0, Polymer used so called behaviors instead of mixins. They are elements similar to mixins with the same purpose and possibilities but using another syntax for creation and usage. You don’t need to create or use behaviors in your code but you might encounter them in third-party components. See more details in https://www.polymer-project.org/1.0/docs/devguide/behaviors.

What we have learned so far
  • We can use mixins to implement some common logic and share it between components.

  • Mixins can contain methods, properties, lifecycle callback methods and observers.

  • After extending a mixin, a Polymer component doesn’t know which properties/methods are inherited and which are not. So, the component treats them the same.

  • In order to allow some property to be used by clients in two-way binding, we have to mark it with the notify parameter.

CUBA Web Components

CUBA platform provides a set of specific web components for solving common problems that arise when working with CUBA applications:

In order to create an application with a custom design, our UI components (cuba-login, cuba-file-field) can be used as a template for creating your own components.

Components that work with entities (cuba-entity, cuba-entities, cuba-entity-form) are most convenient if an application has a relatively simple data model. For more complex cases, use custom service methods and queries (cuba-query, cuba-service, cuba-service-form).

Application Setup

cuba-app is a mandatory element for any CUBA Polymer application. It should be defined in your application as early as possible. cuba-app contains initialization logic that is required by other CUBA Polymer components. That is, all other CUBA Polymer components won’t work if cuba-app is absent in your code.

Below is an example of using cuba-app element.

index.html
<html>
<head>
	<link rel="import" href="src/cuba/init/empty-app.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <empty-app></empty-app>
</body>
</html>
src/cuba/init/empty-app.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-app.html">

<dom-module id="empty-app">
  <template>

    <!-- 'api-url' informs the component where your CUBA REST API is located. -->
    <!-- By default on application creation it is '/app/rest'. -->
    <!-- It can be different if you have changed "Module prefix" in "Project properties > Advanced", -->
    <!-- e.g. '/sales/rest' -->
    <cuba-app api-url="/app/rest/"></cuba-app>

    <!-- Here goes the rest of the application. -->

  </template>
  <script>
    class EmptyApp extends Polymer.Element {
      static get is() {
        return 'user-info-component';
      }
    }

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

After including cuba-app, you can use all other CUBA Polymer components.

cuba-rest-js Library

Under the hood, cuba-app uses the cuba-rest-js library. This library provides a convenient methods for working with CUBA REST API, such as:

  • login(login: string, password: string, options: object): Promise.

  • loadEntities(entityName: string, optoins: object): Promise.

  • getUserInfo(): Promise.

  • and so on.

As you can see, a lot of methods return promises. You can read about promises in MDN documentation.

In some cases you may want to use this API directly instead of using CUBA Polymer components. It can be achieved by implementing the CubaAppAwareBehavior behavior. This behavior provides the app property that has public API mentioned above.

For example, below is a simple application that shows an information about the currently authenticated user.

Source code

index.html
<html>
<head>
	<link rel="import" href="src/cuba/init/user-info-component.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <user-info-component></user-info-component>
</body>
</html>
src/cuba/init/user-info-component.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-app.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-app-aware-behavior.html">

<dom-module id="user-info-component">
  <template>

    <cuba-app api-url="/app/rest/"></cuba-app>

    <template is="dom-if" if="[[userInfoLoaded]]">
      <p><b>First name:</b> [[userInfo.firstName]]</p>
      <p><b>Last name:</b> [[userInfo.lastName]]</p>
      <p><b>Locale:</b> [[userInfo.locale]]</p>
    </template>

  </template>
  <script>
    // CUBA elements are currently in hybrid mode.
    // So, we have to use special mixin syntax to extend its behaviors.
    class UserInfoComponent extends Polymer.mixinBehaviors([CubaAppAwareBehavior], Polymer.Element) {
      static get is() {
        return 'user-info-component';
      }

      static get observers() {
        // In the most cases an observer on `app` is not required
        //  because 'app' is usually used when we are 100% sure that
        //  the application is already initialized
        return [
          '_requestUserInfo(app)'
        ];
      }

      static get properties() {
        return {
          userInfo: Object,
          userInfoLoaded: Boolean
        };
      }

      _requestUserInfo() {
        // CubaAppAwareBehavior provided us with the 'app' property.
        this.app.getUserInfo().then(function(userInfo) {
          this.set('userInfo', userInfo);
          this.set('userInfoLoaded', true);
        }.bind(this));
      }

    }

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

Result

Content of the userInfo object is described in the REST API Swagger documentation.

The way we extended CubaAppAwareBehavior might seem unintuitive. The reason is that CUBA elements are currently in hybrid mode, which means that they support both Polymer 1.0 and Polymer 2.0 syntax. In Polymer 1.0 there were behaviors instead of mixins. You can find more information about hybrid elements at https://www.polymer-project.org/2.0/docs/devguide/hybrid-elements.

Login Form

All CUBA REST API methods require an OAuth token. In order to obtain this token the client must authenticate using user’s login and password. This mechanism is described in the Developer’s Manual.

The cuba-login web component allows you to create a login form. It’s a simple UI component with 2 fields ("User Name", "Password") and the "Login" button. It sends two events on login attempt: cuba-login-success and cuba-login-error.

Below is a working example, please use login test and password test.

Source code

index.html
<html>
<head>
	<link rel="import" href="src/cuba/login/app-with-login.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <app-with-login></app-with-login>
</body>
</html>
src/cuba/login/app-with-login.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-app.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-app-aware-behavior.html">
<link rel="import" href="../../../bower_components/cuba-login/cuba-login.html">

<dom-module id="app-with-login">
  <template>

    <!-- 'cuba-app' always should be the first thing in an application -->
    <!--  uses CUBA REST API -->
    <cuba-app api-url="/app/rest/"></cuba-app>

    <template is="dom-if" if="[[!authenticated]]">
      <!-- Only after the user is authenticated -->
      <!--  the rest of the CUBA REST API can be used -->
      <cuba-login on-cuba-login-success="_onLoginSuccess" on-cuba-login-error="_onLoginError"></cuba-login>
    </template>

    <template is="dom-if" if="[[authenticated]]">
      <h3>Congratulations! You are authenticated and now can use cuba REST API!</h3>
      <div>
        <button on-click="_logout">Logout</button>
      </div>
    </template>

  </template>
  <script>
    class AppWithLogin extends Polymer.mixinBehaviors([CubaAppAwareBehavior], Polymer.Element) {
      static get is() {
        return 'app-with-login';
      }

      static get properties() {
        return {
          authenticated: {
            type: Boolean,
            value: false
          }
        };
      }

      _onLoginSuccess() {
        this.set('authenticated', true);
      }

      _logout() {
        this.set('authenticated', false);
        this.app.logout();
      }

      _onLoginError() {
        alert('Bad credentials');
      }

    }

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

If you check cuba-login source code, you can see that the component is opened for extension by using custom property mixins.

bower_components/cuba-login/cuba-login.html
  #form {
    @apply --cuba-login-form;
  }
  #username {
    @apply --cuba-login-username-input;
  }
  #password {
    @apply --cuba-login-password-input;
  }
  #submit {
    @apply --cuba-login-submit-button;
  }
  .actions {
    display: flex;
    flex-direction: row-reverse;
    @apply --cuba-login-actions;
  }
  ...
  <form id="form">
    <div class="fields">
      <paper-input type="text" id="username" label="[[msg('User Name')]]" value="{{username}}"></paper-input>
      <paper-input type="password" id="password" label="[[msg('Password')]]" value="{{password}}"></paper-input>
    </div>
    <div class="actions">
      <paper-button id="submit" on-tap="submit">[[msg('Login')]]</paper-button>
    </div>
  </form>

Below you can see how these mixins can be implemented. The example is the same as above but contains the <style/> section.

src/cuba/login/app-with-login-styled.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-app.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-app-aware-behavior.html">
<link rel="import" href="../../../bower_components/cuba-login/cuba-login.html">

<dom-module id="app-with-login-styled">
  <template>
    <style>
      .login-form {
        /* That's how we define a mixin */
        --cuba-login-form: {
          display: flex;
          justify-content: center;
        };
        --cuba-login-actions: {
          padding: 5px;
        };
        --cuba-login-submit-button: {
          border: black solid 1px;
        };
      }
    </style>

    <cuba-app api-url="/app/rest/"></cuba-app>

    <template is="dom-if" if="[[!authenticated]]">
      <cuba-login class="login-form"
                  on-cuba-login-success="_onLoginSuccess"
                  on-cuba-login-error="_onLoginError"></cuba-login>
    </template>

    <template is="dom-if" if="[[authenticated]]">
      <h3>Congratulations! You are authenticated and now can use cuba REST API!</h3>
      <div>
        <button on-click="_logout">Logout</button>
      </div>
    </template>

  </template>
  <script>
    class AppWithLoginStyled extends Polymer.mixinBehaviors([CubaAppAwareBehavior], Polymer.Element) {
      static get is() {
        return 'app-with-login-styled';
      }

      static get properties() {
        return {
          authenticated: {
            type: Boolean,
            value: false
          }
        };
      }

      _onLoginSuccess() {
        this.set('authenticated', true);
      }

      _logout() {
        this.set('authenticated', false);
        this.app.logout();
      }

      _onLoginError() {
        alert('Bad credentials');
      }

    }

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

Result of styling

Writing your own login form

cuba-login, as any other CUBA component uses cuba-rest component API under the hood. It means that if you need some very custom login page, you can use the API directly. See an example below.

src/cuba/login/app-with-login-custom.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-app.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-app-aware-behavior.html">

<dom-module id="app-with-login-custom">
  <template>

    <cuba-app api-url="/app/rest/"></cuba-app>

    <template is="dom-if" if="[[!authenticated]]">

      <label>
        Please choose your login:
      </label>
      <br/>
      <br/>
      <select id="typeSelect">
        <option value="marketer">Marketer</option>
        <option value="manager">Manager</option>
        <option value="admin">Admin</option>
      </select>
      <br/>
      <br/>
      <button on-click="_login">Login</button>
    </template>

    <template is="dom-if" if="[[authenticated]]">
      <h3>Congratulations! You are authenticated and now can use cuba REST API!</h3>
      <div>
        <button on-click="_logout">Logout</button>
      </div>
    </template>

  </template>
  <script>
    class AppWithLoginCustom extends Polymer.mixinBehaviors([CubaAppAwareBehavior], Polymer.Element) {
      static get is() {
        return 'app-with-login-custom';
      }

      static get properties() {
        return {
          authenticated: {
            type: Boolean,
            value: false
          }
        };
      }

      _login() {
        const login = this.shadowRoot.querySelector('#typeSelect').value;

        let password;

        // In accordance with the best security anti-patterns
        switch (login) {
          case 'marketer':
            password = 'marketer!';
            break;
          case 'manager':
            password = 'manager:-)';
            break;
          case 'admin':
            password = 'admin123';
            break;
        }

        this.app.login(login, password).then(function() {
          this.set('authenticated', true);
        }.bind(this));
      }

      _logout() {
        this.set('authenticated', false);
        this.app.logout();
      }

      _onLoginError() {
        alert('Bad credentials');
      }

    }

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

Custom login form

Token expiration

A Polymer application receives a token after authentication and then uses it with every request.

By default, a token is valid for 12 hours. After this period requests stop working and the user has to re-login. We recommend to increase the token expiration time and use the persistent token store to save tokens on server restart.

cuba-app sends the cuba-token-expired event that can be used to handle the expiration appropriately.

Working with Entities

CUBA provides a number of components for CRUD operations with entities.

Before we proceed, it’s worth to remind some basic concepts related to entities:

  • Entity name uniquely identifies the class of entity. It consists of two parts divided by "$": {namespace$concept}. For example: taxi$Driver, statistics$FieldDescription, etc. On the middleware, an entity name is specified in the @Entity annotation of the entity Java class.

  • Entity view is a descriptor of what attributes of the entity and its related entities should be loaded from the database. For the sake of performance, a view should contain a minimal possible number of attributes. See more in the Developer’s Manual.

Entity browse
  • The cuba-entities component is designed for loading a list of entities.

  • The cuba-entity component is designed for loading one entity instance by its id.

Below is an example of how these components can be used - a book browser application. A user sees a list of books and can select any book to see its details.

The list of books is loaded by cuba-entities, cuba-entity is used to re-load a particular book. When we show a list of books we load as little information as possible for better performance. When a single book is selected by the user, we can afford loading a lot more: author biography, editions, even a photo of a cover page.

Book browser

Source code

index.html
<html>
<head>
	<link rel="import" href="src/cuba/entity/books-browser.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <books-browser></books-browser>
</body>
</html>
src/cuba/entity/books-browser.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-repeat.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-app.html">
<link rel="import" href="../../../bower_components/cuba-data/cuba-entities.html">
<link rel="import" href="../../../bower_components/cuba-data/cuba-entity.html">

<link rel="import" href="../../../bower_components/paper-spinner/paper-spinner.html">

<dom-module id="books-browser">
  <template>
    <style>
      .book-option {
        color: #0b6ec7;
        padding: 5px 0;
      }
      .book-option-value {
        cursor: pointer;
      }
      .book-option:before {
        content: '\25cf';
      }
      .book-description {
        padding-top: 10px;
      }
    </style>

    <cuba-app api-url="/app/rest/"></cuba-app>

    <!-- Entities will be loaded automatically on the component initialization. -->
    <!-- The result of the request will be stored into 'books' property. -->
    <cuba-entities view="bookBrowse" entity-name="cuba$Book" data="{{books}}"></cuba-entities>

    <!-- auto="false" indicates that no loading happens on initialization. -->
    <!-- So, we need to use this component programmatically. -->
    <!-- While a request is going on the 'bookLoading' property will be set to true. -->
    <cuba-entity view="bookEdit" id="entityLoader" entity-name="cuba$Book" auto="false" loading="{{bookLoading}}"></cuba-entity>

    <h3>
      Please select a book to see more info:
    </h3>

    <template id="bookRepeater" is="dom-repeat" items="[[books]]">
      <div class="book-option">
        <span class="book-option-value" on-click="_onBookSelect">[[item.title]]</span>
      </div>
    </template>

    <div class="book-description">
      <template is="dom-if" if="[[bookLoading]]">
        <paper-spinner active></paper-spinner>
      </template>
      <template is="dom-if" if="[[selectedBook]]">
        Book <b>[[selectedBook.title]]</b> of [[selectedBook.genre]] genre was written by
        [[selectedBook.author.name]] ([[selectedBook.author.born]] - [[_formatDeathDate(selectedBook.author.died)]])
        and published in [[selectedBook.publicationYear]] year.
      </template>
    </div>

  </template>
  <script>
    class BooksBrowser extends Polymer.Element {
      static get is() {
        return 'books-browser';
      }

      static get properties() {
        return {
          books: {
            type: Array,
            value: function() {
              return [];
            }
          },
          selectedBook: Object,
          bookLoading: Boolean
        };
      }

      _onBookSelect(e) {
        this.set('selectedBook', null);

        // It's how we can obtain a clicked item from 'dom-repeat'
        const bookId = this.$.bookRepeater.modelForElement(e.target).get('item.id');
        this.$.entityLoader.entityId = bookId;
        this.$.entityLoader.load().then(function(book) {
          this.set('selectedBook', book);
        }.bind(this));
      }

      _formatDeathDate(date) {
        return !!date ? date : 'Present days';
      }

    }

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

In this example we have omitted a code for login simplicity. In real applications, REST API won’t work until you login or enable anonymous access.

Entity creation

The cuba-entity-form component provides an ability to create new entities. Basically, you have to provide an entity name and an entity instance you want to persist. After that, you can call the submit method and the entity will be saved.

Book creator

Source code

index.html
<html>
<head>
	<link rel="import" href="src/cuba/entity/book-creator.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <book-creator></book-creator>
</body>
</html>
src/cuba/entity/book-creator.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-app.html">
<link rel="import" href="../../../bower_components/cuba-form/cuba-entity-form.html">

<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
<link rel="import" href="../../../bower_components/paper-spinner/paper-spinner.html">

<dom-module id="book-creator">
  <template>
    <cuba-app api-url="/app/rest/"></cuba-app>

    <cuba-entity-form id="bookForm" on-cuba-form-response="_onSaveComplete" entity-name="cuba$Book" entity="[[book]]">
      <label>
        Title
        <br/>
        <iron-input bind-value="{{book.title}}">
          <input />
        </iron-input>
      </label>
      <br/>
      <br/>
      <label>
        Genre
        <br/>
        <iron-input bind-value="{{book.genre}}">
          <input />
        </iron-input>
      </label>
      <br/>
      <br/>
      <label>
        Publication Year
        <br/>
        <iron-input bind-value="{{book.publicationYear}}">
          <input />
        </iron-input>
      </label>
    </cuba-entity-form>

    <div>
      <template is="dom-if" if="[[_savingInProgress]]">
        <paper-spinner active></paper-spinner>
        <br/>
      </template>
    </div>

    <button on-click="_createBook">
      Create book
    </button>

  </template>
  <script>
    class BookCreator extends Polymer.Element {
      static get is() {
        return 'book-creator';
      }

      static get properties() {
        return {
          book: Object,
          _savingInProgress: {
            type: Boolean,
            value: false
          }
        };
      }

      _createBook() {
        this.set('_savingInProgress', true);
        this.$.bookForm.submit();
      }

      _onSaveComplete() {
        this.set('_savingInProgress', false);
        this.set('book', null);
        alert('The book has been successfully created');
      }

    }

    customElements.define(BookCreator.is, BookCreator);
  </script>
</dom-module>
Entity removal

The cuba-entities component has method remove(), which you can use to remove an entity instance.

Entity update

Entity can be updated using the cuba-entity-form component.

Conclusion

That was an overview of how CUBA Polymer components can be used to work with entities. But the components provide more functionality than was described in this section. For example, cuba-entity allows you to set the debounce parameter to avoid excessive requests to a server; cuba-entities can sort entities by any field; and so on. To learn more, check out the public API at https://cuba-elements.github.io/cuba-elements and the source code of components.

Using Services and Queries

In most cases, applications are not limited to CRUD operations with entities. CUBA REST API provides special endpoints for invoking middleware services and running predefined queries. These are places where you can put your business logic and invoke it from the front-end using the cuba-service and cuba-query components.

By default, the components automatically load data on initialization. You can disable this behavior by assigning false to property auto and calling load() method programmatically.

Usage example

    <cuba-query id="query"
                entity-name="sec$User"
                query-name="usersByName"
                data="{{users}}">
    </cuba-query>
    ...
    <cuba-service service-name="cuba_ServerInfoService"
                  method="getReleaseNumber"
                  data="{{releaseNumber}}">
    </cuba-service>

cuba-service-form can be used instead of cuba-service when it’s semantically more correct to use form submit instead of information requests. However, it’s not mandatory, and under the hood they use the same REST API.

Uploading Files

The cuba-file-field component allows you to to upload files to the server.

The example below is a stub and doesn’t upload any files, so you can safely test it.

Source code:

index.html
<html>
<head>
	<link rel="import" href="src/cuba/file/file-upload-app.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <file-upload-app></file-upload-app>
</body>
</html>
src/cuba/file/file-upload-app.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-app.html">
<link rel="import" href="../../../bower_components/cuba-file-field/cuba-file-field.html">

<dom-module id="file-upload-app">
  <template>

    <cuba-app api-url="/app/rest/"></cuba-app>

    <cuba-file-field on-cuba-upload-success="_onUploadSuccess"></cuba-file-field>

    <br/>

    <template is="dom-if" if="[[fileIsUploaded]]">
      File "[[uploadedFile.name]]" is successfully saved into <b>SYS_FILE</b> table.
      The record id is [[uploadedFile.id]].
    </template>

  </template>
  <script>
    class FileUploadApp extends Polymer.Element {
      static get is() {
        return 'file-upload-app';
      }

      static get properties() {
        return {
          uploadedFile: Object,
          fileIsUploaded: Boolean
        };
      }

      _onUploadSuccess(e) {
        this.set('uploadedFile', e.detail);
        this.set('fileIsUploaded', true);
      }

    }

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

Common Tasks

From project to project, from one framework to another, we face the same set of standard problems:

  • How to implement internationalization?

  • How to build a navigation block?

  • How to inject services into components?

  • And so on.

Polymer doesn’t have a goal to solve these problems for us. It’s primary intention was to provide convenient means for creating custom reusable components. Dependency injection, navigation, etc. are beyond this task.

In this chapter, we discuss some of the common problems encountered in front-end development and offer solutions for them. Please consider our recommendations not as ultimate truth but as one of the possible ways to handle a problem.

Organizing Your Code

This section contains some tips that can help you to achieve better readability of the code and facilitate its maintenance.

Shared CSS

Polymer elements use Shadow DOM and their styles don’t overlap with each other. It’s a very useful feature, but sometimes you may want to share some CSS between elements.

Let’s examine an example of how to re-use CSS.

First of all we need to create an element that contains common styles we want to share.

src/recipes/convention/css/shared-styles.html
<dom-module id="shared-styles">
  <template>
    <style>
      /* Mixins and variables cannot be declared just inside <style/> tag. */
      /* We should declare them inside some selector. */
      /* :host > * is a good place to put global variables. */
      :host > * {
        /* CSS variable */
        --white-color: white;
      }

      .blue-block {
        background-color: #006ba9;
      }
    </style>
  </template>
</dom-module>

So, we have declared a class and a variable with a color. Now, we can use them in other components.

src/recipes/convention/css/blue-button.html
<link rel="import" href="../../../../bower_components/polymer/polymer-element.html">
<!-- Import file with styles in order to use them -->
<link rel="import" href="shared-styles.html">

<dom-module id="blue-button">
  <template>
    <!-- The styles below extend shared-styles.html -->
    <style include="shared-styles">
      .blue-block {
        border: none;
        border-radius: 3px;
        /* We use variable declared in shared-styles.html */
        color: var(--white-color);
        padding: 10px 15px;
      }
    </style>

    <!-- The button is blue because the color is specified in shared-styles.html -->
    <button class="blue-block">I'm a button and I'm proud of it!</button>

  </template>
  <script>
    class BlueButton extends Polymer.Element {
      static get is() {
        return 'blue-button';
      }
    }

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

The resulting blue-button element looks as follows:

Private Methods and Properties

Any Polymer component can expose an API consisting of a number of methods and properties. These component members can be called public. But there are also properties and methods that are supposed to be used only by the component itself. We can call these members private.

It’s a good practice to prefix private methods and properties with underscore.

See an example below.

Disco lights application

Source code

index.html
<html>
<head>
	<link rel="import" href="src/recipes/convention/access-control/disc-jockey.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <disc-jockey></disc-jockey>
</body>
</html>
src/recipes/convention/access-control/disc-jockey.html
<link rel="import" href="../../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="disco-lights.html">

<dom-module id="disc-jockey">
  <template>

    <disco-lights id="square"></disco-lights>

    <br/>
    <br/>

    <button on-click="_start">Start</button>
    <button on-click="_stop">Stop</button>

  </template>
  <script>
    class DiscJockey extends Polymer.Element {
      static get is() {
        return 'disc-jockey';
      }

      _start() {
        this.$.square.start();
      }

      _stop() {
        this.$.square.stop();
      }

    }

    customElements.define(DiscJockey.is, DiscJockey);
  </script>
</dom-module>
src/recipes/convention/access-control/disco-lights.html
<link rel="import" href="../../../../bower_components/polymer/polymer-element.html">

<dom-module id="disco-lights">
  <template>
    <style>
      .square {
        background: black;
        display: inline-block;
        height: 20px;
        width: 20px;
      }
    </style>

    <div class="square" id="square"></div>

    Current color: [[_currentColor]]

  </template>
  <script>
    class DiscoLights extends Polymer.Element {
      static get is() {
        return 'disco-lights';
      }

      static get properties() {
        return {
          // 'colorChangeInterval' is a public property because
          //  it's very possible that a client of the component might want
          //  to change it
          colorChangeInterval: {
            type: Number,
            value: 800
          },
          // Scheduler id is used for some internal work.
          // Therefore, it's a private member.
          _schedulerId: {
            type: Number,
            value: null
          },
          // The current color is displayed in the UI of this component.
          _currentColor: String
        }
      }

      // It's definitely a public method because it's intended
      //  to be used by some outer component
      start() {
        if (!this._schedulerId) {
          this._startChangingColor();
        }
      }

      stop() {
        if (!!this._schedulerId) {
          clearInterval(this._schedulerId);
          this.set('_schedulerId', null);
        }
      }

      // It's not a public member but we have no means to change a name
      //  of a standard lifecycle callback method
      connectedCallback() {
        super.connectedCallback();
        this._startChangingColor();
      }

      // The method is supposed to be used only by the component itself.
      // So, it's a private member.
      _startChangingColor() {
        const schedulerId = setInterval(function() {
          let color = '#';
          for (let i = 0; i < 6; i++) {
            color += '0123456789ABCDEF'[Math.floor(Math.random() * 16)];
          }
          this.$.square.style.backgroundColor = color;
          this.set('_currentColor', color);
        }.bind(this), this.colorChangeInterval);

        this.set('_schedulerId', schedulerId);

      }
    }

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

The _currentColor property is made private, but for a component which is designed to be reusable across many projects this property could be public. Another option would be creating an event with the information about currently selected color.

By adopting this convention we achieve at least the following goals:

  • We explicitly declare what element members can be used.

  • We make an API more clear and obvious.

  • During refactoring, we clearly see names of properties and methods that shouldn’t be changed.

Managing Imports

In this section, we consider the problem of correct usage of HTML imports.

Let’s look at the next example. There are 2 components. The first one contains a paper button and a 2nd component. The 2nd component also contains a paper button. The buttons do nothing, but the example demonstrates some interesting details about imports.

Source code:

index.html
<html>
<head>
	<link rel="import" href="src/recipes/convention/import/parent-button-component.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <parent-button-component></parent-button-component>
</body>
</html>
src/recipes/convention/import/parent-button-component.html
<link rel="import" href="../../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../../bower_components/paper-button/paper-button.html">

<link rel="import" href="child-button-component.html">

<dom-module id="parent-button-component">
  <template>

    <paper-button raised>It's a button in a parent component!</paper-button>

    <child-button-component>It's a child component button.</child-button-component>

  </template>
  <script>
    class ParentButtonComponent extends Polymer.Element {
      static get is() {
        return 'parent-button-component';
      }

    }

    customElements.define(ParentButtonComponent.is, ParentButtonComponent);
  </script>
</dom-module>
src/recipes/convention/import/child-button-component.html
<link rel="import" href="../../../../bower_components/polymer/polymer-element.html">

<dom-module id="child-button-component">
  <template>

    <!-- paper-button is absent in imports. But the example still works. -->
    <paper-button raised>
      <slot></slot>
    </paper-button>

  </template>
  <script>
    class ChildButtonComponent extends Polymer.Element {
      static get is() {
        return 'child-button-component';
      }
    }

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

As you can see, we forgot to import the button component into the child component but the example application still works.

What will happen if we later refactor our parent-button-component and decide that the simple HTML <button/> will suffice and remove paper-button? See below.

src/recipes/convention/import/parent-simple-button-component.html
<link rel="import" href="../../../../bower_components/polymer/polymer-element.html">

<link rel="import" href="child-button-component.html">

<dom-module id="parent-simple-button-component">
  <template>

    <button>It's a button in a parent component!</button>

    <child-button-component>It's a child component button</child-button-component>

  </template>
  <script>
    class ParentSimpleButtonComponent extends Polymer.Element {
      static get is() {
        return 'parent-simple-button-component';
      }
    }

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

Result

So, we have broken the child component involuntary.

It’s a simple case to present a matter. In real applications, there are dozens of components that import each other. So, paper-button could be used not in a direct child but in one of the great-great-great-grandchild components. After a complex refactoring, it’s possible to not notice that something broke. And when you finally discover that something doesn’t work, it can be difficult to find a cause.

But there can be even more complicated cases. In our example, we imported paper-button into the parent component and didn’t import it into the child component. But we could do the opposite thing: import it into the child component and do not import it into the parent one. And it would work! Apparently, in this case it can be broken even easier.

The point of all this is that we should use imports carefully. We recommend importing all required components in each custom component, and if you remove some HTML code later, check and remove redundant imports. In this case, all your components are guaranteed to work when other components are removed or refactored.

There is an ability to check imports automatically by running polymer lint command. In order to use it, you must install polymer-cli.

There are other conventions that can be used. For example, you can import all paper and iron elements in a root component and use them afterwards everywhere without import statements. Or you can even import all components in your root component, so you won’t need to use imports elsewhere in the code. The choice should be made by each team based on their preferences. In this case any convention, good or bad, is better than no convention at all.

Tip

If you bundle your client code before production, the problem is entirely in the area of development environment. Production and test code has no import statements because there is a single HTML file containing all code.

Internationalization

Internationalization is a common task for many projects. And even if we don’t need to support multiple languages, it still can be useful to store all user messages together to have better perspective and avoid duplication.

CUBA provides the CubaLocalizeBehavior that can assist you in this task. Basically, it just introduces the msg() method that gets messages from the messages property depending on the current locale. See an example below.

index.html
<html>
<head>
	<link rel="import" href="src/recipes/i18n/simple-greeting-component.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <simple-greeting-component></simple-greeting-component>
</body>
</html>
src/recipes/i18n/simple-greeting-component.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-app.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-app-aware-behavior.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-localize-behavior.html">

<dom-module id="simple-greeting-component">
  <template>

    <cuba-app api-url="/app/rest/"></cuba-app>

    <template is="dom-if" if="[[appReady]]">
      <h3>[[msg('greeting.caption')]]</h3>
      <p>[[msg('greeting.text')]]</p>
    </template>

  </template>
  <script>
    class SimpleGreetingComponent
      extends Polymer.mixinBehaviors([CubaLocalizeBehavior, CubaAppAwareBehavior], Polymer.Element) {

      static get is() {
        return 'simple-greeting-component';
      }

      static get observers() {
        return [
          '_init(app)'
        ];
      }

      static get properties() {
        return {
          appReady: Boolean,
          messages: {
            type: Array,
            value: function() {
              return {
                en: {
                  'greeting.caption': 'Hello, Username!',
                  'greeting.text': 'Welcome to the exiting and wonderful world of the internationalization!!!'
                },
                ru: {
                  'greeting.caption': 'Здравствуй, Анонимус!',
                  'greeting.text': 'Добро пожаловать в увлекательный и захватыващий мир приложений на нескольких языках!!!'
                }
              };
            }
          }
        }
      }

      _init() {
        this.set('appReady', true);
      }

    }

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

Result

CubaLocalizeBehavior requires an initialized cuba-app.

How the default locale is determined is up to you: you can set a locale on component initialization or provide some kind of language switcher that changes the locale. Below is an example of switching locale.

index.html
<html>
<head>
	<link rel="import" href="src/recipes/i18n/locale-switcher.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <locale-switcher></locale-switcher>
</body>
</html>
src/recipes/i18n/locale-switcher.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-app.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-app-aware-behavior.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-localize-behavior.html">

<link rel="import" href="../../../bower_components/paper-radio-group/paper-radio-group.html">
<link rel="import" href="../../../bower_components/paper-radio-button/paper-radio-button.html">

<dom-module id="locale-switcher">
  <template>

    <cuba-app api-url="/app/rest/"></cuba-app>

    <template is="dom-if" if="[[appReady]]">
      <p>[[msg('languageDescriptor')]]</p>

      <paper-radio-group selected="{{app.locale}}">
        <paper-radio-button name="en">[[msg('enLanguageName')]]</paper-radio-button>
        <paper-radio-button name="ru">[[msg('ruLanguageName')]]</paper-radio-button>
      </paper-radio-group>

    </template>

  </template>
  <script>
    class LocaleSwitcher
      extends Polymer.mixinBehaviors([CubaLocalizeBehavior, CubaAppAwareBehavior], Polymer.Element) {

      static get is() {
        return 'locale-switcher';
      }

      static get observers() {
        return [
          '_init(app)'
        ];
      }

      static get properties() {
        return {
          appReady: Boolean,
          messages: {
            type: Array,
            value: function() {
              return {
                en: {
                  'languageDescriptor': 'This text is written in English.',
                  'enLanguageName': 'English',
                  'ruLanguageName': 'Russian'
                },
                ru: {
                  'languageDescriptor': 'Вы видете текст на русском языке.',
                  'enLanguageName': 'Английский',
                  'ruLanguageName': 'Русский'
                }
              };
            }
          }
        }
      }

      _init() {
        this.set('appReady', true);
      }
    }

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

Result

Still it would be more convenient to have a single place where all messages are stored. It can be easily achieved by creating a proxy between CubaLocalizeBehavior and the rest of your application.

index.html
<html>
<head>
	<link rel="import" href="src/recipes/i18n/calcium-adv.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <calcium-adv></calcium-adv>
</body>
</html>
src/recipes/i18n/calcium-adv.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/polymer/lib/elements/dom-if.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-app.html">
<link rel="import" href="../../../bower_components/cuba-app/cuba-app-aware-behavior.html">

<link rel="import" href="../../../bower_components/paper-radio-group/paper-radio-group.html">
<link rel="import" href="../../../bower_components/paper-radio-button/paper-radio-button.html">

<link rel="import" href="i18n-mixin.html">

<dom-module id="calcium-adv">
  <template>

    <cuba-app api-url="/app/rest/"></cuba-app>

    <template is="dom-if" if="[[appReady]]">
      <h3>[[msg('calciumAdv.caption')]]</h3>

      <p>[[msg('calciumAdv.body')]]</p>

      <paper-radio-group selected="{{app.locale}}">
        <paper-radio-button name="en">[[msg('languages.en')]]</paper-radio-button>
        <paper-radio-button name="ru">[[msg('languages.ru')]]</paper-radio-button>
      </paper-radio-group>

    </template>

  </template>
  <script>
    class CalciumAdvertisement
      extends Polymer.mixinBehaviors([CubaAppAwareBehavior], I18nMixin(Polymer.Element)) {

      static get is() {
        return 'calcium-adv';
      }

      static get observers() {
        return [
          '_init(app)'
        ];
      }

      static get properties() {
        return {
          appReady: Boolean
        }
      }

      _init() {
        this.set('appReady', true);
      }
    }

    customElements.define(CalciumAdvertisement.is, CalciumAdvertisement);
  </script>
</dom-module>
src/recipes/i18n/i18n-mixin.html
<link rel="import" href="../../../bower_components/cuba-app/cuba-localize-behavior.html">

<script>

  I18nMixin = function(superClass) {
    return class extends Polymer.mixinBehaviors([CubaLocalizeBehavior], superClass) {
      static get properties() {
        return {
          messages: {
            type: Array,
            value: function() {
              return {
                en: {
                  'calciumAdv.caption': 'Calcium biological role',
                  'calciumAdv.body': 'Calcium is an essential element needed in large quantities. ' +
                    'The Ca2+ ion acts as an electrolyte and is vital to the health of the muscular, circulatory, ' +
                    'and digestive systems; is indispensable to the building of bone; ' +
                    'and supports synthesis and function of blood cells. For example, ' +
                    'it regulates the contraction of muscles, nerve conduction, and the clotting of blood.',

                  'languages.en': 'English',
                  'languages.ru': 'Russian'
                },
                ru: {
                  'calciumAdv.caption': 'Биологическое значение кальция',
                  'calciumAdv.body': 'Кальций - это важный элемент, необходимый в больших количествах. ' +
                  'Ионы Ca2+ работают, как электролиты, и представляют важность для здоровья системы кровообращения, ' +
                  'мышечной и пищеварительной систем. Кальций незаменим для строительства костей и способствует ' +
                  'синтезу и функционированию клеток крови. Например, кальций, регулирует мышечное сокращение, свертывание крови ' +
                  'и проводимость нейронов.',

                  'languages.en': 'Английский',
                  'languages.ru': 'Русский'
                }
              };
            }
          }
        };
      }
    }
  };

</script>

Result

As you can see, now the components just implement I18nMixin and don’t contain any actual messages.

Using REST API

This section contains some recommendations on consuming third-party REST APIs.

Fetch API

index.html generated by CUBA Studio contains a polyfill for Fetch API because it’s used by CUBA Polymer components.

So, you can easily use fetch in your own code.

src/recipes/ajax/fetch-example.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">

<dom-module id="fetch-example">
  <template>

    <!-- Some code here -->

  </template>
  <script>
    class FetchExample extends Polymer.Element {
      static get is() {
        return 'fetch-example';
      }

      connectedCallback() {
        super.connectedCallback();

        fetch('http://www.there-could-be-your-url.com/some-important-resource', {
          method: 'GET',
          headers: {
            accept: 'application/json'
          }
        })
        // 'fetch' returns a promise
          .then(function(response) {
            if (response.status !== 200) {
              // Do something there if a error response came from the server
            }
            // 'json()' returns a promise
            return response.json();
          })
          .then(function(jsonObject) {
            // Do something there with the response.
          });
      }
    }

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

Read this article to learn more about Fetch API.

iron-ajax

iron-ajax component is an another convenient option to perform AJAX requests.

src/recipes/ajax/iron-ajax-example.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="../../../bower_components/iron-ajax/iron-ajax.html">

<dom-module id="iron-ajax-example">
  <template>

    <iron-ajax
      auto
      url="http://www.very-important-resources.org/required-resource"
      params='{"company": "Haulmont"}'
      handle-as="json"
      on-response="_onImportantDataLoaded"></iron-ajax>

  </template>
  <script>
    class IronAjaxExample extends Polymer.Element {
      static get is() {
        return 'iron-ajax-example';
      }

      _onImportantDataLoaded(event, request) {
        const response = request.response;
        // Some actions with the response there
      }
    }

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

Services and Dependency Injection

In this section, by a service we mean a program component that provides some specific functionality to other components. The most common examples of services are components working with REST APIs, internationalization, utility services, caches, etc.

There is a number of approaches of creating services and injecting them into your web components. We recommend to adopt the way described below.

Let’s imagine an application that shows notifications to users. These notifications are represented by a piece of text that is shown to users for a short time to inform them about some event. Different parts of the application need an ability to show notifications.

Here is a possible implementation:

index.html
<html>
<head>
	<link rel="import" href="src/recipes/di/forbidden-button.html">
	<script src="bower_components/webcomponentsjs/webcomponents-loader.js"></script>
</head>
<body>
    <forbidden-button></forbidden-button>
</body>
</html>
src/recipes/di/forbidden-button.html
<link rel="import" href="../../../bower_components/polymer/polymer-element.html">
<link rel="import" href="notification-service.html">

<dom-module id="forbidden-button">
  <template>
    <style>
      .forbidden-btn {
        background: #006ba9;
        border: 1px solid #006ba9;
        border-radius: 5%;
        color: #ffffff;
        padding: 5px 10px;
      }
    </style>

    <button class="forbidden-btn" on-click="notifyWrongdoing">Don't push me!</button>

  </template>
  <script>
    class ForbiddenButton extends Polymer.Element {
      static get is() {
        return 'forbidden-button';
      }

      static get properties() {
        return {
          // Injecting the service
          notificationManager: {
            type: Object,
            value: NotificationManager
          }
        }
      }

      notifyWrongdoing() {
        this.notificationManager.showInfo('You have just pressed a button you should not have pressed. ' +
          'Please, try not to do it again.');
      }
    }

    customElements.define(ForbiddenButton.is, ForbiddenButton);
  </script>
</dom-module>
src/recipes/di/notification-service.html
<link rel="import" href="../../../bower_components/paper-toast/paper-toast.html">

<script>

  // Constructor. Place all your properties and initialization logic here.
  NotificationManagerClass = function() {
    this._toast = null;
  };

  // Place all methods here
  NotificationManagerClass.prototype = {

    get toast() {
      if (!this._toast) {
        this._toast = Polymer.dom(document.body).querySelector('#notificationToast');

        if (!this._toast) {
          this._toast = document.createElement('paper-toast');
          this._toast.id = 'notificationToast';
          Polymer.dom(document.body).appendChild(this._toast);
        }
      }
      return this._toast;
    },

    showInfo: function(text) {
      this.toast.text = text;
      this.toast.show();
    }

  };

  // Create an instance of the service and put it into a global variable
  NotificationManager = new NotificationManagerClass();
</script>

Result

So, basically we just instantiate some object, put it in a global variable and assign this variable to a component’s property.

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.

Adding a Library

Currently CUBA Polymer UI module uses Bower as a package manager. Therefore, you can import any library that is published on GitHub.

Imagine that you want to use paper-toggle-button in your project. Its source code is located at https://github.com/PolymerElements/paper-toggle-button.

There are two ways to add this library to your project:

  1. Via command line:

    $ bower install PolymerElements/paper-toggle-button --save
  2. Manually add it to bower.json:

    ...
    "dependencies": {
      ...
      "paper-toggle-button": "PolymerElements/paper-toggle-button"
    },
    ...

The library will be downloaded to bower_components/paper-toggle-button folder and can be used in your code.

By default, Bower uses a default branch of the required dependency. If you want to use another version, specify it after the # sign:

$ bower install PolymerElements/paper-toggle-button#3.0.0-pre.1 --save
. . .