UI Components
Dependency to artifact linkki-search-vaadin-flow is needed.

linkki provides search components that can be used by any product for performing search operations.

The search can be integrated in two ways:

Necessary classes

Regardless of the kind of search, there are several classes that need to be created:

Search parameter

Model object for search parameters

SampleSearchParameters
public class SampleSearchParameters {

    public static final String PROPERTY_PARTNER_NUMBER = "partnerNumber";
    // and other search parameters

    private String partnerNumber;

    public String getPartnerNumber() {
        return partnerNumber;
    }

    public void setPartnerNumber(String partnerNumber) {
        this.partnerNumber = partnerNumber;
    }

}
PMO for search parameter

A PMO for the search parameter input

SampleSearchParametersPmo
public class SampleSearchParametersPmo {

    public static final String SECTOR_FINANCE = "Finanz und Versicherung";
    public static final String SECTOR_TELECOM = "Telekommunikation";

    private final Supplier<SampleSearchParameters> searchParameters;

    private boolean showMore;

    public SampleSearchParametersPmo(Supplier<SampleSearchParameters> searchParameters) {
        this.searchParameters = searchParameters;
        this.showMore = false;
    }

    @ModelObject
    public SampleSearchParameters getSearchParameters() {
        return searchParameters.get();
    }

    // @BindPlaceholder("z.B. B1230984EK")
    @BindAutoFocus
    @UITextField(position = 10, label = "Partnernummer",
            modelAttribute = SampleSearchParameters.PROPERTY_PARTNER_NUMBER)
    public void partnerNumber() {
        // model binding
    }

Annotating the first element with @BindAutoFocus sets the focus on the first input field, giving the user the ability to start typing immediately instead of having to click on the input field first.

Collected search results

Wrapper for a list of search results and messages of possibly occurred errors

SampleSearchResult
public class SampleSearchResult {

    public static final int DEFAULT_MAX_RESULT_SIZE = 100;

    private final List<SampleModelObject> result;
    private final MessageList messages;

    SampleSearchResult(List<SampleModelObject> result, MessageList messages) {
        this.result = result;
        this.messages = messages;
    }

    public List<SampleModelObject> getResult() {
        return result;
    }

    public MessageList getMessages() {
        return messages;
    }

    public int getMaxResult() {
        return DEFAULT_MAX_RESULT_SIZE;
    }

}
Single search result

Model object of a single search result

SampleModelObject
public class SampleModelObject {

    private final String partnerNumber;
    // and other properties

    public SampleModelObject(String partnerNumber) {
        this.partnerNumber = partnerNumber;
    }

    public String getPartnerNumber() {
        return partnerNumber;
    }
}
PMO for a single search result

PMO for visualizing the search result as a row in the result table

SampleSearchResultRowPmo
public class SampleSearchResultRowPmo {

    private SampleModelObject modelObject;
    private List<MenuItemDefinition> additionalActions;

    public SampleSearchResultRowPmo(SampleModelObject result,
            List<MenuItemDefinition> additionalActions) {
        this.modelObject = result;
        this.additionalActions = additionalActions;
    }

    public SampleModelObject getModelObject() {
        return modelObject;
    }

    @UILink(position = 10, label = "PartnerNumber")
    public String getPartnerNumber() {
        return modelObject.getLink();
    }

    public String getPartnerNumberCaption() {
        return modelObject.getPartnerNummber();
    }

    @UISearchResultAction
    public List<MenuItemDefinition> getActions() {
        return additionalActions;
    }
}

The row PMO can be annotated with @UISearchResultAction to define actions for a table row. These actions are available at the end of a row in a menu.

SearchController

The SearchController is responsible for the execution of the actual search. Based on the search type (context-free or context-dependent), its implementation and usage differs. Details can be found within the chapters regarding context-free search and context-dependent search.

Creating the search component

The search component is created using the SearchLayoutBuilder class.

Example implementation of a search component using SearchLayoutBuilder<PARAM, RESULT, MODEL_OBJECT, ROW>
        return SearchLayoutBuilder
                .<SampleSearchParameters, SampleSearchResult, SampleModelObject, SampleSearchResultRowPmo> with()
                .searchParametersPmo(SampleSearchParametersPmo::new)
                .searchResultRowPmo(m -> new SampleSearchResultRowPmo(m, getAdditionalActions(m)),
                                    SampleSearchResultRowPmo::getModelObject,
                                    SampleSearchResultRowPmo.class)
                .searchController(searchController, SampleSearchResult::getResult)
                .primaryAction(ResultActions::navigateToPartner)
                .maxResult(SampleSearchResult.DEFAULT_MAX_RESULT_SIZE)
                .caption("Search for Partners")
                .build();

The "primary action" is executed upon double-clicking the search result row. When using a context-free search, it is common to open the entry in a new page. The common action for a context-dependent search is to select the entry.

While searching on a separate view, it is desirable to have search parameters that are reflected within the URL. Thereby, the search parameters can be preconfigured by the URL, and can also be shared with other by sending the URL.

The class RoutingSearchController provides functionalities to achieve this.

SearchView

Since the search view must be reachable per URL, a @Route annotation is needed. Besides that, both the BeforeEnterObserver or AfterNavigationObserver interface must be implemented to enable URL parameter processing.

@Route(value = ContextFreeSearchView.NAME, layout = PlaygroundAppLayout.class)
public class ContextFreeSearchView extends VerticalLayout implements AfterNavigationObserver {
The Vaadin Navigation Lifecycle documentation proposes the usage of the AfterNavigationEvent to initialise the UI. However, this event prevents redirecting to other sites. In cases where the search result should directly be opened, using the BeforeEnterEvent is preferable.

The content of the view must be a SearchLayoutPmo that is created using SearchLayoutBuilder. Creating a SearchLayoutPmo is independent of the actual search and is described in detail in "Creating the search component".

