Extending linkki

User-created layout annotation type

linkki offers some standard layout annotations which are sufficient for the majority of use cases. Sometimes, a UI requires a particular layout. For this purpose one can either work with Vaadin layouts directly or create a new layout annotation type that can be used like @UISection.

This chapter shows the steps of creating the custom annotation type @UIHorizontalLayout, which differs from a @UISection with Horizontal Layout because it does not contain a section header and positions the labels above the fields.

horizontallayout
@UIHorizontalLayout is also included as standard annotation. This example is a slightly simplified version of the standard annotation.

The @UIHorizontalLayout annotation can be used to annotate a PMO as shown in the following listing:

Usage of @UIHorizontalLayout
@UIHorizontalLayout
public class HotelSearchPmo {

    private static final SecureRandom SECURE_RANDOM = new SecureRandom();
    private int noOfGuests;
    private LocalDate arrival;
    private LocalDate depature;

    @UIIntegerField(position = 10, label = "Number of Guests")
    public int getNoOfGuests() {
        return noOfGuests;
    }

    public void setNoOfGuests(int noOfGuests) {
        this.noOfGuests = noOfGuests;
    }

    ...
}

Annotation Type

The next listing shows the annotation type for the horizontal layout. The relevant concepts are discussed below.

Implementation of the annotation type UIHorizontalLayout
@Retention(RUNTIME) (1)
@Target(TYPE) (1)
@LinkkiComponent(HorizontalComponentDefinitonCreator.class) (2)
@LinkkiLayout(HorizontalLayoutDefinitionCreator.class) (3)
@LinkkiBoundProperty(EmptyPropertyCreator.class) (4)
public @interface UIHorizontalLayout {

}
1 Just like any other Java annotation type @UIHorizontalLayout has to specify @Retention and @Target. More detailed information about annotation types can be found at the Oracle Docs.
2 The @LinkkiComponent meta-annotation specifies which ComponentDefinitionCreator class is used for @UIHorizontalLayout. Its purpose is to define how the actual UI layout is created.
3 The @LinkkiLayout meta-annotation specifies which LayoutDefinitionCreator to use. A LayoutDefinitionCreator creates a LayoutDefinition that defines how UI elements are added to the layout.
4 Finally, the @LinkkiBoundProperty meta-annotation specifies which PropertyCreator to use.
The custom layout annotation uses the same Creator / Definition pattern that is used for custom UI elements.

Component Definition

The next listing shows the HorizontalComponentDefinitonCreator that returns the LinkkiComponentDefinition using a lambda expression.

Implementation of the HorizontalComponentDefinitonCreator
    public static class HorizontalComponentDefinitonCreator
            implements ComponentDefinitionCreator<UIHorizontalLayout> {

        @Override
        public LinkkiComponentDefinition create(UIHorizontalLayout annotation, AnnotatedElement annotatedElement) {
            return (p) -> new HorizontalLayout();
        }
    }

The purpose of the LinkkiComponentDefinition is to define how the actual Vaadin HorizontalLayout object is created. In this case it just needs to return a new HorizontalLayout instance. Due to the simplicity in this case a lambda expression is used to implement the LinkkiComponentDefinition. Since the HorizontalComponentDefinitonCreator gets a reference to the UIHorizontalLayout annotation, it can access its annotation type elements and use the values within the LinkkiComponentDefiniton. Using this mechanism it is possible to influence the layout at the creation time through annotation type elements.

Layout Definition

The HorizontalLayoutDefinitionCreator that returns the LinkkiLayoutDefinition is implemented by a lambda expression and is shown in the next listing:

Implementation of the HorizontalLayoutDefinitionCreator
    public static class HorizontalLayoutDefinitionCreator implements LayoutDefinitionCreator<UIHorizontalLayout> {

        @Override
        public LinkkiLayoutDefinition create(UIHorizontalLayout annotation, AnnotatedElement annotatedElement) {
            return (pc, pmo, bc) -> createChildren(pc, pmo, bc);
        }

        private void createChildren(Object parentComponent, Object pmo, BindingContext bindingContext) {
            HorizontalLayout horizonalLayout = (HorizontalLayout)parentComponent;
            horizonalLayout.setDefaultVerticalComponentAlignment(Alignment.BASELINE);

            UiCreator.createUiElements(pmo, bindingContext,
                                       c -> new LabelComponentWrapper((Component)c, WrapperType.COMPONENT))
                    .forEach(w -> horizonalLayout.add(w.getComponent()));
        }
    }

