All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dellroad.stuff.vaadin24.field.BinderCustomField Maven / Gradle / Ivy

The newest version!

/*
 * Copyright (C) 2022 Archie L. Cobbs. All rights reserved.
 */

package org.dellroad.stuff.vaadin24.field;

import com.vaadin.flow.component.AbstractField;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HasValidation;
import com.vaadin.flow.component.customfield.CustomField;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.data.binder.BeanValidationBinder;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.binder.ValidationException;
import com.vaadin.flow.data.binder.ValidationResult;
import com.vaadin.flow.data.binder.ValueContext;

import java.time.LocalDate;
import java.util.Objects;

import org.dellroad.stuff.vaadin24.util.VaadinUtil;
import org.dellroad.stuff.vaadin24.util.WholeBeanValidator;

/**
 * Support superclass for {@link CustomField}s containing sub-fields that are managed by an internal {@link Binder}.
 *
 * 

* As with many {@link CustomField}'s, this class this allows editing a complex value type as a single bound * property using multiple sub-fields. A common example is a date range value, which is a composite of two {@link LocalDate} * values representing start and end date. * *

* The purpose of this class is to add an internal {@link Binder} so that the individual sub-fields can be properly * (and separately) validated. This class also includes support for validating the overall value, for example, * requiring the date range start date to be prior to the end date. * *

* * *

Binding * *

* Although a {@link BinderCustomField} may be bound as a field to a property in some larger containing class' {@link Binder}, * each {@link BinderCustomField} also contains its own internal {@link Binder} to which its private sub-fields are bound. * *

* Subclasses can customize this internal {@link Binder} by overriding {@link #createBinder}. * *

Field Value * *

* The value of a {@link BinderCustomField} is generated by {@linkplain #createNewBean creating a new bean instance} and then * applying the values of the internal sub-fields to it. * *

* In order for this class to create new instances, its value type must have a public zero-arg constructor; otherwise, * the subclass must override {@link #createNewBean}. * *

* In any case, the value of a {@link BinderCustomField} remains {@linkplain #getEmptyValue its empty value} * as long as any of its sub-fields remain in an invalid state according to the internal {@link Binder}. * *

Binders and Validation * *

* Individual sub-fields are validated normally using the internal {@link Binder}; however, this guarantees only that * the sub-fields are valid. It does not validate this {@link BinderCustomField}'s value, which is derived from a combination * of the sub-fields; normally, that's the responsibilty of some outer {@link Binder}, not the internal {@link Binder}. * *

* Subclasses can override {@link #validate validate()} to implement "whole bean" validation contraints on * this {@link BinderCustomField}'s value. However, for whole bean validation to have any effect, some code must register * the validation provided by {@link #validate validate()} to the outer binding. This happens automatically when this * field is created by a {@link FieldBuilder}, or by any other mechanism which recognizes the {@link ValidatingField} interface * (which this class implements). In particular, it's possible to recursively nest {@link BinderCustomField}'s using * {@link FieldBuilder} annotations and have proper validation at each level. * *

* See also {@link WholeBeanValidator} for another way to do "whole bean" validation using JSR 303 validation constraints. * *

Layout * *

* For layout, {@link BinderCustomField} simply concatenates the sub-fields into a {@link HorizontalLayout}. * Subclasses can customize this behavior by overriding {@link #layoutComponents}. * *

Example * *

* See {@link FieldBuilderCustomField} for an example. * * @param field value type */ @SuppressWarnings("serial") public abstract class BinderCustomField extends CustomField implements ValidatingField, T>, T> { /** * The field value type. */ protected final Class modelType; /** * The binder that is bound to this instance's sub-fields. */ protected final Binder binder; /** * Whether any sub-fields currently have a validation error. */ protected boolean subfieldValidationErrors; // Handle setPresentationValue() being invoked prior to initialize() if initialization is delayed private InitState initState = InitState.INITIAL; private T initialValue; // Constructors /** * Constructor. * * @param modelType field value type * @throws IllegalArgumentException if {@code modelType} is null */ public BinderCustomField(Class modelType) { // Sanity check if (modelType == null) throw new IllegalArgumentException("null modelType"); // Initialize this.modelType = modelType; this.binder = this.createBinder(); this.initialize(); // When any bound sub-field changes, recalculate this field's value this.binder.addValueChangeListener(e -> this.updateValue()); // Whenever any sub-field becomes invalid, remove our error message (if any) to avoid clutter this.binder.addStatusChangeListener(e -> { this.subfieldValidationErrors = e.hasValidationErrors(); if (this.subfieldValidationErrors) this.setErrorMessage(null); }); } /** * Get the model type. */ public Class getModelType() { return this.modelType; } // Subclass Methods /** * Initialize this instance. * *

* The implementation in {@link BinderCustomField} invokes {@link #createAndBindFields} and then {@link #layoutComponents}. * *

* Note: this method is invoked from the constructor, so any subclass constructor initialization will not have been done yet. * Subclasses can fix this by overriding this method and invoking * {@link VaadinUtil#accessCurrentSession VaadinUtil.accessCurrentSession}{@code (() -> super.initialize())}. */ protected void initialize() { if (this.initState.equals(InitState.INITIALIZED)) throw new IllegalStateException("duplicate initialization"); this.createAndBindFields(); this.layoutComponents(); final boolean hasInitialValue = this.initState.equals(InitState.INITIAL_VALUE); this.initState = InitState.INITIALIZED; if (hasInitialValue) { this.setPresentationValue(this.initialValue); this.initialValue = null; } } /** * Create a new {@link Binder} for the given type. * *

* The implementation in {@link BinderCustomField} delegates to {@link Binder#Binder(Class)}. * Subclasses can override this method to substitute {@link BeanValidationBinder}, configure additional validators, etc. * *

* Note: this method is invoked from the constructor. * * @return field builder */ protected Binder createBinder() { return new Binder<>(this.modelType); } /** * Create this field's sub-fields and bind them to {@link #binder}. */ protected abstract void createAndBindFields(); /** * Layout components required for this field. * *

* The implementation in {@link BinderCustomField} iterates the bound fields in {@link #binder} * into a new {@link HorizontalLayout} which is then added to this instance, assuming all fields * are also actually {@link Component}'s. * *

* Subclasses can override this method to add decoration and/or layout differently. */ protected void layoutComponents() { final HorizontalLayout layout = new HorizontalLayout(); this.add(layout); this.binder.getFields() .map(Component.class::cast) .forEach(layout::add); } /** * Create a new instance of the bean model class. * *

* This value is used to create beans to be populated with sub-field values and returned from {@link #generateModelValue}. * *

* The implementation in {@link BinderCustomField} attempts to invoke a default constructor for the bean class. * * @return new empty bean */ protected T createNewBean() { try { return this.modelType.getConstructor().newInstance(); } catch (ReflectiveOperationException e) { throw new RuntimeException(String.format("unexpected error invoking %s constructor", this.modelType), e); } } // ValidatingField /** * {@inheritDoc} * *

* The implementation in {@code BinderCustomField} always returns {@link ValidationResult#ok}. */ @Override public ValidationResult validate(T value, ValueContext ctx) { return ValidationResult.ok(); } // HasEnabled /** * {@inheritDoc} * *

* The implementation in {@code BinderCustomField} delegates to the superclass and then removes * any error message from sub-fields that implement {@link HasValidation}. */ @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); if (enabled) this.updateValue(); // trigger validation to (re)populate error messages else { this.binder.getFields() .filter(HasValidation.class::isInstance) .map(HasValidation.class::cast) .forEach(field -> { field.setInvalid(false); field.setErrorMessage(null); }); } } // CustomField /** * Sets an error message to the component. * *

* The implementation in {@link BinderCustomField} delegates to the overridden superclass method * except when {@code errorMessage} is not null and any sub-field currently has a validation error. * This is to avoid clutter in the display. */ @Override public void setErrorMessage(String errorMessage) { if (errorMessage == null || !this.subfieldValidationErrors) super.setErrorMessage(errorMessage); } /** * {@inheritDoc} * *

* The implementation in {@link BinderCustomField} returns {@linkplain #getEmptyValue the empty value} if * the internal binder is invalid, otherwise {@linkplain #createNewBean a newly created bean instance} populated * with the current sub-fields' values. */ @Override protected T generateModelValue() { final T bean = this.createNewBean(); try { this.binder.writeBean(bean); } catch (ValidationException e) { return this.getEmptyValue(); } return bean; } /** * {@inheritDoc} * *

* The implementation in {@link BinderCustomField} updates the sub-fields from {@code value} unless it is equal to * {@linkplain #getEmptyValue the empty value}, in which case from {@linkplain #createNewBean a newly created bean instance}. */ @Override protected void setPresentationValue(T value) { switch (this.initState) { case INITIAL: this.initialValue = value; this.initState = InitState.INITIAL_VALUE; return; case INITIAL_VALUE: this.initialValue = value; return; default: break; } if (!Objects.equals(value, this.getEmptyValue())) this.binder.readBean(value); else this.binder.removeBean(); } // InitState private enum InitState { INITIAL, // initial state INITIAL_VALUE, // setPresentationValue() has been invoked, but not initialize() yet INITIALIZED; // initialize() has been invoked } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy