Extending Components

You can create a new component by extending any existing component.

For most components, there is a client-side component and a corresponding server-side component:

  • Client-side component: Contains the HTML, CSS, and JavaScript, and defines a set of properties that determine the behavior of the component on the client side.

  • Server-side component: Contains Java code that allows for changing of the client-side properties, and manages the component behavior on the server side.

You can extend a component on either the server or client side. Note that these are alternative approaches that are mutually exclusive.

In this section we demonstrate the two different approaches to achieve the same changes to the prebuilt text field component.

Extending a Component Using the Server-side Approach

Extending a server-side component is useful when you want to add new functionality (as opposed to visual aspects) to an existing component. Suitable examples include automatically processing data, adding default validators, and combining multiple simple components into a field that manages complex data.

Tip
If your component contains a lot of logic that could easily be done on the client side, consider implementing it as a Web Component and creating a wrapper for it. This approach may offer a better user experience and result in less load on the server.

In this example, we create a NumericField component by extending the TextField component. The new component contains a default number that the user can change using + and - controls.

Example: Creating a NumericField component by extending the TextField component.

@CssImport("./styles/numeric-field-styles.css")
public class NumericField extends TextField {

    private Button substractBtn;
    private Button addBtn;

    private static final int DEFAULT_VALUE = 0;
    private static final int DEFAULT_INCREMENT = 1;

    private int numericValue;
    private int incrementValue;
    private int decrementValue;

    public NumericField() {
        this(DEFAULT_VALUE, DEFAULT_INCREMENT,
                -DEFAULT_INCREMENT);
    }

    public NumericField(int value, int incrementValue,
                        int decrementValue) {
        setNumericValue(value);
        this.incrementValue = incrementValue;
        this.decrementValue = decrementValue;

        setPattern("-?[0-9]*");
        setPreventInvalidInput(true);

        addChangeListener(event -> {
            String text = event.getSource().getValue();
            if (StringUtils.isNumeric(text)) {
                setNumericValue(Integer.parseInt(text));
            } else {
                setNumericValue(DEFAULT_VALUE);
            }
        });

        substractBtn = new Button("-", event -> {
            setNumericValue(numericValue +
                    decrementValue);
        });

        addBtn = new Button("+", event -> {
            setNumericValue(numericValue +
                    incrementValue);
        });

        getElement().setAttribute("theme", "numeric");
        styleBtns();

        addToPrefix(substractBtn);
        addToSuffix(addBtn);
    }

    private void styleBtns() {
        // Note: The same as addThemeVariants
        substractBtn.getElement()
                .setAttribute("theme", "icon");
        addBtn.getElement()
                .setAttribute("theme", "icon");
    }

    public void setNumericValue(int value) {
        numericValue = value;
        setValue(value + "");
    }

    // getters and setters
}

As an alternative, you can extend the Composite class that has a minimal API. This hides methods available in the more extensive API that is exposed when your custom components extends an implementation of Component.

Note
The Element API contains methods to update and query various parts of the element, such as the attributes. Every component has a getElement() method that allows you to to access it. See Creating a Component Using Multiple Elements for more.

We import additional styles for the component using the @CssImport annotation. These styles apply only to our NumericField component, and not to all TextField components.

Example: Creating numeric-field-styles.css to customize the appearance of the vaadin-text-field component.

:host([theme~="numeric"]) [part="input-field"] {
    background-color: var(--lumo-base-color);
    border: 1px solid var(--lumo-contrast-30pct);
    box-sizing: border-box;
}

:host([theme~="numeric"]) [part="value"]{
    text-align: center;
}

See Styling Components for more information.

Extending a Component Using the Client-side Approach

Vaadin client-side components are based on Polymer 3 that supports extending existing components. You can use the extends property to extend existing Polymer elements.

There are five ways to inherit a template from another Polymer element:

  1. Inheriting a base class template without modifying it.

  2. Overriding a base class template in a child class.

  3. Modifying a copy of a superclass template.

  4. Extending a base class template in a child class.

  5. Providing template-extension points in a base class for content from a child class.

Extending by Modifying a Copy of a Superclass Template

In this example, we demonstrate how to create a new component by modifying a copy of a superclass template. We build a NumberFieldElement by extending Vaadin.TextFieldElement. The new component contains a default number that the user can change using + and - controls.

It is important to remember that when a component template is extended, the properties and methods of the parent template become available to the child template.

Note
By default, a child component uses the template of the parent component, unless the child component provides its own template by overriding the static getter method template. The parent’s template is accessed using super.template.

Next, specify the element from which the child component inherits. In this case we specify that NumberFieldElement inherits (including the properties and methods) from Vaadin.TextFieldElement:

import {html} from
   '@polymer/polymer/lib/utils/html-tag.js';
import {TextFieldElement} from
   '@vaadin/vaadin-text-field/src/vaadin-text-field.js';

let memoizedTemplate;

class NumberFieldElement extends TextFieldElement {

    static get template() {
        if (!memoizedTemplate) {
            const superTemplate = super.template
                    .cloneNode(true);
            const inputField = superTemplate.content
                .querySelector('[part="input-field"]');
            const prefixSlot = superTemplate.content
            .querySelector('[name="prefix"]');
            const decreaseButton = html`<div
                part="decrease-button"
                on-click="_decreaseValue"></div>`;
            const increaseButton = html`<div
                part="increase-button"
                on-click="_increaseValue"></div>`;
            inputField.insertBefore(
                decreaseButton.content, prefixSlot);
            inputField.appendChild(
                increaseButton.content);
            memoizedTemplate = html`<style>
                 [part="decrease-button"]::before {
                   content: "−";
                 }

                 [part="increase-button"]::before {
                   content: "+";
                 }
               </style>
               ${superTemplate}`;
        }
        return memoizedTemplate;
    }

    static get is() {
        return 'vaadin-number-field';
    }

    static get properties() {
        return {
            decrementValue: {
              type: Number,
              value: -1,
              reflectToAttribue: true,
              observer: '_decrementChanged'
            },
            incrementValue: {
              type: Number,
              value: 1,
              reflectToAttribue: true,
              observer: '_incrementChanged'
            }

            // Note: the value is stored in the
            // TF's value property.
        };
    }

    _decreaseValue() {
        this.__add(this.decrementValue);
    }

    _increaseValue() {
        this.__add(this.incrementValue);
    }

    __add(value) {
        this.value = parseInt(this.value, 10) + value;
        this.dispatchEvent(
            new CustomEvent('change', {bubbles: true}));
    }

    _valueChanged(newVal, oldVal) {
        this.value = this.focusElement.value;
        super._valueChanged(this.value, oldVal);
    }

    /* ... */
}

To modify the template we override the template static getter. Note that the expression ${super.template} inserts the base class template into the newly constructed template. The newly constructed template is memoized for further invocations of template.

See Inherit a template from another Polymer element in the Polymer documentation for more.