Drop Downs and Defaults

Introduction

Invoking an action whose parameters are primitives or values (int, date, string etc) is simple: the user can just type in or use a date picker. Invoking an action with a parameter of reference type (such as Customer or Order) requires the viewer to provide some mechanism by which the end-user can select the relevant instance.

If the list of available options is fixed then the developer can provided a list a choices…​() supporting method (for either and action parameter or when editing a property). These are rendered in a drop-down.

If the list of available options is much larger, then the developer can use an autoComplete…​() supporting method. The user user enters a few characters and this is used to search for matching reference(s), again rendered in a drop-down.

Similarly, when invoking an action, there may well be suitable defaults for the action arguments. For example, if placing an Order then — even if the Product argument might not have a sensible default — the quantity argument could reasonably be defaulted to 1. Or, the Product might indeed have a default, say the product previously placed by this user. The developer indicates this using a default…​() supporting method.

Choices

A drop-down list of choices for a property can be specified using a supporting method matched by prefix, returned datatype and name:

import lombok.Getter;
import lombok.Setter;

public class Customer {

    @Property(editing = Editing.ENABLED)                    (1)
    @Getter @Setter
    private String paymentMethod;

    public List<String> choicesPaymentMethod() {            (2)
        return Arrays.asList("Visa", "Mastercard", "Amex");
    }

    // ...
}
1 If required; properties are by default disabled globally.
2 Note the "choices" prefix and the suffix matching up with the getter. The method must return a collection of the same type as the property.

For an action, the choices for the N-th parameter can be specified by a prefix, the parameter number, with the suffix being the name of the action:

public class Customer {

    public Order invoice(
            ShoppingCart cart,
            String paymentMethod,
            DiscountVoucher voucher,
            LocalDate shipBy) {
        ...
        return this;
    }

    public List<String> choices1Invoice() {         (1)
        return this.getPaymentMethods();
    }

    // ...
}
1 "choices" prefix, N-th param, suffix matches up with the action’s name. Returns a collection of the same type as the parameter.

Defaults

For properties, there is no concept of a default value ; just initialize the field to an appropriate value the object is created. For actions, though, providing a default value for its parameters can substantially improve the user experience.

For an action, the choices for the N-th parameter can be specified by "number":

public class Customer {

    public Order invoice(
            ShoppingCart cart,
            String paymentMethod,
            DiscountVoucher voucher,
            LocalDate shipBy) {
        ...
        return this;
    }

    public String default1Invoice() {       (1)
        return this.getPaymentMethod();     (2)
    }

    // ...
}
1 "default" prefix, N-th param, the suffix matches up with the action’s name.
Returns object of same type as parameter.
2 A common idiom is to return the current value of a property of the object.

AutoComplete

The autocomplete is similar to choices, but accepts a string parameter, to search for matching results. A property for example might have:

public class Order {

    @Property(editing = Editing.ENABLED)                        (1)
    @Getter @Setter
    private Product product;

    public List<Product> autoCompleteProduct(                   (2)
                            @MinLength(2) String search) {      (3)
        return productRepository.findByReferenceOrName(search);
    }

    // ...
}
1 If required; properties are by default disabled globally.
2 "autoComplete" prefix, suffix matches property name.
Returns a collection of the property’s type.
3 The @MinLength(…​) annotation indicates the minimum number of characters that must be entered before a search is initiated.

Actions are very similar. Here the supporting "autoComplete" method is matched by number:

public class Customer {

    public Order invoice(
            ShoppingCart cart,
            String paymentMethod,
            DiscountVoucher voucher,
            LocalDate shipBy) {
        ...
        return this;
    }

    public List<DiscountVoucher> autoComplete2Invoice(      (1)
                    @MinLength(2) String voucherCode) {
        return discountVoucherRepository.findByVoucherCode(this, voucherCode);
    }

    // ...
}
1 "autoComplete" prefix, N-th param, suffix matches action name.
Returns a collection of the parameters type.

An autoComplete method can be used in conjunction with a default method, but it doesn’t make sense to provide both an autoComplete and a choices method.

"Globally" defined drop-downs

Very often the set of available choices depends on the data type of the property/action parameter, rather than the individual property/parameter itself. And similarly the algorithm to search for references again may well depend only on that reference type.

In the case of choices, annotating a class as "bounded" (as in a "bounded" or fixed number of instances) means that a choices drop-down will automatically be defined. For example:

@DomainObject(
    bounded = true
)
public class Product { /* ... */ }

For more on this, see @DomainObject#bounding.

Or, if the data type is an enum, then a drop-down will be provided automatically. A payment method is a good example of this:

public enum PaymentMethod {
    VISA, MASTERCARD, AMEX;
}

Something similar can be achieved for autoComplete. Here the domain object indicates a repository query to execute. For example:

@DomainObject(
    autoCompleteRepository = Customers.class,
    autoCompleteMethod = "findByReferenceOrName"
)
public class Customer { /* ... */ }

with:

Customers.java
@DomainService
public class Customers {
    @Action(semantics=SemanticsOf.SAFE)
    public List<Customer> findByReferenceOrName(@MinLength(3) String refOrName) {
        ...
    }
}

For more on this, see @DomainObject#autoCompleteRepository.

There’s no need for the nominated method to be an actual action; any method of any domain service will do, so long as it accepts a string and returns the correct list.

More on action parameters

Dependent choices

For action it is also possible (in a limited form) to define dependencies between parameters. Specifically, if one parameter is a drop-down choice, then other drop-down choices can be derived from it.

A good example is a category/sub-category:

public ToDoItem categorize(
            Category category,
            Subcategory subcategory) {
    setCategory(category);
    setSubcategory(subcategory);
}

public List<Category> choices0Categorize() {
    return categoryRepository.allCategories();
}
public List<Subcategory> choices1Categorize(        (1)
                                Category category) {
    return subcategoryRepository.findBy(category);
}
1 Returns a list of choices for the 2nd parameter based on the argument provided for the first.

Multi-select parameters

As well as scalar values, action parameters can also be collections. For this to be valid, a choices or autoComplete supporting method must be provided.

For example, suppose we want to "tag" or "label" an object:

public StoryCard tag(List<Tag> tags) {
    getTags().addAll(tags);
}

public List<Tag> autoCompleteTag(@MinLength(1) search) {
    return tagRepository.findByName(search);
}

If the action has been associated with a collection, using @Action#choicesFrom(), then the collection can be used to provide a list of candidate values.

The Web UI (Wicket viewer) handles this by rendering checkboxes against the associated collection; the user can select/deselect these checkboxes and the selected items are taken as the values for the multi-select action.

Alternative Programming Models

If you define an action as a mixin, then there are two other ways in which the supporting methods for parameters can be specified:

  • using the name of the parameter (rather than its number)

  • using a Java record (or static data class) to capture all the argument values, rather than separate parameters.

You may find that these alternatives make for more maintainable code. Check out the mixin docs for more details