diff --git a/README.md b/README.md index f52524b8..c379aa88 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ This is the relevant dependency: org.vaadin.miki superfields - 0.9.1 + 0.9.2 ``` @@ -58,7 +58,9 @@ The author of the majority of the code is Miki, but this project would not be po * Gerald Koch * Sebastian Kühnau * Jean-François Lamy +* Erik Lumme * Simon Martinelli +* Dmitry Nazukin * Stefan Penndorf * Stuart Robinson * Kaspar Scherrer @@ -68,3 +70,5 @@ The author of the majority of the code is Miki, but this project would not be po ## Small print All components are provided "as is", with no warranty or liability. See license for details. + +This library is not officially supported or endorsed by Vaadin and is not part of the Vaadin Platform. diff --git a/demo-v14/pom.xml b/demo-v14/pom.xml index b2416634..021985f1 100644 --- a/demo-v14/pom.xml +++ b/demo-v14/pom.xml @@ -4,11 +4,11 @@ superfields-parent org.vaadin.miki - 0.9.1 + 0.9.2 superfields-demo-v14 - 0.9.1 + 0.9.2 V14 demo app for SuperFields Showcase application for V14 and SuperFields. war @@ -23,7 +23,7 @@ org.vaadin.miki superfields - 0.9.1 + 0.9.2 javax.servlet @@ -38,7 +38,11 @@ slf4j-simple 1.7.30 - + + org.atteo.classindex + classindex + 3.10 + com.vaadin diff --git a/demo-v14/src/main/java/org/vaadin/miki/DemoComponentFactory.java b/demo-v14/src/main/java/org/vaadin/miki/DemoComponentFactory.java index 922b807f..1ae18dc9 100644 --- a/demo-v14/src/main/java/org/vaadin/miki/DemoComponentFactory.java +++ b/demo-v14/src/main/java/org/vaadin/miki/DemoComponentFactory.java @@ -1,381 +1,87 @@ package org.vaadin.miki; -import com.vaadin.flow.component.BlurNotifier; import com.vaadin.flow.component.Component; -import com.vaadin.flow.component.FocusNotifier; import com.vaadin.flow.component.HasValue; -import com.vaadin.flow.component.UI; -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.button.ButtonVariant; -import com.vaadin.flow.component.checkbox.Checkbox; -import com.vaadin.flow.component.combobox.ComboBox; import com.vaadin.flow.component.html.Div; -import com.vaadin.flow.component.html.Paragraph; import com.vaadin.flow.component.html.Span; -import com.vaadin.flow.component.icon.Icon; -import com.vaadin.flow.component.icon.VaadinIcon; -import com.vaadin.flow.component.notification.Notification; -import com.vaadin.flow.component.orderedlayout.FlexComponent; -import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; -import com.vaadin.flow.component.radiobutton.RadioButtonGroup; -import com.vaadin.flow.component.textfield.TextField; -import com.vaadin.flow.component.textfield.TextFieldVariant; -import com.vaadin.flow.function.SerializableBiConsumer; +import com.vaadin.flow.data.binder.Validator; +import com.vaadin.flow.internal.ReflectTools; +import org.atteo.classindex.ClassIndex; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.vaadin.miki.events.state.StateChangeNotifier; -import org.vaadin.miki.events.text.TextSelectionNotifier; -import org.vaadin.miki.markers.CanReceiveSelectionEventsFromClient; -import org.vaadin.miki.markers.CanSelectText; -import org.vaadin.miki.markers.HasDatePattern; -import org.vaadin.miki.markers.HasLocale; -import org.vaadin.miki.markers.WithNullValueOptionallyAllowed; -import org.vaadin.miki.shared.dates.DatePattern; -import org.vaadin.miki.shared.dates.DatePatterns; -import org.vaadin.miki.superfields.buttons.MultiClickButton; -import org.vaadin.miki.superfields.buttons.SimpleButtonState; -import org.vaadin.miki.superfields.dates.SuperDatePicker; -import org.vaadin.miki.superfields.dates.SuperDateTimePicker; -import org.vaadin.miki.superfields.gridselect.GridSelect; -import org.vaadin.miki.superfields.itemgrid.ItemGrid; -import org.vaadin.miki.superfields.lazyload.ComponentObserver; -import org.vaadin.miki.superfields.lazyload.LazyLoad; -import org.vaadin.miki.superfields.lazyload.ObservedField; -import org.vaadin.miki.superfields.numbers.AbstractSuperNumberField; -import org.vaadin.miki.superfields.numbers.SuperBigDecimalField; -import org.vaadin.miki.superfields.numbers.SuperDoubleField; -import org.vaadin.miki.superfields.numbers.SuperIntegerField; -import org.vaadin.miki.superfields.numbers.SuperLongField; -import org.vaadin.miki.superfields.tabs.SuperTabs; -import org.vaadin.miki.superfields.tabs.TabHandler; -import org.vaadin.miki.superfields.tabs.TabHandlers; -import org.vaadin.miki.superfields.text.SuperTextArea; -import org.vaadin.miki.superfields.text.SuperTextField; -import org.vaadin.miki.superfields.unload.UnloadObserver; +import org.vaadin.miki.demo.ComponentProvider; +import org.vaadin.miki.demo.ContentBuilder; +import org.vaadin.miki.demo.NeedsValidatorStorage; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.demo.ValidatorStorage; import java.io.Serializable; -import java.time.LocalDate; -import java.time.LocalDateTime; +import java.lang.reflect.InvocationTargetException; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.LinkedHashMap; -import java.util.Locale; import java.util.Map; -import java.util.function.Consumer; -import java.util.function.Supplier; -import java.util.stream.Collectors; +import java.util.stream.StreamSupport; /** - * Stores information about components to demo. + * Builds demo pages for components. * @author miki * @since 2020-07-04 */ public final class DemoComponentFactory implements Serializable { - private static final int NOTIFICATION_TIME = 1500; + private static final Logger LOGGER = LoggerFactory.getLogger(DemoComponentFactory.class); - public static DemoComponentFactory get() { - return new DemoComponentFactory(); - } + private static final Comparator> CLASS_ORDER_COMPARATOR = Comparator.comparingInt(t -> t.isAnnotationPresent(Order.class) ? t.getAnnotation(Order.class).value() : Integer.MAX_VALUE - t.getSimpleName().charAt(0)); - private static Component generateParagraph(Class type, int row, int column) { - Paragraph result = new Paragraph(type.getSimpleName()); - result.setTitle(String.format("row %d, column %d", row, column)); - return result; + private static boolean isNotInterface(Class type) { + return !type.isInterface(); } - private static Component generateDiv(Class type, int row, int column) { - return new LazyLoad
(() -> { - final Div result = new Div(); - result.addClassNames("item-grid-cell"); - result.add(new Span(String.format("Row: %d. Column: %d.", row, column))); - final TextField text = new TextField("Class name: ", type.getSimpleName()); - text.setValue(type.getSimpleName()); - text.addClassName("highlighted"); - text.addBlurListener(event -> text.setValue(type.getSimpleName())); - result.add(text); - return result; - }).withId(type.getSimpleName()+"-"+row+"-"+column); + public static DemoComponentFactory get() { + return new DemoComponentFactory(); } private final Map, Component> components = new LinkedHashMap<>(); - private final Map, SerializableBiConsumer>> contentBuilders = new LinkedHashMap<>(); - - private DemoComponentFactory() { - this.components.put(SuperIntegerField.class, new SuperIntegerField(null, "Integer:").withMaximumIntegerDigits(6).withHelperText("(6 digits)")); - this.components.put(SuperLongField.class, new SuperLongField(null, "Long:").withMaximumIntegerDigits(11).withId("long").withHelperText("(11 digits)")); - this.components.put(SuperDoubleField.class, new SuperDoubleField(null, "Double:").withMaximumIntegerDigits(8).withMaximumFractionDigits(4).withHelperText("(8 + 4 digits)")); - this.components.put(SuperBigDecimalField.class, new SuperBigDecimalField(null, "Big decimal:").withMaximumIntegerDigits(12).withMaximumFractionDigits(3).withMinimumFractionDigits(1).withId("big-decimal").withHelperText("(12 + 3 digits)")); - this.components.put(SuperDatePicker.class, new SuperDatePicker("Pick a date:").withDatePattern(DatePatterns.YYYY_MM_DD).withValue(LocalDate.now()).withHelperText("(default date pattern is YYYY-MM-DD)")); - this.components.put(SuperDateTimePicker.class, new SuperDateTimePicker("Pick a date and time:").withDatePattern(DatePatterns.M_D_YYYY_SLASH).withValue(LocalDateTime.now()).withHelperText("(default date pattern is month/day/year)")); - this.components.put(SuperTextField.class, new SuperTextField("Type something:").withPlaceholder("(nothing typed)").withId("super-text-field").withHelperText("(anything goes)")); - this.components.put(SuperTextArea.class, new SuperTextArea("Type a lot of something:").withPlaceholder("(nothing typed)").withId("super-text-area").withHelperText("(anything goes)")); - this.components.put(SuperTabs.class, new SuperTabs((Supplier) HorizontalLayout::new) - .withTabContentGenerator(s -> new Paragraph("Did you know? All SuperFields are "+s)) - .withItems( - "Java friendly", "Super-configurable", "Open source", - "Fun to use", "Reasonably well documented" - ).withId("super-tabs") - ); - this.components.put(ObservedField.class, new ObservedField()); - this.components.put(MultiClickButton.class, new MultiClickButton( - event -> UI.getCurrent().navigate(""), - new SimpleButtonState("Click to navigate to Info Page").withThemeVariant(ButtonVariant.LUMO_PRIMARY), - new SimpleButtonState("Are you sure?", VaadinIcon.INFO_CIRCLE.create()), - new SimpleButtonState("Really navigate away?", VaadinIcon.INFO.create()).withThemeVariant(ButtonVariant.LUMO_ERROR) - ).withId("multi-click-button")); - final GridSelect gridSelect = new GridSelect(SuperFieldsGridItem.class, true); - this.components.put(GridSelect.class, gridSelect); // note: extra config below to include all fields - this.components.put(ComponentObserver.class, new ComponentObserver()); - this.components.put(UnloadObserver.class, UnloadObserver.get().withoutQueryingOnUnload()); - this.components.put(ItemGrid.class, new ItemGrid>( - null, - () -> { - VerticalLayout result = new VerticalLayout(); - result.setSpacing(true); - result.setPadding(true); - result.setAlignItems(FlexComponent.Alignment.STRETCH); - result.setWidthFull(); - return result; - }, - DemoComponentFactory::generateParagraph, - event -> { - if (event.isSelected()) - event.getCellInformation().getComponent().getElement().getClassList().add("selected"); - else event.getCellInformation().getComponent().getElement().getClassList().remove("selected"); - }, - SuperIntegerField.class, SuperLongField.class, SuperDoubleField.class, - SuperBigDecimalField.class, SuperDatePicker.class, SuperDateTimePicker.class, - SuperTabs.class, LazyLoad.class, ObservedField.class, - ComponentObserver.class, UnloadObserver.class, ItemGrid.class - ) - .withRowComponentGenerator(rowNumber -> { - HorizontalLayout result = new HorizontalLayout(); - result.setSpacing(true); - result.setAlignItems(FlexComponent.Alignment.CENTER); - result.setPadding(true); - return result; - }) - ); - gridSelect.getGrid().getColumnByKey("nameLength").setAutoWidth(true); - gridSelect.setItems(this.components.keySet().stream().map(SuperFieldsGridItem::new).collect(Collectors.toList())); - - this.contentBuilders.put(CanSelectText.class, this::buildCanSelectText); - this.contentBuilders.put(HasValue.class, this::buildHasValue); - this.contentBuilders.put(AbstractSuperNumberField.class, this::buildAbstractSuperNumberField); - this.contentBuilders.put(WithNullValueOptionallyAllowed.class, this::buildNullValueOptionallyAllowed); - this.contentBuilders.put(HasLocale.class, this::buildHasLocale); - this.contentBuilders.put(ItemGrid.class, this::buildItemGrid); - this.contentBuilders.put(HasDatePattern.class, this::buildHasDatePattern); - this.contentBuilders.put(SuperTabs.class, this::buildSuperTabs); - this.contentBuilders.put(ObservedField.class, this::buildObservedField); - this.contentBuilders.put(ComponentObserver.class, this::buildIntersectionObserver); - this.contentBuilders.put(UnloadObserver.class, this::buildUnloadObserver); - this.contentBuilders.put(FocusNotifier.class, this::buildFocusNotifier); - this.contentBuilders.put(BlurNotifier.class, this::buildBlurNotifier); - this.contentBuilders.put(StateChangeNotifier.class, this::buildStateChangeNotifier); - - } - - private void buildAbstractSuperNumberField(Component component, Consumer callback) { - final Checkbox autoselect = new Checkbox("Select automatically on focus?"); - autoselect.addValueChangeListener(event -> ((AbstractSuperNumberField) component).setAutoselect(event.getValue())); - - final Checkbox separatorHidden = new Checkbox("Hide grouping separator on focus?"); - separatorHidden.addValueChangeListener(event -> ((AbstractSuperNumberField) component).setGroupingSeparatorHiddenOnFocus(event.getValue())); - - final Checkbox prefix = new Checkbox("Show prefix component?"); - prefix.addValueChangeListener(event -> ((AbstractSuperNumberField) component).setPrefixComponent( - event.getValue() ? new Span(">") : null - )); - - final Checkbox suffix = new Checkbox("Show suffix component?"); - suffix.addValueChangeListener(event -> ((AbstractSuperNumberField) component).setSuffixComponent( - event.getValue() ? new Span("€") : null - )); - - final Checkbox alignRight = new Checkbox("Align text to the right?"); - alignRight.addValueChangeListener(event -> { - if(event.getValue()) - ((AbstractSuperNumberField) component).addThemeVariants(TextFieldVariant.LUMO_ALIGN_RIGHT); - else - ((AbstractSuperNumberField) component).removeThemeVariants(TextFieldVariant.LUMO_ALIGN_RIGHT); - } - ); - callback.accept(new Component[]{autoselect, separatorHidden, prefix, suffix, alignRight}); - } - - private void buildFocusNotifier(Component component, Consumer callback) { - ((FocusNotifier)component).addFocusListener(event -> - Notification.show("Component "+component.getClass().getSimpleName()+" received focus.", NOTIFICATION_TIME, Notification.Position.BOTTOM_END) - ); - callback.accept(new Component[]{new Span("Focus the demo component to see a notification.")}); - } - - private void buildBlurNotifier(Component component, Consumer callback) { - ((BlurNotifier)component).addBlurListener(event -> - Notification.show("Component "+component.getClass().getSimpleName()+" lost focus.", NOTIFICATION_TIME, Notification.Position.BOTTOM_END) - ); - callback.accept(new Component[]{new Span("Leave the demo component to see a notification.")}); - } - - private void buildStateChangeNotifier(Component component, Consumer callback) { - ((StateChangeNotifier)component).addStateChangeListener(event -> - Notification.show("Component "+component.getClass().getSimpleName()+" changed its state.", NOTIFICATION_TIME, Notification.Position.BOTTOM_END) - ); - callback.accept(new Component[]{new Span("Notifications will be shown when this component changes its state for any reason.")}); - } - - private void buildHasLocale(Component component, Consumer callback) { - final ComboBox locales = new ComboBox<>("Select locale:", new Locale("pl", "PL"), Locale.UK, Locale.FRANCE, Locale.GERMANY, Locale.CHINA); - locales.setItemLabelGenerator(locale -> locale.getDisplayCountry() + " / "+locale.getDisplayLanguage()); - locales.setAllowCustomValue(false); - locales.addValueChangeListener(event -> ((HasLocale) component).setLocale(event.getValue())); - callback.accept(new Component[]{locales}); - } - - private void buildNullValueOptionallyAllowed(Component component, Consumer callback) { - final Checkbox allow = new Checkbox("Allow empty string as null value?", event -> ((WithNullValueOptionallyAllowed)component).setNullValueAllowed(event.getValue())); - callback.accept(new Component[]{allow}); - } - - private void buildHasValue(Component component, Consumer callback) { - final Checkbox toggle = new Checkbox("Mark component as read only?", event -> ((HasValue)component).setReadOnly(event.getValue())); - ((HasValue) component).addValueChangeListener(this::onAnyValueChanged); - callback.accept(new Component[]{toggle}); - } - - private void buildCanSelectText(Component component, Consumer callback) { - final Button selectAll = new Button("Select all", event -> ((CanSelectText)component).selectAll()); - final Button selectNone = new Button("Select none", event -> ((CanSelectText)component).selectNone()); - final HorizontalLayout layout = new HorizontalLayout(new Span("Type something in the field, then click one of the buttons:"), selectAll, selectNone); - layout.setAlignItems(FlexComponent.Alignment.CENTER); - callback.accept(new Component[]{ - layout - }); - if(component instanceof CanReceiveSelectionEventsFromClient) { - final Checkbox receiveFromClient = new Checkbox("Allow selection events initiated by keyboard or mouse?", - event -> ((CanReceiveSelectionEventsFromClient) component).setReceivingSelectionEventsFromClient(event.getValue())); - callback.accept(new Component[] {receiveFromClient}); - } - if(component instanceof TextSelectionNotifier) { - final Span selection = new Span(); - ((TextSelectionNotifier) component).addTextSelectionListener(event -> selection.setText(event.getSelectedText())); - Icon icon = VaadinIcon.INFO_CIRCLE.create(); - icon.setColor("green"); - icon.getElement().setAttribute("title", "When the component does not receive events from the browser, selection events will only be called for server-side initiated actions."); - callback.accept(new Component[]{ - new HorizontalLayout(new Span("Most recently selected text: <"), selection, new Span(">"), icon) - }); - } - } + private final Map, ContentBuilder> contentBuilders = new LinkedHashMap<>(); @SuppressWarnings("unchecked") - private void buildItemGrid(Component component, Consumer callback) { - final RadioButtonGroup buttons = new RadioButtonGroup<>(); - buttons.setItems(1, 2, 3, 4, 5, 6); - buttons.setLabel("Number of columns:"); - buttons.setValue(ItemGrid.DEFAULT_COLUMN_COUNT); - buttons.addValueChangeListener(event -> ((ItemGrid)component).setColumnCount(event.getValue())); - - final Checkbox alternate = new Checkbox("Display lazy loading cells?", event -> - ((ItemGrid>)component).setCellGenerator( - event.getValue() ? DemoComponentFactory::generateDiv : DemoComponentFactory::generateParagraph - ) - ); - - callback.accept(new Component[]{buttons, alternate}); - } - - private void buildHasDatePattern(Component component, Consumer callback) { - final ComboBox patterns = new ComboBox<>("Select date display pattern:", - DatePatterns.YYYY_MM_DD, DatePatterns.M_D_YYYY_SLASH, - DatePatterns.DD_MM_YYYY_DOTTED, DatePatterns.DD_MM_YY_OR_YYYY_DOTTED, DatePatterns.D_M_YY_DOTTED, - DatePatterns.YYYYMMDD, DatePatterns.DDMMYY - ); - final Button clearPattern = new Button("Clear pattern", event -> ((HasDatePattern)component).setDatePattern(null)); - clearPattern.setDisableOnClick(true); - final Component clearPatternOrContainer; - // issue #87 requires a note - if(component instanceof SuperDatePicker) - clearPatternOrContainer = clearPattern; - else { - Icon icon = new Icon(VaadinIcon.INFO); - icon.setColor("green"); - icon.getElement().setAttribute("title", "Setting pattern does not work out of the box for SuperDateTimePicker if it is in an invisible layout. See issue #87, https://github.com/vaadin-miki/super-fields/issues/87."); - clearPatternOrContainer = new HorizontalLayout(clearPattern, icon); - ((HorizontalLayout)clearPatternOrContainer).setAlignItems(FlexComponent.Alignment.CENTER); - } - patterns.addValueChangeListener(event -> { - ((HasDatePattern) component).setDatePattern(event.getValue()); - clearPattern.setEnabled(true); - }); - callback.accept(new Component[]{patterns, clearPatternOrContainer}); - } - - private void buildObservedField(Component component, Consumer callback) { - final Span description = new Span("An instance of ObservedField is added below these texts. It has a value change listener that updates the counter whenever the field is shown on screen, for example as a result of resizing window or scrolling. The value does not change when the field gets hidden due to styling (display: none). The field itself in an empty HTML and it cannot be normally seen, but it still is rendered by the browser."); - final Span counterText = new Span("The field has become visible this many times so far: "); - final Span counterLabel = new Span("0"); - final Span instruction = new Span("The field is rendered below this text. Resize the window a few times to hide this line to see the value change events being triggered."); - counterLabel.addClassName("counter-label"); - ((ObservedField)component).addValueChangeListener(event -> { - if(event.getValue()) - counterLabel.setText(String.valueOf(Integer.parseInt(counterLabel.getText()) + 1)); - }); - callback.accept(new Component[]{description, new HorizontalLayout(counterText, counterLabel), instruction}); - } - - private void buildIntersectionObserver(Component component, Consumer callback) { - for(String s: new String[]{"span-one", "span-two", "span-three"}) { - Span span = new Span("This text is observed by the intersection observer. Resize the window to make it disappear and see what happens. It has id of "+s+". "); - span.setId(s); - ((ComponentObserver)component).observe(span); - callback.accept(new Component[]{span}); - } - ((ComponentObserver) component).addComponentObservationListener(event -> { - if(event.isFullyVisible()) { - Notification.show("Component with id " + event.getObservedComponent().getId().orElse("(no id)") + " is now fully visible."); - if(event.getObservedComponent().getId().orElse("").equals("span-two")) { - event.getSource().unobserve(event.getObservedComponent()); - Notification.show("Component with id span-two has been unobserved."); - } - } - else if(event.isNotVisible()) - Notification.show("Component with id "+event.getObservedComponent().getId().orElse("(no id)")+" is now not visible."); - }); - } - - private void buildUnloadObserver(Component component, Consumer callback) { - final Checkbox query = new Checkbox("Query on window unload?", event -> ((UnloadObserver)component).setQueryingOnUnload(event.getValue())); - final Span description = new Span("This component optionally displays a browser-native window when leaving this app. Select the checkbox above and try to close the window or tab to see it in action."); - final Span counterText = new Span("There were this many attempts to leave this app so far: "); - final Span counter = new Span("0"); - ((UnloadObserver)component).addUnloadListener(event -> { - if(event.isBecauseOfQuerying()) - counter.setText(String.valueOf(Integer.parseInt(counter.getText()) + 1)); - LoggerFactory.getLogger(this.getClass()).info("Unload event; attempt? {}; captured in {} and UnloadObserver is inside {}", event.isBecauseOfQuerying(), this.getClass().getSimpleName(), event.getSource().getParent().orElse(new Span()).getClass().getSimpleName()); - }); - - callback.accept(new Component[]{query, description, new HorizontalLayout(counterText, counter)}); - } - - private void buildSuperTabs(Component component, Consumer callback) { - final Checkbox multilineTabs = new Checkbox("Multiline tabs?", event -> ((SuperTabs)component).setMultiline(event.getValue())); - - final ComboBox tabHandlers = new ComboBox<>("Select a tab handler: ", - TabHandlers.VISIBILITY_HANDLER, TabHandlers.REMOVING_HANDLER, TabHandlers.selectedContentHasClassName("selected-tab")); - tabHandlers.addValueChangeListener(event -> { - if(event.getValue() != null) - ((SuperTabs)component).setTabHandler(event.getValue()); - }); - - callback.accept(new Component[]{multilineTabs, tabHandlers}); - } + private DemoComponentFactory() { + final ValidatorStorage storage = new ValidatorStorage(); + + StreamSupport.stream(ClassIndex.getSubclasses(ComponentProvider.class).spliterator(), false) + .filter(DemoComponentFactory::isNotInterface) + .sorted(CLASS_ORDER_COMPARATOR) + .forEach(type -> { + try { + final ComponentProvider componentProvider = type.getDeclaredConstructor().newInstance(); + final Component component = componentProvider.getComponent(); + this.components.put(component.getClass(), component); + if (componentProvider instanceof Validator && component instanceof HasValue) + //noinspection rawtypes + storage.registerValidator((HasValue) component).accept((Validator) componentProvider); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + LOGGER.error("creating a component from {} failed due to error {} with message {}", type.getSimpleName(), e.getClass().getSimpleName(), e.getMessage()); + } + }); - private void onAnyValueChanged(HasValue.ValueChangeEvent valueChangeEvent) { - Notification.show(String.format("%s changed value to %s", valueChangeEvent.getHasValue().getClass().getSimpleName(), valueChangeEvent.getValue())); + StreamSupport.stream(ClassIndex.getSubclasses(ContentBuilder.class).spliterator(), false) + .filter(DemoComponentFactory::isNotInterface) + .sorted(CLASS_ORDER_COMPARATOR) + .forEach(type -> { + try { + final ContentBuilder contentBuilder = type.getDeclaredConstructor().newInstance(); + final Class suitableType = ReflectTools.getGenericInterfaceType(contentBuilder.getClass(), ContentBuilder.class); + this.contentBuilders.put(suitableType, contentBuilder); + if (contentBuilder instanceof NeedsValidatorStorage) + ((NeedsValidatorStorage) contentBuilder).setValidatorStorage(storage); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + LOGGER.error("creating a content builder from {} failed due to error {} with message {}", type.getSimpleName(), e.getClass().getSimpleName(), e.getMessage()); + } + }); } /** @@ -408,17 +114,20 @@ public Component buildDemoPageFor(Class type) { this.contentBuilders.entrySet().stream(). filter(entry -> entry.getKey().isAssignableFrom(type)). - forEach(entry -> { - VerticalLayout section = new VerticalLayout(); - section.setSizeUndefined(); - section.addClassName("section-layout"); - Span header = new Span("Configuration options for "+entry.getKey().getSimpleName()); - header.addClassName("section-header"); - section.add(header); - entry.getValue().accept(component, section::add); - result.add(section); - }); + forEach(entry -> result.add(this.buildContentFor(component, entry.getKey().getSimpleName(), entry.getValue()))); return result; } + @SuppressWarnings("unchecked") // type is checked + private Component buildContentFor(Component component, String typeName, ContentBuilder builder) { + final VerticalLayout section = new VerticalLayout(); + section.setSizeUndefined(); + section.addClassName("section-layout"); + final Span header = new Span("Configuration options for "+typeName); + header.addClassName("section-header"); + section.add(header); + builder.buildContent((C)component, section::add); + return section; + } + } diff --git a/demo-v14/src/main/java/org/vaadin/miki/MainLayout.java b/demo-v14/src/main/java/org/vaadin/miki/MainLayout.java index d7c84590..6318d80c 100644 --- a/demo-v14/src/main/java/org/vaadin/miki/MainLayout.java +++ b/demo-v14/src/main/java/org/vaadin/miki/MainLayout.java @@ -54,7 +54,7 @@ public MainLayout() { public void afterNavigation(AfterNavigationEvent event) { if(event.getLocation().getPath().isEmpty()) this.navigationTabs.setSelectedIndex(0); - else + else if(!event.getLocation().getPath().equals("binder")) this.navigationTabs.setSelectedTab(this.tabs.get(event.getLocation().getSegments().get(1))); } } diff --git a/demo-v14/src/main/java/org/vaadin/miki/SampleModel.java b/demo-v14/src/main/java/org/vaadin/miki/SampleModel.java new file mode 100644 index 00000000..e7f63cb1 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/SampleModel.java @@ -0,0 +1,21 @@ +package org.vaadin.miki; + +/** + * A generic model to use in validation. + * @param Type of the object to hold. + * @author miki + * @since 2020-11-14 + */ +public class SampleModel { + + private T value; + + public T getValue() { + return value; + } + + public void setValue(T value) { + this.value = value; + } + +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/ComponentProvider.java b/demo-v14/src/main/java/org/vaadin/miki/demo/ComponentProvider.java new file mode 100644 index 00000000..57c3cfb3 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/ComponentProvider.java @@ -0,0 +1,23 @@ +package org.vaadin.miki.demo; + +import com.vaadin.flow.component.Component; +import org.atteo.classindex.IndexSubclasses; + +/** + * Marker interface for objects that provide an instance of a Vaadin {@link Component}. + * It is assumed that implementations of this interface provide a public, no-arg constructor. + * @param Type of the component to be created. + * @author miki + * @since 2020-11-17 + */ +@FunctionalInterface +@IndexSubclasses +public interface ComponentProvider { + + /** + * Builds a fresh instance of a component. + * @return A non-null instance of an object. + */ + T getComponent(); + +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/ContentBuilder.java b/demo-v14/src/main/java/org/vaadin/miki/demo/ContentBuilder.java new file mode 100644 index 00000000..a7fbdc90 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/ContentBuilder.java @@ -0,0 +1,26 @@ +package org.vaadin.miki.demo; + +import com.vaadin.flow.component.Component; +import org.atteo.classindex.IndexSubclasses; + +import java.util.function.Consumer; + +/** + * Marker interface for objects that build content for components. + * Implementations of this interface must have a public no-arg constructor. + * @param Type of the object to build content for. + * @author miki + * @since 2020-11-18 + */ +@IndexSubclasses +@FunctionalInterface +public interface ContentBuilder { + + /** + * Builds content. + * @param component {@link Component} to build content for. Already cast to {@code T} for easier use. + * @param callback Callback to call when content is built. + */ + void buildContent(T component, Consumer callback); + +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/NeedsValidatorStorage.java b/demo-v14/src/main/java/org/vaadin/miki/demo/NeedsValidatorStorage.java new file mode 100644 index 00000000..28b86d94 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/NeedsValidatorStorage.java @@ -0,0 +1,17 @@ +package org.vaadin.miki.demo; + +/** + * Marker interface for an object that needs access to {@link ValidatorStorage}. + * @author miki + * @since 2020-11-18 + */ +@FunctionalInterface +public interface NeedsValidatorStorage { + + /** + * Sets the {@link ValidatorStorage} associated with this object. + * @param storage Storage. Must not be {@code null}. + */ + void setValidatorStorage(ValidatorStorage storage); + +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/Order.java b/demo-v14/src/main/java/org/vaadin/miki/demo/Order.java new file mode 100644 index 00000000..ace0bca5 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/Order.java @@ -0,0 +1,19 @@ +package org.vaadin.miki.demo; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines order in which classes will be sorted. + * @author miki + * @since 2020-11-20 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Order { + + int value() default Integer.MAX_VALUE; + +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/ValidatorStorage.java b/demo-v14/src/main/java/org/vaadin/miki/demo/ValidatorStorage.java new file mode 100644 index 00000000..b35b80c0 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/ValidatorStorage.java @@ -0,0 +1,38 @@ +package org.vaadin.miki.demo; + +import com.vaadin.flow.component.HasValue; +import com.vaadin.flow.data.binder.Validator; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Stores component-specific {@link Validator}s. + * @author miki + * @since 2020-11-18 + */ +public class ValidatorStorage { + + private final Map, Validator> validators = new HashMap<>(); + + public > void registerValidator(C instance, Validator validator) { + this.validators.put(instance, validator); + } + + public > Consumer> registerValidator(final C instance) { + return validator -> this.validators.put(instance, validator); + } + + public boolean isValidatorPresent(HasValue component) { + return this.validators.containsKey(component); + } + + @SuppressWarnings("unchecked") + public > Validator getValidator(C instance) { + if(this.isValidatorPresent(instance)) + return (Validator)this.validators.get(instance); + else return null; + } + +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/builders/AbstractSuperNumberFieldBuilder.java b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/AbstractSuperNumberFieldBuilder.java new file mode 100644 index 00000000..b51b2082 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/AbstractSuperNumberFieldBuilder.java @@ -0,0 +1,50 @@ +package org.vaadin.miki.demo.builders; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.textfield.TextFieldVariant; +import org.vaadin.miki.demo.ContentBuilder; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.superfields.numbers.AbstractSuperNumberField; + +import java.util.function.Consumer; + +/** + * Builds content for {@link AbstractSuperNumberField}. + * @author miki + * @since 2020-11-19 + */ +@Order(30) +@SuppressWarnings("squid:S5411") // no way around boxed values +public class AbstractSuperNumberFieldBuilder implements ContentBuilder> { + + @Override + public void buildContent(AbstractSuperNumberField component, Consumer callback) { + final Checkbox autoselect = new Checkbox("Select automatically on focus?"); + autoselect.addValueChangeListener(event -> component.setAutoselect(event.getValue())); + + final Checkbox separatorHidden = new Checkbox("Hide grouping separator on focus?"); + separatorHidden.addValueChangeListener(event -> component.setGroupingSeparatorHiddenOnFocus(event.getValue())); + + final Checkbox prefix = new Checkbox("Show prefix component?"); + prefix.addValueChangeListener(event -> component.setPrefixComponent( + event.getValue() ? new Span(">") : null + )); + + final Checkbox suffix = new Checkbox("Show suffix component?"); + suffix.addValueChangeListener(event -> component.setSuffixComponent( + event.getValue() ? new Span("€") : null + )); + + final Checkbox alignRight = new Checkbox("Align text to the right?"); + alignRight.addValueChangeListener(event -> { + if(event.getValue()) + component.addThemeVariants(TextFieldVariant.LUMO_ALIGN_RIGHT); + else + component.removeThemeVariants(TextFieldVariant.LUMO_ALIGN_RIGHT); + } + ); + callback.accept(new Component[]{autoselect, separatorHidden, prefix, suffix, alignRight}); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/builders/BlurNotifierBuilder.java b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/BlurNotifierBuilder.java new file mode 100644 index 00000000..0ea9715b --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/BlurNotifierBuilder.java @@ -0,0 +1,27 @@ +package org.vaadin.miki.demo.builders; + +import com.vaadin.flow.component.BlurNotifier; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.notification.Notification; +import org.vaadin.miki.demo.ContentBuilder; +import org.vaadin.miki.demo.Order; + +import java.util.function.Consumer; + +/** + * Builds content for {@link BlurNotifier}. + * @author miki + * @since 2020-11-19 + */ +@Order(130) +public class BlurNotifierBuilder implements ContentBuilder> { + + @Override + public void buildContent(BlurNotifier component, Consumer callback) { + component.addBlurListener(event -> + Notification.show(String.format(NotificationConstants.BLUR_MESSAGE, component.getClass().getSimpleName()), NotificationConstants.NOTIFICATION_TIME, Notification.Position.BOTTOM_END) + ); + callback.accept(new Component[]{new Span("Leave the demo component to see a notification.")}); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/builders/CanSelectTextBuilder.java b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/CanSelectTextBuilder.java new file mode 100644 index 00000000..8a0da25a --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/CanSelectTextBuilder.java @@ -0,0 +1,52 @@ +package org.vaadin.miki.demo.builders; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import org.vaadin.miki.demo.ContentBuilder; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.events.text.TextSelectionNotifier; +import org.vaadin.miki.markers.CanReceiveSelectionEventsFromClient; +import org.vaadin.miki.markers.CanSelectText; + +import java.util.function.Consumer; + +/** + * Builds content for {@link CanSelectText}. + * @author miki + * @since 2020-11-18 + */ +@Order(10) +public class CanSelectTextBuilder implements ContentBuilder { + + @Override + public void buildContent(CanSelectText component, Consumer callback) { + final Button selectAll = new Button("Select all", event -> component.selectAll()); + final Button selectNone = new Button("Select none", event -> component.selectNone()); + final HorizontalLayout layout = new HorizontalLayout(new Span("Type something in the field, then click one of the buttons:"), selectAll, selectNone); + layout.setAlignItems(FlexComponent.Alignment.CENTER); + callback.accept(new Component[]{ + layout + }); + if(component instanceof CanReceiveSelectionEventsFromClient) { + final Checkbox receiveFromClient = new Checkbox("Allow selection events initiated by keyboard or mouse?", + event -> ((CanReceiveSelectionEventsFromClient) component).setReceivingSelectionEventsFromClient(event.getValue())); + callback.accept(new Component[] {receiveFromClient}); + } + if(component instanceof TextSelectionNotifier) { + final Span selection = new Span(); + ((TextSelectionNotifier) component).addTextSelectionListener(event -> selection.setText(event.getSelectedText())); + Icon icon = VaadinIcon.INFO_CIRCLE.create(); + icon.setColor("green"); + icon.getElement().setAttribute("title", "When the component does not receive events from the browser, selection events will only be called for server-side initiated actions."); + callback.accept(new Component[]{ + new HorizontalLayout(new Span("Most recently selected text: <"), selection, new Span(">"), icon) + }); + } + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/builders/ComponentObserverBuilder.java b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/ComponentObserverBuilder.java new file mode 100644 index 00000000..bb1a067b --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/ComponentObserverBuilder.java @@ -0,0 +1,40 @@ +package org.vaadin.miki.demo.builders; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.notification.Notification; +import org.vaadin.miki.demo.ContentBuilder; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.superfields.lazyload.ComponentObserver; + +import java.util.function.Consumer; + +/** + * Builds content for {@link ComponentObserver}. + * @author miki + * @since 2020-11-19 + */ +@Order(100) +public class ComponentObserverBuilder implements ContentBuilder { + + @Override + public void buildContent(ComponentObserver component, Consumer callback) { + for(String s: new String[]{"span-one", "span-two", "span-three"}) { + Span span = new Span("This text is observed by the intersection observer. Resize the window to make it disappear and see what happens. It has id of "+s+". "); + span.setId(s); + component.observe(span); + callback.accept(new Component[]{span}); + } + component.addComponentObservationListener(event -> { + if(event.isFullyVisible()) { + Notification.show("Component with id " + event.getObservedComponent().getId().orElse("(no id)") + " is now fully visible."); + if(event.getObservedComponent().getId().orElse("").equals("span-two")) { + event.getSource().unobserve(event.getObservedComponent()); + Notification.show("Component with id span-two has been unobserved."); + } + } + else if(event.isNotVisible()) + Notification.show("Component with id "+event.getObservedComponent().getId().orElse("(no id)")+" is now not visible."); + }); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/builders/FocusNotifierBuilder.java b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/FocusNotifierBuilder.java new file mode 100644 index 00000000..18a639ed --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/FocusNotifierBuilder.java @@ -0,0 +1,27 @@ +package org.vaadin.miki.demo.builders; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.FocusNotifier; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.notification.Notification; +import org.vaadin.miki.demo.ContentBuilder; +import org.vaadin.miki.demo.Order; + +import java.util.function.Consumer; + +/** + * Builds content for {@link FocusNotifier}. + * @author miki + * @since 2020-11-19 + */ +@Order(120) +public class FocusNotifierBuilder implements ContentBuilder> { + + @Override + public void buildContent(FocusNotifier component, Consumer callback) { + component.addFocusListener(event -> + Notification.show(String.format(NotificationConstants.FOCUS_MESSAGE, component.getClass().getSimpleName()), NotificationConstants.NOTIFICATION_TIME, Notification.Position.BOTTOM_END) + ); + callback.accept(new Component[]{new Span("Focus the demo component to see a notification.")}); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/builders/HasDatePatternBuilder.java b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/HasDatePatternBuilder.java new file mode 100644 index 00000000..6cde1ab8 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/HasDatePatternBuilder.java @@ -0,0 +1,53 @@ +package org.vaadin.miki.demo.builders; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import org.vaadin.miki.demo.ContentBuilder; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.markers.HasDatePattern; +import org.vaadin.miki.shared.dates.DatePattern; +import org.vaadin.miki.shared.dates.DatePatterns; +import org.vaadin.miki.superfields.dates.SuperDatePicker; + +import java.util.function.Consumer; + +/** + * Builds content for {@link HasDatePattern}. + * @author miki + * @since 2020-11-19 + */ +@Order(70) +public class HasDatePatternBuilder implements ContentBuilder { + + @Override + public void buildContent(HasDatePattern component, Consumer callback) { + final ComboBox patterns = new ComboBox<>("Select date display pattern:", + DatePatterns.YYYY_MM_DD, DatePatterns.M_D_YYYY_SLASH, + DatePatterns.DD_MM_YYYY_DOTTED, DatePatterns.DD_MM_YY_OR_YYYY_DOTTED, DatePatterns.D_M_YY_DOTTED, + DatePatterns.YYYYMMDD, DatePatterns.DDMMYY + ); + final Button clearPattern = new Button("Clear pattern", event -> component.setDatePattern(null)); + clearPattern.setDisableOnClick(true); + final Component clearPatternOrContainer; + // issue #87 requires a note + if(component instanceof SuperDatePicker) + clearPatternOrContainer = clearPattern; + else { + Icon icon = new Icon(VaadinIcon.INFO); + icon.setColor("green"); + icon.getElement().setAttribute("title", "Setting pattern does not work out of the box for SuperDateTimePicker if it is in an invisible layout. See issue #87, https://github.com/vaadin-miki/super-fields/issues/87."); + clearPatternOrContainer = new HorizontalLayout(clearPattern, icon); + ((HorizontalLayout)clearPatternOrContainer).setAlignItems(FlexComponent.Alignment.CENTER); + } + patterns.addValueChangeListener(event -> { + component.setDatePattern(event.getValue()); + clearPattern.setEnabled(true); + }); + callback.accept(new Component[]{patterns, clearPatternOrContainer}); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/builders/HasLocaleBuilder.java b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/HasLocaleBuilder.java new file mode 100644 index 00000000..abb35b08 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/HasLocaleBuilder.java @@ -0,0 +1,28 @@ +package org.vaadin.miki.demo.builders; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.combobox.ComboBox; +import org.vaadin.miki.demo.ContentBuilder; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.markers.HasLocale; + +import java.util.Locale; +import java.util.function.Consumer; + +/** + * Builds content for {@link HasLocale}. + * @author miki + * @since 2020-11-19 + */ +@Order(50) +public class HasLocaleBuilder implements ContentBuilder { + + @Override + public void buildContent(HasLocale component, Consumer callback) { + final ComboBox locales = new ComboBox<>("Select locale:", new Locale("pl", "PL"), Locale.UK, Locale.FRANCE, Locale.GERMANY, Locale.CHINA); + locales.setItemLabelGenerator(locale -> locale.getDisplayCountry() + " / "+locale.getDisplayLanguage()); + locales.setAllowCustomValue(false); + locales.addValueChangeListener(event -> component.setLocale(event.getValue())); + callback.accept(new Component[]{locales}); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/builders/HasValueBuilder.java b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/HasValueBuilder.java new file mode 100644 index 00000000..37d0fc61 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/HasValueBuilder.java @@ -0,0 +1,61 @@ +package org.vaadin.miki.demo.builders; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.HasValue; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.notification.Notification; +import com.vaadin.flow.data.binder.Binder; +import com.vaadin.flow.data.binder.Validator; +import org.vaadin.miki.SampleModel; +import org.vaadin.miki.demo.ContentBuilder; +import org.vaadin.miki.demo.NeedsValidatorStorage; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.demo.ValidatorStorage; + +import java.util.function.Consumer; + +/** + * Builds content for {@link HasValue}. + * Reuses {@link ValidatorStorage}. + * @author miki + * @since 2020-11-18 + */ +@Order(20) +public class HasValueBuilder implements ContentBuilder>, NeedsValidatorStorage { + + private ValidatorStorage storage; + + @Override + public void setValidatorStorage(ValidatorStorage storage) { + this.storage = storage; + } + + @Override + public void buildContent(HasValue component, Consumer callback) { + final Checkbox toggle = new Checkbox("Mark component as read only?", event -> component.setReadOnly(event.getValue())); + component.addValueChangeListener(this::onAnyValueChanged); + callback.accept(new Component[]{toggle}); + if(this.storage != null && this.storage.isValidatorPresent(component)) { + final Span binder = new Span("This component has a validation check."); + this.addBinder(component); + callback.accept(new Component[]{binder}); + } + } + + private void onAnyValueChanged(HasValue.ValueChangeEvent valueChangeEvent) { + Notification.show(String.format("%s changed value to %s", valueChangeEvent.getHasValue().getClass().getSimpleName(), valueChangeEvent.getValue())); + } + + @SuppressWarnings("unchecked") + private void addBinder(HasValue component) { + final Validator validator = this.storage.getValidator(component); + final SampleModel sampleModel = new SampleModel<>(); + final Binder> binder = new Binder<>((Class>)(Class) SampleModel.class); + binder.setBean(sampleModel); + binder.forField(component) + .withValidator(validator) + .bind(SampleModel::getValue, SampleModel::setValue); + } + +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/builders/ItemGridBuilder.java b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/ItemGridBuilder.java new file mode 100644 index 00000000..0e9ceafd --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/ItemGridBuilder.java @@ -0,0 +1,39 @@ +package org.vaadin.miki.demo.builders; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.radiobutton.RadioButtonGroup; +import org.vaadin.miki.demo.ContentBuilder; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.demo.providers.ItemGridGenerators; +import org.vaadin.miki.superfields.itemgrid.ItemGrid; + +import java.util.function.Consumer; + +/** + * Builds content for {@link ItemGrid}. + * @author miki + * @since 2020-11-19 + */ +@Order(60) +@SuppressWarnings("squid:S5411") // no way around boxed values +public class ItemGridBuilder implements ContentBuilder> { + + @Override + public void buildContent(ItemGrid component, Consumer callback) { + final RadioButtonGroup buttons = new RadioButtonGroup<>(); + buttons.setItems(1, 2, 3, 4, 5, 6); + buttons.setLabel("Number of columns:"); + buttons.setValue(ItemGrid.DEFAULT_COLUMN_COUNT); + buttons.addValueChangeListener(event -> component.setColumnCount(event.getValue())); + + @SuppressWarnings("unchecked") + final Checkbox alternate = new Checkbox("Display lazy loading cells?", event -> + ((ItemGrid>)component).setCellGenerator( + event.getValue() ? ItemGridGenerators::generateDiv : ItemGridGenerators::generateParagraph + ) + ); + + callback.accept(new Component[]{buttons, alternate}); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/builders/NotificationConstants.java b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/NotificationConstants.java new file mode 100644 index 00000000..031938d8 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/NotificationConstants.java @@ -0,0 +1,22 @@ +package org.vaadin.miki.demo.builders; + +/** + * Storage class for constants related to notifications. + * @author miki + * @since 2020-11-19 + */ +class NotificationConstants { + + static final int NOTIFICATION_TIME = 1500; + + static final String FOCUS_MESSAGE = "Component %s received focus."; + + static final String BLUR_MESSAGE = "Component %s lost focus."; + + static final String STATE_MESSAGE = "Component %s changed its state."; + + private NotificationConstants() { + // no instances allowed + } + +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/builders/ObservedFieldBuilder.java b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/ObservedFieldBuilder.java new file mode 100644 index 00000000..69ac758d --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/ObservedFieldBuilder.java @@ -0,0 +1,34 @@ +package org.vaadin.miki.demo.builders; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import org.vaadin.miki.demo.ContentBuilder; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.superfields.lazyload.ObservedField; + +import java.util.function.Consumer; + +/** + * Builds content for {@link ObservedField}. + * @author miki + * @since 2020-11-19 + */ +@Order(90) +public class ObservedFieldBuilder implements ContentBuilder { + + @Override + @SuppressWarnings("squid:S5411") + public void buildContent(ObservedField component, Consumer callback) { + final Span description = new Span("An instance of ObservedField is added below these texts. It has a value change listener that updates the counter whenever the field is shown on screen, for example as a result of resizing window or scrolling. The value does not change when the field gets hidden due to styling (display: none). The field itself in an empty HTML and it cannot be normally seen, but it still is rendered by the browser."); + final Span counterText = new Span("The field has become visible this many times so far: "); + final Span counterLabel = new Span("0"); + final Span instruction = new Span("The field is rendered below this text. Resize the window a few times to hide this line to see the value change events being triggered."); + counterLabel.addClassName("counter-label"); + component.addValueChangeListener(event -> { + if(event.getValue()) + counterLabel.setText(String.valueOf(Integer.parseInt(counterLabel.getText()) + 1)); + }); + callback.accept(new Component[]{description, new HorizontalLayout(counterText, counterLabel), instruction}); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/builders/StateChangeNotifierBuilder.java b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/StateChangeNotifierBuilder.java new file mode 100644 index 00000000..c874bc30 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/StateChangeNotifierBuilder.java @@ -0,0 +1,27 @@ +package org.vaadin.miki.demo.builders; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.notification.Notification; +import org.vaadin.miki.demo.ContentBuilder; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.events.state.StateChangeNotifier; + +import java.util.function.Consumer; + +/** + * Provides content for {@link StateChangeNotifier}. + * @author miki + * @since 2020-11-19 + */ +@Order(140) +public class StateChangeNotifierBuilder implements ContentBuilder> { + + @Override + public void buildContent(StateChangeNotifier component, Consumer callback) { + component.addStateChangeListener(event -> + Notification.show(String.format(NotificationConstants.STATE_MESSAGE, component.getClass().getSimpleName()), NotificationConstants.NOTIFICATION_TIME, Notification.Position.BOTTOM_END) + ); + callback.accept(new Component[]{new Span("Notifications will be shown when this component changes its state for any reason.")}); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/builders/SuperTabsBuilder.java b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/SuperTabsBuilder.java new file mode 100644 index 00000000..9658f942 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/SuperTabsBuilder.java @@ -0,0 +1,35 @@ +package org.vaadin.miki.demo.builders; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.combobox.ComboBox; +import org.vaadin.miki.demo.ContentBuilder; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.superfields.tabs.SuperTabs; +import org.vaadin.miki.superfields.tabs.TabHandler; +import org.vaadin.miki.superfields.tabs.TabHandlers; + +import java.util.function.Consumer; + +/** + * Builds content for {@link SuperTabs}. + * @author miki + * @since 2020-11-19 + */ +@Order(80) +public class SuperTabsBuilder implements ContentBuilder> { + + @Override + public void buildContent(SuperTabs component, Consumer callback) { + final Checkbox multilineTabs = new Checkbox("Multiline tabs?", event -> component.setMultiline(event.getValue())); + + final ComboBox tabHandlers = new ComboBox<>("Select a tab handler: ", + TabHandlers.VISIBILITY_HANDLER, TabHandlers.REMOVING_HANDLER, TabHandlers.selectedContentHasClassName("selected-tab")); + tabHandlers.addValueChangeListener(event -> { + if(event.getValue() != null) + component.setTabHandler(event.getValue()); + }); + + callback.accept(new Component[]{multilineTabs, tabHandlers}); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/builders/UnloadObserverBuilder.java b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/UnloadObserverBuilder.java new file mode 100644 index 00000000..c8f391f0 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/UnloadObserverBuilder.java @@ -0,0 +1,36 @@ +package org.vaadin.miki.demo.builders; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import org.slf4j.LoggerFactory; +import org.vaadin.miki.demo.ContentBuilder; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.superfields.unload.UnloadObserver; + +import java.util.function.Consumer; + +/** + * Builds content for {@link UnloadObserver}. + * @author miki + * @since 2020-11-19 + */ +@Order(110) +public class UnloadObserverBuilder implements ContentBuilder { + + @Override + public void buildContent(UnloadObserver component, Consumer callback) { + final Checkbox query = new Checkbox("Query on window unload?", event -> component.setQueryingOnUnload(event.getValue())); + final Span description = new Span("This component optionally displays a browser-native window when leaving this app. Select the checkbox above and try to close the window or tab to see it in action."); + final Span counterText = new Span("There were this many attempts to leave this app so far: "); + final Span counter = new Span("0"); + component.addUnloadListener(event -> { + if(event.isBecauseOfQuerying()) + counter.setText(String.valueOf(Integer.parseInt(counter.getText()) + 1)); + LoggerFactory.getLogger(this.getClass()).info("Unload event; attempt? {}; captured in {} and UnloadObserver is inside {}", event.isBecauseOfQuerying(), this.getClass().getSimpleName(), event.getSource().getParent().orElse(new Span()).getClass().getSimpleName()); + }); + + callback.accept(new Component[]{query, description, new HorizontalLayout(counterText, counter)}); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/builders/WithNullValueOptionallyAllowedBuilder.java b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/WithNullValueOptionallyAllowedBuilder.java new file mode 100644 index 00000000..b0c246b6 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/builders/WithNullValueOptionallyAllowedBuilder.java @@ -0,0 +1,24 @@ +package org.vaadin.miki.demo.builders; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.checkbox.Checkbox; +import org.vaadin.miki.demo.ContentBuilder; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.markers.WithNullValueOptionallyAllowed; + +import java.util.function.Consumer; + +/** + * Builds content for {@link WithNullValueOptionallyAllowed}. + * @author miki + * @since 2020-11-19 + */ +@Order(40) +public class WithNullValueOptionallyAllowedBuilder implements ContentBuilder> { + + @Override + public void buildContent(WithNullValueOptionallyAllowed component, Consumer callback) { + final Checkbox allow = new Checkbox("Allow empty string as null value?", event -> component.setNullValueAllowed(event.getValue())); + callback.accept(new Component[]{allow}); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/providers/ComponentObserverProvider.java b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/ComponentObserverProvider.java new file mode 100644 index 00000000..13449aca --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/ComponentObserverProvider.java @@ -0,0 +1,18 @@ +package org.vaadin.miki.demo.providers; + +import org.vaadin.miki.demo.ComponentProvider; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.superfields.lazyload.ComponentObserver; + +/** + * Provides {@link ComponentObserver}. + * @author miki + * @since 2020-11-18 + */ +@Order(130) +public class ComponentObserverProvider implements ComponentProvider { + @Override + public ComponentObserver getComponent() { + return new ComponentObserver(); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/providers/GridSelectProvider.java b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/GridSelectProvider.java new file mode 100644 index 00000000..00fcc6f1 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/GridSelectProvider.java @@ -0,0 +1,31 @@ +package org.vaadin.miki.demo.providers; + +import org.atteo.classindex.ClassIndex; +import org.vaadin.miki.demo.ComponentProvider; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.superfields.gridselect.GridSelect; + +import java.util.stream.StreamSupport; + +/** + * Provides {@link GridSelect}. + * @author miki + * @since 2020-11-18 + */ +@Order(120) +public class GridSelectProvider implements ComponentProvider> { + + @Override + public GridSelect getComponent() { + final GridSelect gridSelect = new GridSelect<>(SuperFieldsGridItem.class, true, + // scans all providers (as that should be all showcased components) and adds them to the grid + StreamSupport.stream(ClassIndex.getSubclasses(ComponentProvider.class).spliterator(), false) + .filter(type -> !type.isInterface()) + .map(Class::getSimpleName) + .map(s -> s.endsWith("Provider") ? s.substring(0, s.length()-8) : s) + .map(SuperFieldsGridItem::new) + .toArray(SuperFieldsGridItem[]::new)); + gridSelect.getGrid().getColumnByKey("nameLength").setAutoWidth(true); + return gridSelect; + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/providers/ItemGridGenerators.java b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/ItemGridGenerators.java new file mode 100644 index 00000000..667c314f --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/ItemGridGenerators.java @@ -0,0 +1,55 @@ +package org.vaadin.miki.demo.providers; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Paragraph; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.textfield.TextField; +import org.vaadin.miki.superfields.lazyload.LazyLoad; + +/** + * Container class for generators to item grid. + * @author miki + * @since 2020-11-18 + */ +public final class ItemGridGenerators { + + /** + * Simple generator (row, column and type name). + * @param type Type to generate cell for. + * @param row Row number. + * @param column Column number. + * @return A grid cell. + */ + public static Component generateParagraph(Class type, int row, int column) { + Paragraph result = new Paragraph(type.getSimpleName()); + result.setTitle(String.format("row %d, column %d", row, column)); + return result; + } + + /** + * More elaborate cell generator. + * @param type Type to generate cell for. + * @param row Row number. + * @param column Column number. + * @return A grid cell. + */ + public static Component generateDiv(Class type, int row, int column) { + return new LazyLoad
(() -> { + final Div result = new Div(); + result.addClassNames("item-grid-cell"); + result.add(new Span(String.format("Row: %d. Column: %d.", row, column))); + final TextField text = new TextField("Class name: ", type.getSimpleName()); + text.setValue(type.getSimpleName()); + text.addClassName("highlighted"); + text.addBlurListener(event -> text.setValue(type.getSimpleName())); + result.add(text); + return result; + }).withId(type.getSimpleName() + "-" + row + "-" + column); + } + + private ItemGridGenerators() { + // no instances allowed + } + +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/providers/ItemGridProvider.java b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/ItemGridProvider.java new file mode 100644 index 00000000..e0e86578 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/ItemGridProvider.java @@ -0,0 +1,61 @@ +package org.vaadin.miki.demo.providers; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import org.vaadin.miki.demo.ComponentProvider; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.superfields.dates.SuperDatePicker; +import org.vaadin.miki.superfields.dates.SuperDateTimePicker; +import org.vaadin.miki.superfields.itemgrid.ItemGrid; +import org.vaadin.miki.superfields.lazyload.ComponentObserver; +import org.vaadin.miki.superfields.lazyload.LazyLoad; +import org.vaadin.miki.superfields.lazyload.ObservedField; +import org.vaadin.miki.superfields.numbers.SuperBigDecimalField; +import org.vaadin.miki.superfields.numbers.SuperDoubleField; +import org.vaadin.miki.superfields.numbers.SuperIntegerField; +import org.vaadin.miki.superfields.numbers.SuperLongField; +import org.vaadin.miki.superfields.tabs.SuperTabs; +import org.vaadin.miki.superfields.unload.UnloadObserver; + +/** + * Provides an {@link ItemGrid}. + * @author miki + * @since 2020-11-18 + */ +@Order(150) +public class ItemGridProvider implements ComponentProvider>> { + + @Override + public ItemGrid> getComponent() { + return new ItemGrid>( + null, + () -> { + VerticalLayout result = new VerticalLayout(); + result.setSpacing(true); + result.setPadding(true); + result.setAlignItems(FlexComponent.Alignment.STRETCH); + result.setWidthFull(); + return result; + }, + ItemGridGenerators::generateParagraph, + event -> { + if (event.isSelected()) + event.getCellInformation().getComponent().getElement().getClassList().add("selected"); + else event.getCellInformation().getComponent().getElement().getClassList().remove("selected"); + }, + SuperIntegerField.class, SuperLongField.class, SuperDoubleField.class, + SuperBigDecimalField.class, SuperDatePicker.class, SuperDateTimePicker.class, + SuperTabs.class, LazyLoad.class, ObservedField.class, + ComponentObserver.class, UnloadObserver.class, ItemGrid.class + ) + .withRowComponentGenerator(rowNumber -> { + HorizontalLayout result = new HorizontalLayout(); + result.setSpacing(true); + result.setAlignItems(FlexComponent.Alignment.CENTER); + result.setPadding(true); + return result; + }); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/providers/MultiClickButtonProvider.java b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/MultiClickButtonProvider.java new file mode 100644 index 00000000..ad9735b8 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/MultiClickButtonProvider.java @@ -0,0 +1,28 @@ +package org.vaadin.miki.demo.providers; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.icon.VaadinIcon; +import org.vaadin.miki.demo.ComponentProvider; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.superfields.buttons.MultiClickButton; +import org.vaadin.miki.superfields.buttons.SimpleButtonState; + +/** + * Provides {@link MultiClickButton}. + * @author miki + * @since 2020-11-17 + */ +@Order(110) +public class MultiClickButtonProvider implements ComponentProvider { + + @Override + public MultiClickButton getComponent() { + return new MultiClickButton( + event -> UI.getCurrent().navigate(""), + new SimpleButtonState("Click to navigate to Info Page").withThemeVariant(ButtonVariant.LUMO_PRIMARY), + new SimpleButtonState("Are you sure?", VaadinIcon.INFO_CIRCLE.create()), + new SimpleButtonState("Really navigate away?", VaadinIcon.INFO.create()).withThemeVariant(ButtonVariant.LUMO_ERROR) + ).withId("multi-click-button"); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/providers/ObservedFieldProvider.java b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/ObservedFieldProvider.java new file mode 100644 index 00000000..745f9a59 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/ObservedFieldProvider.java @@ -0,0 +1,19 @@ +package org.vaadin.miki.demo.providers; + +import org.vaadin.miki.demo.ComponentProvider; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.superfields.lazyload.ObservedField; + +/** + * Provides {@link ObservedField}. + * @author miki + * @since 2020-11-17 + */ +@Order(100) +public class ObservedFieldProvider implements ComponentProvider { + + @Override + public ObservedField getComponent() { + return new ObservedField(); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperBigDecimalFieldProvider.java b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperBigDecimalFieldProvider.java new file mode 100644 index 00000000..395c0abc --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperBigDecimalFieldProvider.java @@ -0,0 +1,29 @@ +package org.vaadin.miki.demo.providers; + +import com.vaadin.flow.data.binder.ValidationResult; +import com.vaadin.flow.data.binder.Validator; +import com.vaadin.flow.data.binder.ValueContext; +import org.vaadin.miki.demo.ComponentProvider; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.superfields.numbers.SuperBigDecimalField; + +import java.math.BigDecimal; + +/** + * Provider for {@link SuperBigDecimalField}. + * @author miki + * @since 2020-11-17 + */ +@Order(40) +public class SuperBigDecimalFieldProvider implements ComponentProvider, Validator { + + @Override + public SuperBigDecimalField getComponent() { + return new SuperBigDecimalField(null, "Big decimal:").withMaximumIntegerDigits(12).withMaximumFractionDigits(3).withMinimumFractionDigits(1).withId("big-decimal").withHelperText("(12 + 3 digits)"); + } + + @Override + public ValidationResult apply(BigDecimal bigDecimal, ValueContext valueContext) { + return bigDecimal != null && bigDecimal.longValue() % 2 == 0 ? ValidationResult.ok() : ValidationResult.error("only values with even integer part are allowed"); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperDatePickerProvider.java b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperDatePickerProvider.java new file mode 100644 index 00000000..036acfb3 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperDatePickerProvider.java @@ -0,0 +1,31 @@ +package org.vaadin.miki.demo.providers; + +import com.vaadin.flow.data.binder.ValidationResult; +import com.vaadin.flow.data.binder.Validator; +import com.vaadin.flow.data.binder.ValueContext; +import org.vaadin.miki.demo.ComponentProvider; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.shared.dates.DatePatterns; +import org.vaadin.miki.superfields.dates.SuperDatePicker; + +import java.time.DayOfWeek; +import java.time.LocalDate; + +/** + * Provides {@link SuperDatePicker}. + * @author miki + * @since 2020-11-18 + */ +@Order(50) +public class SuperDatePickerProvider implements ComponentProvider, Validator { + + @Override + public SuperDatePicker getComponent() { + return new SuperDatePicker("Pick a date:").withDatePattern(DatePatterns.YYYY_MM_DD).withValue(LocalDate.now()).withHelperText("(default date pattern is YYYY-MM-DD)"); + } + + @Override + public ValidationResult apply(LocalDate localDate, ValueContext valueContext) { + return localDate != null && localDate.getDayOfWeek() == DayOfWeek.MONDAY ? ValidationResult.error("Mondays are not allowed") : ValidationResult.ok(); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperDateTimePickerProvider.java b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperDateTimePickerProvider.java new file mode 100644 index 00000000..7ee92c25 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperDateTimePickerProvider.java @@ -0,0 +1,30 @@ +package org.vaadin.miki.demo.providers; + +import com.vaadin.flow.data.binder.ValidationResult; +import com.vaadin.flow.data.binder.Validator; +import com.vaadin.flow.data.binder.ValueContext; +import org.vaadin.miki.demo.ComponentProvider; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.shared.dates.DatePatterns; +import org.vaadin.miki.superfields.dates.SuperDateTimePicker; + +import java.time.LocalDateTime; + +/** + * Provides {@link SuperDateTimePicker}. + * @author miki + * @since 2020-11-17 + */ +@Order(60) +public class SuperDateTimePickerProvider implements ComponentProvider, Validator { + + @Override + public SuperDateTimePicker getComponent() { + return new SuperDateTimePicker("Pick a date and time:").withDatePattern(DatePatterns.M_D_YYYY_SLASH).withValue(LocalDateTime.now()).withHelperText("(default date pattern is month/day/year)"); + } + + @Override + public ValidationResult apply(LocalDateTime localDateTime, ValueContext valueContext) { + return localDateTime != null && localDateTime.getDayOfYear() < 50 ? ValidationResult.error("the first 50-or-so days of each year are not allowed") : ValidationResult.ok(); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperDoubleFieldProvider.java b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperDoubleFieldProvider.java new file mode 100644 index 00000000..e0907fa5 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperDoubleFieldProvider.java @@ -0,0 +1,26 @@ +package org.vaadin.miki.demo.providers; + +import com.vaadin.flow.data.binder.ValidationResult; +import com.vaadin.flow.data.binder.Validator; +import com.vaadin.flow.data.binder.ValueContext; +import org.vaadin.miki.demo.ComponentProvider; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.superfields.numbers.SuperDoubleField; + +/** + * Provider for {@link SuperDoubleField}. + * @author miki + * @since 2020-11-17 + */ +@Order(30) +public class SuperDoubleFieldProvider implements ComponentProvider, Validator { + @Override + public SuperDoubleField getComponent() { + return new SuperDoubleField(null, "Double:").withMaximumIntegerDigits(8).withMaximumFractionDigits(4).withHelperText("(8 + 4 digits)"); + } + + @Override + public ValidationResult apply(Double number, ValueContext valueContext) { + return number != null && number > 50 ? ValidationResult.ok() : ValidationResult.error("only even above 50 are accepted"); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/SuperFieldsGridItem.java b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperFieldsGridItem.java similarity index 84% rename from demo-v14/src/main/java/org/vaadin/miki/SuperFieldsGridItem.java rename to demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperFieldsGridItem.java index 1e1753df..01a2fd1f 100644 --- a/demo-v14/src/main/java/org/vaadin/miki/SuperFieldsGridItem.java +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperFieldsGridItem.java @@ -1,4 +1,4 @@ -package org.vaadin.miki; +package org.vaadin.miki.demo.providers; /** * Simple data class to showcase in grid select demo. @@ -12,8 +12,8 @@ public class SuperFieldsGridItem { private String name; - SuperFieldsGridItem(Class type) { - this.name = type.getSimpleName(); + SuperFieldsGridItem(String name) { + this.name = name; this.nameLength = this.name.length(); } diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperIntegerFieldProvider.java b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperIntegerFieldProvider.java new file mode 100644 index 00000000..376364fa --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperIntegerFieldProvider.java @@ -0,0 +1,27 @@ +package org.vaadin.miki.demo.providers; + +import com.vaadin.flow.data.binder.ValidationResult; +import com.vaadin.flow.data.binder.Validator; +import com.vaadin.flow.data.binder.ValueContext; +import org.vaadin.miki.demo.ComponentProvider; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.superfields.numbers.SuperIntegerField; + +/** + * Provider for {@link SuperIntegerField}. + * @author miki + * @since 2020-11-17 + */ +@Order(10) +public class SuperIntegerFieldProvider implements ComponentProvider, Validator { + + @Override + public SuperIntegerField getComponent() { + return new SuperIntegerField(null, "Integer:").withMaximumIntegerDigits(6).withHelperText("(6 digits)"); + } + + @Override + public ValidationResult apply(Integer integer, ValueContext valueContext) { + return integer != null && integer % 2 == 0 ? ValidationResult.ok() : ValidationResult.error("only even values are allowed"); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperLongFieldProvider.java b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperLongFieldProvider.java new file mode 100644 index 00000000..0dc5e0e6 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperLongFieldProvider.java @@ -0,0 +1,27 @@ +package org.vaadin.miki.demo.providers; + +import com.vaadin.flow.data.binder.ValidationResult; +import com.vaadin.flow.data.binder.Validator; +import com.vaadin.flow.data.binder.ValueContext; +import org.vaadin.miki.demo.ComponentProvider; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.superfields.numbers.SuperLongField; + +/** + * Provider for {@link SuperLongField}. + * @author miki + * @since 2020-11-17 + */ +@Order(20) +public class SuperLongFieldProvider implements ComponentProvider, Validator { + + @Override + public SuperLongField getComponent() { + return new SuperLongField(null, "Long:").withMaximumIntegerDigits(11).withId("long").withHelperText("(11 digits)"); + } + + @Override + public ValidationResult apply(Long integer, ValueContext valueContext) { + return integer != null && integer % 2 == 0 ? ValidationResult.ok() : ValidationResult.error("only even values are allowed"); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperTabsProvider.java b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperTabsProvider.java new file mode 100644 index 00000000..c9b2f72c --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperTabsProvider.java @@ -0,0 +1,30 @@ +package org.vaadin.miki.demo.providers; + +import com.vaadin.flow.component.html.Paragraph; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import org.vaadin.miki.demo.ComponentProvider; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.superfields.tabs.SuperTabs; + +import java.util.function.Supplier; + +/** + * Provides {@link SuperTabs}. + * @author miki + * @since 2020-11-17 + */ +@Order(90) +public class SuperTabsProvider implements ComponentProvider> { + + @Override + public SuperTabs getComponent() { + + return new SuperTabs((Supplier) HorizontalLayout::new) + .withTabContentGenerator(s -> new Paragraph("Did you know? All SuperFields are "+s)) + .withItems( + "Java friendly", "Super-configurable", "Open source", + "Fun to use", "Reasonably well documented" + ).withId("super-tabs"); + } + +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperTextAreaProvider.java b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperTextAreaProvider.java new file mode 100644 index 00000000..6e37950e --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperTextAreaProvider.java @@ -0,0 +1,27 @@ +package org.vaadin.miki.demo.providers; + +import com.vaadin.flow.data.binder.ValidationResult; +import com.vaadin.flow.data.binder.Validator; +import com.vaadin.flow.data.binder.ValueContext; +import org.vaadin.miki.demo.ComponentProvider; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.superfields.text.SuperTextArea; + +/** + * Provides {@link SuperTextArea}. + * @author miki + * @since 2020-11-17 + */ +@Order(80) +public class SuperTextAreaProvider implements ComponentProvider, Validator { + + @Override + public SuperTextArea getComponent() { + return new SuperTextArea("Type a lot of something:").withPlaceholder("(nothing typed)").withId("super-text-area").withHelperText("(anything without ? goes)"); + } + + @Override + public ValidationResult apply(String s, ValueContext valueContext) { + return s != null && s.contains("?") ? ValidationResult.error("? is not allowed") : ValidationResult.ok(); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperTextFieldProvider.java b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperTextFieldProvider.java new file mode 100644 index 00000000..e96be6d8 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/SuperTextFieldProvider.java @@ -0,0 +1,27 @@ +package org.vaadin.miki.demo.providers; + +import com.vaadin.flow.data.binder.ValidationResult; +import com.vaadin.flow.data.binder.Validator; +import com.vaadin.flow.data.binder.ValueContext; +import org.vaadin.miki.demo.ComponentProvider; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.superfields.text.SuperTextField; + +/** + * Provides a {@link SuperTextField}. + * @author miki + * @since 2020-11-18 + */ +@Order(70) +public class SuperTextFieldProvider implements ComponentProvider, Validator { + + @Override + public SuperTextField getComponent() { + return new SuperTextField("Type something:").withPlaceholder("(nothing typed)").withId("super-text-field").withHelperText("(anything without a ! goes)"); + } + + @Override + public ValidationResult apply(String s, ValueContext valueContext) { + return s != null && s.contains("!") ? ValidationResult.error("! is not allowed") : ValidationResult.ok(); + } +} diff --git a/demo-v14/src/main/java/org/vaadin/miki/demo/providers/UnloadObserverProvider.java b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/UnloadObserverProvider.java new file mode 100644 index 00000000..fb62f828 --- /dev/null +++ b/demo-v14/src/main/java/org/vaadin/miki/demo/providers/UnloadObserverProvider.java @@ -0,0 +1,19 @@ +package org.vaadin.miki.demo.providers; + +import org.vaadin.miki.demo.ComponentProvider; +import org.vaadin.miki.demo.Order; +import org.vaadin.miki.superfields.unload.UnloadObserver; + +/** + * Provides {@link UnloadObserver}. + * @author miki + * @since 2020-11-18 + */ +@Order(140) +public class UnloadObserverProvider implements ComponentProvider { + + @Override + public UnloadObserver getComponent() { + return UnloadObserver.get().withoutQueryingOnUnload(); + } +} diff --git a/pom.xml b/pom.xml index f796be6e..fcd4c184 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.vaadin.miki superfields-parent - 0.9.1 + 0.9.2 superfields demo-v14 diff --git a/superfields/pom.xml b/superfields/pom.xml index 69529c73..f22343a8 100644 --- a/superfields/pom.xml +++ b/superfields/pom.xml @@ -8,7 +8,7 @@ superfields SuperFields Code for various V14+ fields and other components. - 0.9.1 + 0.9.2 10 diff --git a/superfields/release-notes.md b/superfields/release-notes.md index e60e46ed..716a7664 100644 --- a/superfields/release-notes.md +++ b/superfields/release-notes.md @@ -1,3 +1,11 @@ +# 0.9.2 - Bugfixes to number fields +## New features and enhancements +(nothing reported) +## Changes to API +(nothing reported) +## Bug fixes +* \#241 - [SuperBigDecimalField loses validation information](https://github.com/vaadin-miki/super-fields/issues/241) +* \#243 - [Text selection mixin throws JS errors when used in Grid](https://github.com/vaadin-miki/super-fields/issues/243) # 0.9.1 - Vaadin 14.4 ## New features and enhancements * \#224 - [Changing locale should preserve number precision information](https://github.com/vaadin-miki/super-fields/issues/224) diff --git a/superfields/src/main/java/org/vaadin/miki/shared/text/TextSelectionDelegate.java b/superfields/src/main/java/org/vaadin/miki/shared/text/TextSelectionDelegate.java index b004f55e..86958ec1 100644 --- a/superfields/src/main/java/org/vaadin/miki/shared/text/TextSelectionDelegate.java +++ b/superfields/src/main/java/org/vaadin/miki/shared/text/TextSelectionDelegate.java @@ -27,7 +27,9 @@ * The source component must implement {@link CanSelectText} and {@link CanReceiveSelectionEventsFromClient} and delegate them * to this object. * The client-side component must mix in {@code text-selection-mixin.js} or otherwise react to needed JS method calls. - * Finally, the delegating class must implement a method {@code @ClientCallable void selectionChanged(int, int, String)}. + * Finally, the delegating class must implement methods: {@code @ClientCallable void selectionChanged(int, int, String)} and + * {@code @ClientCallable void reinitialiseEventListening()}. The first method should call this object's + * {@link #fireTextSelectionEvent(boolean, int, int, String)} and the second method should just be delegated to this object. * * @author miki * @since 2020-06-01 @@ -115,7 +117,6 @@ public void selectNone() { } } - @Override public void select(int from, int to) { if(from <= to) @@ -182,4 +183,12 @@ public void setReceivingSelectionEventsFromClient(boolean receivingSelectionEven this.receivingSelectionEventsFromClient = receivingSelectionEventsFromClient; this.informClientAboutSendingEvents(receivingSelectionEventsFromClient); } + + /** + * This method should be called in response to {@code @ClientCallable void reinitialiseListeners()} on the owning object. + */ + // fix for https://github.com/vaadin-miki/super-fields/issues/243 and the way components are initialised inside Grid + public void reinitialiseListeners() { + this.informClientAboutSendingEvents(this.isReceivingSelectionEventsFromClient()); + } } diff --git a/superfields/src/main/java/org/vaadin/miki/superfields/numbers/AbstractSuperNumberField.java b/superfields/src/main/java/org/vaadin/miki/superfields/numbers/AbstractSuperNumberField.java index 4ca3ce51..5eb01b35 100644 --- a/superfields/src/main/java/org/vaadin/miki/superfields/numbers/AbstractSuperNumberField.java +++ b/superfields/src/main/java/org/vaadin/miki/superfields/numbers/AbstractSuperNumberField.java @@ -349,6 +349,10 @@ protected void setPresentationValue(T number) { String formatted = number == null ? "" : this.format.format(number); LOGGER.debug("value {} to be presented as {} with {} decimal digits", number, formatted, this.format.getMaximumFractionDigits()); this.field.setValue(formatted); + // fixes #241 caused by a Vaadin bug https://github.com/vaadin/vaadin-text-field/issues/547 + this.field.getElement().getNode().runWhenAttached(ui -> ui.beforeClientResponse(this.field, context -> + this.field.getElement().setProperty("invalid", super.isInvalid()) + )); } /** @@ -586,6 +590,7 @@ public String getErrorMessage() { @Override public void setInvalid(boolean invalid) { + super.setInvalid(invalid); this.field.setInvalid(invalid); } diff --git a/superfields/src/main/java/org/vaadin/miki/superfields/text/SuperTextArea.java b/superfields/src/main/java/org/vaadin/miki/superfields/text/SuperTextArea.java index e0880908..ff7dad9a 100644 --- a/superfields/src/main/java/org/vaadin/miki/superfields/text/SuperTextArea.java +++ b/superfields/src/main/java/org/vaadin/miki/superfields/text/SuperTextArea.java @@ -107,4 +107,9 @@ private void selectionChanged(int start, int end, String selection) { this.delegate.fireTextSelectionEvent(true, start, end, selection); } + @ClientCallable + private void reinitialiseListening() { + this.delegate.reinitialiseListeners(); + } + } diff --git a/superfields/src/main/java/org/vaadin/miki/superfields/text/SuperTextField.java b/superfields/src/main/java/org/vaadin/miki/superfields/text/SuperTextField.java index 2964356f..0a756aeb 100644 --- a/superfields/src/main/java/org/vaadin/miki/superfields/text/SuperTextField.java +++ b/superfields/src/main/java/org/vaadin/miki/superfields/text/SuperTextField.java @@ -98,6 +98,11 @@ private void selectionChanged(int start, int end, String selection) { this.delegate.fireTextSelectionEvent(true, start, end, selection); } + @ClientCallable + private void reinitialiseListening() { + this.delegate.reinitialiseListeners(); + } + @Override public boolean isReceivingSelectionEventsFromClient() { return this.delegate.isReceivingSelectionEventsFromClient(); diff --git a/superfields/src/main/resources/META-INF/resources/frontend/super-text-field.js b/superfields/src/main/resources/META-INF/resources/frontend/super-text-field.js index 87c0b44d..d1292596 100644 --- a/superfields/src/main/resources/META-INF/resources/frontend/super-text-field.js +++ b/superfields/src/main/resources/META-INF/resources/frontend/super-text-field.js @@ -7,6 +7,7 @@ class SuperTextField extends TextSelectionMixin.to(TextFieldElement) { setCallingServer(callingServer) { console.log('STF: configuring event listeners; callingServer flag is '+callingServer); + console.log('STF: this now refers to '+this); this.listenToEvents(this.inputElement, this, callingServer); } diff --git a/superfields/src/main/resources/META-INF/resources/frontend/text-selection-mixin.js b/superfields/src/main/resources/META-INF/resources/frontend/text-selection-mixin.js index 3d469f5f..9b700539 100644 --- a/superfields/src/main/resources/META-INF/resources/frontend/text-selection-mixin.js +++ b/superfields/src/main/resources/META-INF/resources/frontend/text-selection-mixin.js @@ -46,22 +46,33 @@ export class TextSelectionMixin { listenToEvents(inputComponent, webComponent, notifyServer) { console.log('TSM: setting up text selection for component <'+webComponent.tagName+'>'); - if (webComponent.selectionMixin === undefined) { - webComponent.selectionMixin = { - input: inputComponent, - callServer: notifyServer, - startsAt: -1, - endsAt: -1, - selection: '' + if (inputComponent === undefined) { + console.log('TSM: input component is undefined, attempting to find it from shadow root/input element'); + inputComponent = webComponent.inputElement; + if (inputComponent === undefined) { + console.warn('TSM: no input component, server will reinitialise this component shortly'); + // this trick has been suggested by the magnificent Erik Lumme, thank you! + webComponent.$server.reinitialiseListening(); } - - const listener = () => webComponent.updateData(webComponent.selectionMixin, webComponent); - inputComponent.addEventListener('mouseup', listener); - inputComponent.addEventListener('keyup', listener); - inputComponent.addEventListener('mouseleave', listener); } - else { - webComponent.selectionMixin.callServer = notifyServer; + if (inputComponent !== undefined) { + console.log('TSM: input component is ' + inputComponent); + if (webComponent.selectionMixin === undefined) { + webComponent.selectionMixin = { + input: inputComponent, + callServer: notifyServer, + startsAt: -1, + endsAt: -1, + selection: '' + } + + const listener = () => webComponent.updateData(webComponent.selectionMixin, webComponent); + inputComponent.addEventListener('mouseup', listener); + inputComponent.addEventListener('keyup', listener); + inputComponent.addEventListener('mouseleave', listener); + } else { + webComponent.selectionMixin.callServer = notifyServer; + } } } } diff --git a/superfields/src/test/java/org/vaadin/miki/superfields/numbers/BaseTestsForIntegerNumbers.java b/superfields/src/test/java/org/vaadin/miki/superfields/numbers/BaseTestsForIntegerNumbers.java index 0a72c8d0..d5c3401c 100644 --- a/superfields/src/test/java/org/vaadin/miki/superfields/numbers/BaseTestsForIntegerNumbers.java +++ b/superfields/src/test/java/org/vaadin/miki/superfields/numbers/BaseTestsForIntegerNumbers.java @@ -3,6 +3,10 @@ import com.github.mvysny.kaributesting.v10.MockVaadin; import com.vaadin.flow.component.ComponentEvent; import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.data.binder.Binder; +import com.vaadin.flow.data.binder.ValidationResult; +import com.vaadin.flow.data.binder.Validator; +import com.vaadin.flow.data.binder.ValueContext; import com.vaadin.flow.shared.Registration; import org.junit.After; import org.junit.Assert; @@ -14,6 +18,7 @@ import java.util.HashSet; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; @@ -25,6 +30,62 @@ */ class BaseTestsForIntegerNumbers { + public static final class NumberWrapper { + private X number; + + public X getNumber() { + return number; + } + + public void setNumber(X number) { + this.number = number; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NumberWrapper that = (NumberWrapper) o; + return Objects.equals(getNumber(), that.getNumber()); + } + + @Override + public int hashCode() { + return Objects.hash(getNumber()); + } + } + + public static final class NumberValidator implements Validator { + + private X referenceValue; + + @Override + public ValidationResult apply(X x, ValueContext valueContext) { + return Objects.equals(x, this.referenceValue) ? ValidationResult.ok() : ValidationResult.error("nope"); + } + + public X getReferenceValue() { + return referenceValue; + } + + public void setReferenceValue(X referenceValue) { + this.referenceValue = referenceValue; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NumberValidator that = (NumberValidator) o; + return Objects.equals(getReferenceValue(), that.getReferenceValue()); + } + + @Override + public int hashCode() { + return Objects.hash(getReferenceValue()); + } + } + private final Supplier> fieldSupplier; private final T baseTestNumber; private final T negativeTestNumber; @@ -271,4 +332,35 @@ public void testChangesInLocaleDoNotAffectPrecision() { Assert.assertEquals("max integer digits must not change when changing locale", maxDigits, this.field.getMaximumIntegerDigits()); } + @Test + public void testBinderAndValidation() { + final NumberWrapper wrapper = new NumberWrapper<>(); + @SuppressWarnings("unchecked") + final Binder> binder = new Binder<>((Class>)(Class)NumberWrapper.class); + final NumberValidator validator = new NumberValidator<>(); + validator.setReferenceValue(this.baseTestNumber); + this.field.setNegativeValueAllowed(true); + binder.forField(this.field) + .withValidator(validator) + .bind("number"); + binder.setBean(wrapper); + Assert.assertFalse("validator must fail with no number", binder.isValid()); + wrapper.setNumber(this.baseTestNumber); + binder.setBean(wrapper); + Assert.assertTrue("validator must not fail with the correct number", binder.isValid()); + wrapper.setNumber(this.negativeTestNumber); + binder.setBean(wrapper); + Assert.assertFalse("validator must fail with wrong number", binder.isValid()); + + this.field.setValue(this.baseTestNumber); + Assert.assertTrue("validator must be ok after field was changed to good number", binder.isValid()); + + validator.setReferenceValue(this.negativeTestNumber); + Assert.assertFalse("validator must not be ok after it was changed", binder.isValid()); + validator.setReferenceValue(this.baseTestNumber); + this.field.setValue(this.baseTestNumber); + Assert.assertTrue("validator must be ok", binder.isValid()); + + } + }