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.
@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:
@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.
@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.
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:
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:
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.
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. |
@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;
}