Skip to content

Commit

Permalink
CollectionField ready (#311)
Browse files Browse the repository at this point in the history
#303 CollectionField done, together with helpers, documentation and demo
  • Loading branch information
vaadin-miki authored Sep 10, 2021
1 parent c12b9fe commit 9f58b85
Show file tree
Hide file tree
Showing 28 changed files with 1,658 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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.superfields.collections.CollectionField;
import org.vaadin.miki.superfields.collections.CollectionValueComponentProvider;
import org.vaadin.miki.superfields.collections.IndexedButton;
import org.vaadin.miki.superfields.text.SuperTextField;
import org.vaadin.miki.superfields.util.CollectionComponentProviders;

import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

/**
* Builds content for {@link CollectionField}.
* @author miki
* @since 2021-09-10
*/
@Order(5)
public class HasCollectionValueComponentProviderBuilder implements ContentBuilder<CollectionField<String, List<String>>> {

@Override
public void buildContent(CollectionField<String, List<String>> component, Consumer<Component[]> callback) {
final Map<CollectionValueComponentProvider<String, ?>, String> providersWithCaptions = new LinkedHashMap<>();
// three options available
final CollectionValueComponentProvider<String, SuperTextField> textFields = CollectionComponentProviders::textField;
providersWithCaptions.put(textFields, "Just text fields");
providersWithCaptions.put(CollectionComponentProviders.rowWithRemoveButtonFirst(
CollectionComponentProviders::textField, "Remove"
), "Text field with remove button");
providersWithCaptions.put(CollectionComponentProviders.columnWithHeaderAndFooterRows(
CollectionComponentProviders::textField,
Collections.emptyList(),
Arrays.asList(
CollectionComponentProviders.removeButton("Remove"),
(index, controller) -> new IndexedButton("Add below", event -> controller.add(index+1))
)
), "Text field with remove and add below buttons");

// configure the combo box
final ComboBox<CollectionValueComponentProvider<String, ?>> box = new ComboBox<>("Choose item layout:", providersWithCaptions.keySet());
box.setItemLabelGenerator(providersWithCaptions::get);
box.addValueChangeListener(event -> component.setCollectionValueComponentProvider(event.getValue()));
box.setAllowCustomValue(false);
box.setValue(textFields);

callback.accept(new Component[]{box});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.vaadin.miki.demo.builders;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HasEnabled;
import com.vaadin.flow.component.checkbox.Checkbox;
import org.vaadin.miki.demo.ContentBuilder;
import org.vaadin.miki.demo.Order;

import java.util.function.Consumer;

/**
* Builds content for {@link HasEnabled}.
* @author miki
* @since 2021-09-04
*/
@Order(19)
public class HasEnabledBuilder implements ContentBuilder<HasEnabled> {

@Override
public void buildContent(HasEnabled component, Consumer<Component[]> callback) {
callback.accept(new Component[]{
new Checkbox("Mark component as disabled?", event -> component.setEnabled(!event.getValue()))
});

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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.collections.CollectionField;
import org.vaadin.miki.superfields.text.SuperTextField;
import org.vaadin.miki.superfields.util.CollectionComponentProviders;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
* Provides a {@link CollectionField} for {@code List<String>}s.
* @author miki
* @since 2021-08-25
*/
@Order(145)
public class CollectionFieldProvider implements ComponentProvider<CollectionField<String, List<String>>>, Validator<List<String>> {
@Override
public CollectionField<String, List<String>> getComponent() {
return new CollectionField<>(
ArrayList::new,
CollectionComponentProviders.columnWithHeaderAndFooterRows(
Arrays.asList(
CollectionComponentProviders.removeAllButton("Clear"),
CollectionComponentProviders.addFirstButton("Add as first")
),
Collections.singletonList(CollectionComponentProviders.addLastButton("Add as last"))),
CollectionComponentProviders.rowWithRemoveButtonFirst(CollectionComponentProviders.labelledField(SuperTextField::new, "Value"), "Remove")
);
}

@Override
public ValidationResult apply(List<String> strings, ValueContext valueContext) {
return strings.size() != 3 ? ValidationResult.error("the list must have exactly 3 elements") : ValidationResult.ok();
}
}
20 changes: 20 additions & 0 deletions superfields/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@ Both components behave funky when changing locale at runtime if their calendars

**Please note:** only `SuperDatePicker` has support for text selection API.

## `CollectionField` and related helpers

### `CollectionField`

Almost out-of-the-box, fully configurable `CustomField` for data of type `List<X>`. Supports custom layouts, fields, removing individual or all elements and adding at specified indices.

To help navigate around functional interfaces `CollectionComponentProviders` contains some common use cases, like buttons for removing, clearing or adding elements.

### `HasIndex` and `IndexedButton`

While intended to work with `CollectionField`, here is a general interface for anything that needs an index (an integer number). `IndexedButton` does nothing by itself, but it is a `Button` that implements `HasIndex`. It is used e.g. to represent a button that removes an element from a collection.

## `HasHeader`, `HasFooter` and layout helpers

A three-part layout, with header, content and footer, is quite common in UX design. Two helper classes are offered:
* `HeaderFooterFieldWrapper` - a fully functional `CustomField` that has a header and a footer, and the content is just the field;
* `HeaderFooterLayoutWrapper` - a fully functional layout (`HasComponents`) that has a header and a footer, and the content is a layout to which `HasComponents` method calls are delegated.

Both wrappers attempt to be API-transparent and can replace already existing layouts or fields (for example, replacing a `SuperTextField` with a `HeaderFooterFieldWrapper` should just result in a header and footer around the text field, and no other changes should be needed).

## Select fields

### `ItemGrid`
Expand Down
6 changes: 6 additions & 0 deletions superfields/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@
<version>1.3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.30</version>
<scope>test</scope>
</dependency>
</dependencies>

<distributionManagement>
Expand Down
24 changes: 24 additions & 0 deletions superfields/src/main/java/org/vaadin/miki/markers/HasIndex.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.vaadin.miki.markers;

/**
* Marker interface for objects having an index.
* It is left out for implementations to define what that index means.
*
* @author miki
* @since 2021-08-18
*/
public interface HasIndex {

/**
* Returns the current index of the object.
* @return Current index.
*/
int getIndex();

/**
* Changes the index of the object.
* @param index New index.
*/
void setIndex(int index);

}
43 changes: 43 additions & 0 deletions superfields/src/main/java/org/vaadin/miki/markers/HasReadOnly.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.vaadin.miki.markers;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HasEnabled;
import com.vaadin.flow.component.HasValue;

/**
* Marker interface for objects that can be in read-only state without having value.
* @author miki
* @since 2021-09-04
*/
public interface HasReadOnly {

/**
* Checks whether this object is in read-only mode.
* @return Whether the object is in read-only mode.
*/
boolean isReadOnly();

/**
* Sets the new read-only state.
* @param readOnly Whether the object should be in read-only mode.
*/
void setReadOnly(boolean readOnly);

/**
* Helper method to update read-only state of a component if it supports the method.
* @param readOnly New state.
* @param component Component.
* If it implements {@link HasReadOnly} or {@link HasValue}, the state will be updated.
* If it implements {@link HasEnabled}, read-only means disabled.
* Otherwise, nothing happens.
*/
static void setReadOnly(boolean readOnly, Component component) {
if(component instanceof HasReadOnly)
((HasReadOnly) component).setReadOnly(readOnly);
else if(component instanceof HasValue)
((HasValue<?, ?>) component).setReadOnly(readOnly);
else if(component instanceof HasEnabled)
((HasEnabled) component).setEnabled(!readOnly);
}

}
24 changes: 24 additions & 0 deletions superfields/src/main/java/org/vaadin/miki/markers/HasText.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.vaadin.miki.markers;

/**
* Marker interface for objects that have a String property called {@code text}.
* It is up to the implementation to define what this means.
*
* @author miki
* @since 2021-08-31
*/
public interface HasText {

/**
* Sets text of this object.
* @param text Text to set.
*/
void setText(String text);

/**
* Returns current text of this object.
* @return Current text.
*/
String getText();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.vaadin.miki.markers;

/**
* Mixin interface for easy chaining of {@link #setIndex(int)}.
* @param <SELF> This.
*
* @author miki
* @since 2021-08-31
*/
public interface WithIndexMixin<SELF extends HasIndex> extends HasIndex {

/**
* Chains {@link #setIndex(int)} and returns itself.
* @param index Index to set.
* @return This.
*/
@SuppressWarnings("unchecked")
default SELF withIndex(int index) {
this.setIndex(index);
return (SELF) this;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,7 @@
* @author miki
* @since 2020-07-07
*/
public interface WithTextMixin<SELF extends WithTextMixin<SELF>> {

/**
* Sets text of this object.
* @param text Text to set.
*/
void setText(String text);

/**
* Returns current text of this object.
* @return Current text.
*/
String getText();
public interface WithTextMixin<SELF extends HasText> extends HasText {

/**
* Chains {@link #setText(String)} and returns itself.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.vaadin.miki.superfields.collections;

import com.vaadin.flow.component.Component;

import java.io.Serializable;

/**
* Catch-all interface for everything related to producing {@link Component}s for a {@link CollectionField}.
* @param <C> Type of component provided.
* @author miki
* @since 2021-09-10
*/
@FunctionalInterface
public interface CollectionComponentProvider<C extends Component> extends Serializable {

/**
* Constructs the component for an object at given index.
* @param index Current index of {@code object} in the collection.
* @param controller A {@link CollectionController} to be used for callbacks.
* @return Non-{@code null} component.
*/
C provideComponent(int index, CollectionController controller);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.vaadin.miki.superfields.collections;

import java.io.Serializable;

/**
* Marker interface for all components that can control a collection.
* Note: this is not generic in any way to allow reusing one controller among many components.
* @author miki
* @since 2021-08-14
*/
public interface CollectionController extends Serializable {

/**
* Returns the current size of the collection.
* @return A non-negative number.
*/
int size();

/**
* Checks if the collection is empty.
* @return {@code true} if {@link #size()} returns {@code 0}, {@code false} otherwise.
*/
default boolean isEmpty() {
return this.size() == 0;
}

/**
* Clears the collection and removes all elements from it.
*/
void removeAll();

/**
* Adds a new component at a specified position, moving subsequent elements by one.
* @param atIndex Index to add at.
*/
void add(int atIndex);

/**
* Adds a new component at the end of current list.
* By default, it has the same effect as calling {@link #add(int)} with {@link #size()} as parameter.
*/
default void add() {
this.add(this.size());
}

/**
* Removes a component at given index. All subsequent components are moved forward by one.
* @param atIndex Index to remove component at.
*/
void remove(int atIndex);

/**
* Removes the last component.
* By default, it has the same effect as calling {@link #remove(int)} with {@link #size()}{@code - 1} as parameter.
*/
default void remove() {
this.remove(this.size() - 1);
}
}
Loading

0 comments on commit 9f58b85

Please sign in to comment.