    private final BindingContext bindingContext = new BindingContext();

    public ContextFreeSearchView() {
        searchController = createSearchController();
        add(VaadinUiCreator.createComponent(createSearchLayoutPmo(), bindingContext));
        setSizeFull();
    }

The search controller must be initialised and needs the value of the @Route annotation as first parameter.

        return new RoutingSearchController<>(ContextFreeSearchView.NAME,
                searchService::search,
                new SampleSearchParametersMapper(),
                SampleSearchResult::getMessages);

Finally, within afterNavigation, the AfterNavigationEvents Location must be used to initialize the RoutingSearchController. The search controller then reads the search parameters of the URL and executes the search if necessary.

The search parameters can be visualised by updating the BindingContext containing the SearchlayoutPmo.
    @Override
    public void afterNavigation(AfterNavigationEvent event) {
        searchController.initialize(event.getLocation());
        bindingContext.modelChanged();
    }
Query parameter mapping

An implementation of SearchParameterMapper must be created to map the search parameters to query parameters and vice versa.

public class SampleSearchParametersMapper implements SearchParameterMapper<SampleSearchParameters> {

The toSearchParameters method creates a search parameter object based on the query parameters:

    public SampleSearchParameters toSearchParameters(Map<String, List<String>> queryParams) {
        var searchParams = new SampleSearchParameters();
        var parameters = ParamsUtil.flatten(queryParams);

        searchParams.setPartnerNumber(parameters.get(SampleSearchParameters.PROPERTY_PARTNER_NUMBER));
        ...

Consequently, toQueryParameters creates query parameters from a given search parameter object:

    public Map<String, List<String>> toQueryParameters(SampleSearchParameters searchParams) {
        var queryParams = new LinkedHashMap<String, String>();

        queryParams.put(SampleSearchParameters.PROPERTY_PARTNER_NUMBER, searchParams.getPartnerNumber());
        ...

In case that every parameter only has one value, ParamsUtil.flatten can be used to use Map<String, String> instead of Map<String, List<String>>. Besides that, ParamsUtil offers some handy format and parse methods.

        queryParams.put(SampleSearchParameters.PROPERTY_DATE_OF_BIRTH,
                        ParamsUtil.formatIsoDate(searchParams.getDateOfBirth()));

        ParamsUtil.parseIsoDate(parameters.get(SampleSearchParameters.PROPERTY_DATE_OF_BIRTH))
                .ifPresent(searchParams::setDateOfBirth);

The class SimpleSearchController can be used in cases where a search on the current view is needed, e.g., within a dialog.

The UI component can be created with a SearchLayoutBuilder.

Example
Dialog
        var searchLayoutPmo = createSearchLayoutPmo(searchController, selection -> {
            if (!validateSelection(selection).containsErrorMsg()) {
                primaryAction.accept(selection);
            }
        });
        ValidationService validationService = () -> searchLayoutPmo.getSelectedResult()
                .map(SampleSearchResultRowPmo::getModelObject)
                .map(this::validateSelection)
                .orElse(createEmptySelectionMessage());
        var searchDialog = new PmoBasedDialogFactory(validationService)
                .newOkCancelDialog("Search for a business partner",
                                   () -> searchLayoutPmo.getSelectedResult()
                                           .map(SampleSearchResultRowPmo::getModelObject)
                                           .ifPresent(primaryAction),
                                   searchLayoutPmo);
SimpleSearchController
        return new SimpleSearchController<>(SampleSearchParameters::new,
                searchService::search,
                SampleSearchResult::getMessages);