Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Java records for form bindings #17947

Closed
diversit opened this issue Oct 30, 2023 · 3 comments
Closed

Support Java records for form bindings #17947

diversit opened this issue Oct 30, 2023 · 3 comments

Comments

@diversit
Copy link

Describe your motivation

Java records has been GA since Java 16. Since a record is immutable it is more suitable for a functional or 'data driven' coding style which Oracle is now more propagandising now there is better support for it in Java 21 (e.g. switch expressions).
Furthermore it greatly reduces the required code since it automatically provides getters, equals, toString, hashcode, etc.

Unfortunately Vaadin does not seem to support Java records yet.
There is this issue #16879 regarding support for serialising Java records.
Binding a record to form fields is also not supported yet since the binder looks for getter and setter methods in an object.

Describe the solution you'd like

Support Java records in a Binder where the Binder is able to provide a instance of the record with the contents of the bound fields when all fields are valid.

Currently when using a Java Bean, a instance of the bean must be provided and the Binder can then write into the bean.

var binder = new Binder<>(MyBean.class);
// binding binder to form fields
var myBean = new MyBean();
binder.writeBean(myBean);

Since a Java record has no default constructor, it would be nicer if the binder could provide an instance of the record with all fields.

var binder = new Binder<>(MyRecord.class);
// binding binder to form fields
var myRecord = binder.writeRecord();

When creating a record, the binder should be smart enough to create an instance of a record for all bound form fields.
When reading a record, to fill the form, the binder should be smart enough to use the records property methods instead of the Java Bean getter methods.

Describe alternatives you've considered

Trying Java records with Vaadin 24.x throws an exception since it cannot find a getter method for a property.
Looking into the Binder.java sources it looked heavily tied to Java Bean and not easy to customise for records.

Additional context

Java records are already supported in many frameworks like Spring, Hibernate, etc. It would be a pity if Vaadin would not support this Java language feature.

@TatuLund
Copy link
Contributor

TatuLund commented Feb 5, 2024

It is possible to read data from Record with Binder, but naturally it is not supporting introspecting it using new Binder<>(MyRecord.class), neither it does have method to generate a new record with current values of the bound fields.

So one could envision to apply it for reactive views in some fashion like below.

    public BinderRecord() {
        Span nameSpan = new Span();
        nameSpan.addClassNames(LumoUtility.FontWeight.BOLD,
                LumoUtility.Margin.Right.SMALL);
        ReadOnlyHasValue<String> name = new ReadOnlyHasValue<>(
                nameSpan::setText);
        Span ageSpan = new Span();
        ReadOnlyHasValue<Integer> age = new ReadOnlyHasValue<>(
                value -> ageSpan.setText(value.toString()));

        Binder<Person> binder = new Binder<>();
        binder.forField(name).bind(person -> person.name(), null);
        binder.forField(age).bind(person -> person.age(), null);

        Person person = new Person("John", 25);
        binder.readBean(person);
        add(nameSpan, ageSpan);

    }

The above trivial example makes it look quite clumsy though.

I would like to use this example to provoke discussion about the actual use cases for Records with Binder. I feel that immutable Records are not the best fit for DTO's with two - way data transfer use case.

@Sheikah45
Copy link

I would like to use this example to provoke discussion about the actual use cases for Records with Binder. I feel that immutable Records are not the best fit for DTO's with two - way data transfer use case.

I have been using Vaadin in a production environment for a while and I think that binding to records is most naturally a fit for CustomField implementations similar to the following.

public class DateRangeField extends CustomField<DateRange> {

    private final Binder<DateRange> binder = new Binder<>();

    public DateRangeField() {
        DatePicker startDatePicker = new DatePicker();
        DatePicker endDatePicker = new DatePicker();
        add(new HorizontalLayout(startDatePicker, endDatePicker));

        binder.forField(startDatePicker).asRequired().bind(DateRange::start, "start");
        binder.forField(endDatePicker).asRequired().bind(DateRange::end, "end");
        
        binder.withValidator(Validator.from(dateRange -> !dateRange.start().isAfter(dateRange.end()), "Start cannot be after end"));

        binder.addValueChangeListener(event -> updateValue);
    }

    @Override
    protected DateRange generateModelValue() {
        return binder.createBeanIfValid();
    }

    @Override
    protected void setPresentationValue(DateRange dateRange) {
        binder.readBean(dateRange);
    }
}

public record DateRange(LocalDate start, LocalDate end) {
    public DateRange {
        Objects.requireNonNull(start);
        Objects.requireNonNull(end);
        if (start.isAfter(end)) {
            throw new IllegalArgumentException("Start cannot be after end");
        }
    }
}

In this case binding to record with validation can reduce a significant amount of boilerplate where currently you would need to have separate validation logic that executes in the generateModelValue and then extract the fields to construct the value object. The binder helps to centralize the object creation logic and makes it much more clear what the conditions are.

The concept of using an immutable object to represent the value of a custom field already fits in nicely with many of the provided HasValue classes that come in vaadin-core. Like the DatePicker where you can set custom validation like min or max date or format. If any of these validation constraints are violated then the empty value is just returned instead and if it is valid the immutable value is returned so you don't have to worry about the value changing under you. This natural immutability is inherent in almost all of the basic provided HasValue classes and it would be great to easily provide this in user defined custom field classes

As it currently stands to perform validation you need a mutable object. If you use this mutable object and just set it as the object for the binder then it is possible that after calling getValue on your custom field that the object could change as it proceeds through the backend if the user continues to interact with the widget and you have not taken the necessary precautions to prevent mutation.

A record binder would greatly help to promote a better pattern of usage with immutability that naturally matches custom fields and can easily encourage best practices.

One of the biggest challenges I for see is the actual binding syntax and enabling a form of type safety while limiting redundancy. In the example above I suggested a bind method of the form bind(Function<RECORD, VALUE> accessor, String componentName) so that you can achieve type safety while also providing the name of the component that can be used to reflectively construct the record. Granted it may be possible to use only the accessor and some deep magic with SerializedLambda's but I am not sure that can be assumed stable and reliable in the long run.

@tepi
Copy link
Contributor

tepi commented Aug 27, 2024

Closing this as implementation was merged in #19806

@tepi tepi closed this as completed Aug 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: September 2024 (24.5)
Development

No branches or pull requests

5 participants