public class BindTooltipAspectDefinition implements LinkkiAspectDefinition {
public static final String NAME = "tooltip";
private final TooltipType tooltipType;
private final String staticValue;
public BindTooltipAspectDefinition(TooltipType tooltipType, String staticValue) {
this.tooltipType = tooltipType;
this.staticValue = staticValue;
}
@Override
public Handler createUiUpdater(PropertyDispatcher propertyDispatcher, ComponentWrapper componentWrapper) {
Aspect<String> aspect = createAspect();
Consumer<String> setter = componentWrapper::setTooltip;
if (tooltipType == TooltipType.STATIC) {
setter.accept(propertyDispatcher.pull(aspect));
return Handler.NOP_HANDLER;
} else {
return () -> setter.accept(propertyDispatcher.pull(aspect));
}
}
public Aspect<String> createAspect() {
if (tooltipType == TooltipType.STATIC) {
return Aspect.of(NAME, staticValue);
} else {
return Aspect.of(NAME);
}
}
public static enum TooltipType {
STATIC,
/**
* Tooltip is bound to the property using the method get<PropertyName>Tooltip().
*/
DYNAMIC;
}
}
Extending linkki
Custom Aspects
In addition to built-in Aspects, you can easily create your own.
Creating a new aspect
This guide walks you through the process of implementing a simple aspect that allows components to show a dynamically bound tooltip. In this example, you will build the aspect as a standalone annotation that can be used in addition to any UI Element Annotation.
A similar aspect is already included in linkki. It is only used as an example due to its simplicity. |
Step 1: Create a new AspectDefinition
An aspect definition defines how an aspect is created.
To define your own definition you have to implement LinkkiAspectDefinition
with the following two methods:
-
initModelUpdate(PropertyDispatcher, ComponentWrapper, Handler)
: Method that registers a listener to the wrapped UI component which react to changes in the UI. This method is only mandatory if the defined aspect needs to write into the model. -
createUiUpdater(PropertyDispatcher, ComponentWrapper)
: Creates aHandler
that is triggered when the UI has to be updated.
We want to define an aspect that can either have a static value or read a value from a get<Property>Tooltip()
method, depending on a TooltipType
enumeration:
-
initModelUpdate
is not needed in this example because the tooltip won’t change in the model upon UI change. -
In the method
createUiUpdater
, thepropertyDispatcher
is asked for the tooltip value which is then set in the component usingComponentWrapper#setTooltip
. ThepropertyDispatcher
needs an aspect to know which value it should retrieve, and how. This aspect is created in the methodcreateAspect()
. -
createAspect()
creates the aspect depending on the values given in the annotation.
Step 2: Create a new annotation
We first create a new annotation that later links the annotated component to our aspect definition:
@Retention(RetentionPolicy.RUNTIME)
@Target(value = { ElementType.METHOD })
public @interface BindTooltip {
TooltipType tooltipType() default TooltipType.STATIC;
String value() default "";
}
-
The annotation must have the retention policy
RetentionPolicy#RUNTIME
to be able to be discovered by linkki at runtime. -
The attribute
tooltipType
determines whether the tooltip is static text or is provided by a method dynamically. -
If the
tooltipType
isBindTooltipType#STATIC
, the attributevalue
allows the user to define the content of the tooltip.
Using the name value for an annotation’s attribute allows users to omit the attribute name if it is the only attribute: @BindTooltip("My tooltip") instead of @BindTooltip(value="My tooltip")
|
Step 3: Connect the annotation with the aspect definition with @LinkkiAspect
Lastly, BindTooltip
has to be annotated with @LinkkiAspect
to tell linkki how to create the aspect from the annotation.
The value for that annotation is a class implementing AspectDefinitionCreator<BindTooltip>
that can be created as an inner class:
@LinkkiAspect(BindTooltipAspectDefintionCreator.class)
public @interface BindTooltip {
class BindTooltipAspectDefintionCreator implements AspectDefinitionCreator<BindTooltip> {
@Override
public LinkkiAspectDefinition create(BindTooltip annotation) {
return new BindTooltipAspectDefinition(annotation.tooltipType(), annotation.value());
}
}
Inheritance
In general, annotations on methods are not inherited. Thus, an aspect annotation has to be annotated again for a overridden method, if the aspect should also be effective in the subclass.
linkki provides a meta annotation for aspect annotations that can be annotated on classes and interfaces to be inherited: @InheritedAspect
.
If an aspect annotation is annotated with @InheritedAspect
, the aspect annotation is also taken into account, although it is only annotated on one of the super classes or interfaces.
Note that this mechanism is not without limitations. For the entire inheritance hierarchy, only one annotation per annotation type is taken into account. If the annotation type appears multiple times, the annotation on the PMO class itself is preferred over the one on the super class, which is preferred over annotations on any of the implemented interfaces.
As the inheritance structure can be very complex, the usage of @InheritedAspect
should be carefully evaluated when defining your own aspect.
Aspects with asynchronously loaded values
If the value of the aspect may take longer to load, the whole UI would be blocked as long as the value is loading.
In this case, it may be sensible to define an aspect that accept CompletableFuture
as return value, making the aspect value retrieval asynchronous.
The resulting aspect would work similarly to UILabel or UITableComponent.
To achieve this, the aspect definition can use the super class FutureAwareAspectDefinition
, which is a subclass of ModelToUiAspectDefinition
that contains additional implementation to handle asynchronous updates.
In its implementation, Server Push is utilized to update the value on completion if the aspect value is assignable to CompletableFuture.class
.
On top of the methods that need to be implemented for ModelToUiApsectDefinition
, subclasses of FutureAwareAspectDefinition
need to provide implementation for the method getValueOnError
additionally.
This method defines how error that occur in the CompletableFuture
should be handled.
Custom future aware components should also set the CSS attribute future-aware
which is available as a constant in LinkkiTheme.ATTR_FUTURE_AWARE
.
This attribute activates the CSS selector that shows a loading indicator if the attribute content-loading
is set.
It also displays the generic error message that is shown when loading the content throws an exception.