A LinkkiLayoutDefinition defines how child components are added to the Layout, while the LayoutDefinitionCreator creates the LinkkiLayoutDefinition, as the name suggests. As LinkkiLayoutDefinition is a FunctionalInterface it can be created using a lambda expression, as done in the HorizontalLayoutDefinitionCreator. If necessary, the HorizontalLayoutDefinitionCreator can pass information from the UIHorizontalLayout annotation to the LinkkiLayoutDefintion.

Within the HorizontalLayoutDefinitionCreator#createChildren() method the child UI elements are created and afterwards added to the HorizontalLayout. Fortunately it is possible to reuse standard linkki functionality for this purpose. The UiCreator is utilized to create a Stream of UI components from the PMO, while the CaptionComponentWrapper takes the Label of each UI component and adds it to the component as caption. After receiving the stream of components from the UiCreator the components can simply be added to the HorizontalLayout.

Bound Property

The scope of the layout annotation is the PMO as a whole. Per convention the BoundProperty of a PMO itself is empty. Unlike a BoundProperty of a PMO element, as discussed here, a BoundProperty of a PMO does not have a model object or attribute. As a result of these characteristics the EmptyPropertyCreator that creates an empty BoundProperty can be used for the UIHorizontalLayout annotation and other layout annotations.

The convention that the BoundProperty of a PMO is empty comes into play as well when the @BindTooltip(tooltipType = TooltipType.DYNAMIC) annotation is used: If a BoundProperty is annotated with @BindTooltip(tooltipType = TooltipType.DYNAMIC) linkki searches for the method get<bound-property-name>Tooltip() to retrieve the content for the tooltip. Since the name of the BoundProperty is empty per convention for the whole PMO, linkki will search for the method getTooltip() if the PMO is annotated with @BindTooltip(tooltipType = TooltipType.DYNAMIC).

Using the PMO

The next listing shows how a PMO that is annotated with the UIHorizontalLayout annotation can be added to a Page:

Usage of an PMO that is annotated with @UIHorizontalLayout
        add(VaadinUiCreator.createComponent(new HotelSearchPmo(), new BindingContext()));

VaadinUiCreator#createComponent creates Vaadin components from PMO objects and binds them to the passed BindingContext.

Using slots

Layouts can also define slots which can be filled with UI elements using the @BindSlot annotation. One way to do this is by using Lit templates. Creating a Lit template works similarly to the previously described definition of a custom UI layout with the difference that the ComponentDefinitionCreator returns a Lit template Java class.

A Lit template is basically described within a TypeScript file which specifies the layout including CSS styles and available slots.

TypeScript Lit template which defines two available slots
class SampleSlotLayout extends LitElement {

    static styles = css`
        :host {
            width: 100%;
            height: 100%;
        }

        ::slotted([slot="right-slot"]) {
            padding-right: 32px;
        }
        
        #slot-container {
            display: flex;
        }
        
        .right-slot-container {
            flex-grow: 1;
            display: flex;
            justify-content: end;
        }
    `;

    render() {
        return html`
            <vaadin-horizontal-layout id="slot-container">
                <div>
                    <slot name="left-slot"></slot>
                </div>
                <div class="right-slot-container">
                    <slot name="right-slot"></slot>
                </div>
            </vaadin-horizontal-layout>
        `;
    }
}

customElements.define('sample-slot-layout', SampleSlotLayout);

This template can be applied to a Java class which extends LitTemplate by using the @Tag and @JsModule annotations. Afterwards, this layout class can be provided by a ComponentDefinitionCreator.

If the Lit template defines slot elements, the names of these slots can be added to the class as static attributes in order to set the slots by using the @BindSlot annotation.
Lit template Java class with two slots
@Tag("sample-slot-layout")
@JsModule("./layouts/sample-slot-layout.ts")
public class BindSlotLayout extends LitTemplate {

    public static final String SLOT_LEFT = "left-slot";
    public static final String SLOT_RIGHT = "right-slot";

    private static final long serialVersionUID = 1L;
}