miércoles, 2 de enero de 2019

03. Routing

The examples from this post have been copied from the vaadin tutorial
This post is only instructional and is a bit boring as no application is created, but you may take it as simple reference card. Sorry folks!

1. @Route annotation

Here is a simple example of this annotation


1
2
3
4
5
6
@Route("")
public class HelloWorld extends Div {
  public HelloWorld() {
    setText("Hello world");
  }
}

This is shown in the URL http://localhost:8080/myProjectName

Where myProjectName can be used or not depending on the type of deployment (maybe in Jetty is not used but in Tomcat should be used)

@Route ("some/path") references http://localhost:8080/myProjectName/some/path

If the @Route annotation is not present, this criterion is used:

  1. The route is the class name but decapitalized ("MyClass" -> "myclass")
  2. The suffix "view" will be trimmed ("PersonView" -> "person")
  3. The suffix "main" will be trimmed ("MainPerson"-> "person",  "MainView"-> "")

2. Events

2.1 Before leaving a page...

Before leaving a page can be detected by means of "BeforeLeaveEvent" only if:
  1. The UI implements the "BeforeLeaveObserver" interface (preferred) or
  2. The UI executes the method addBeforeLeaveListener(BeforeLeaveListener)
This enables to change, deny or delay the navigation.

"BeforeLeaveEvent" has a method "postpone" that puts off the navigation until a condition is met. Then the "ContinueNavigationAction" method resumes the transition. Here is an example from vaadin manual


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class SignupForm extends Div implements BeforeLeaveObserver {
    @Override
    public void beforeLeave(BeforeLeaveEvent event) {
        if (this.hasChanges()) {
            ContinueNavigationAction action = event.postpone();
            ConfirmDialog.build("Are you sure you want to leave this page?")
                    .ifAccept(action::proceed).show();
        }
    }

    private boolean hasChanges() {
        // no-op implementation
        return true;
    }
}

2.2. Before entering a page ...

Before entering a page can be detected by means of "BeforeEnterEvent" only if
  1. The UI implements the "BeforeEnterObserverinterface (preferred) or
  2. The UI executes the method addBeforeEnterListener(BeforeEnterListener)
This enables to redirect or deny the navigation in case the user has not been logged or has no permission or maybe if there are no data to display.

This event has a method called "rerouteTo" in order to redirect the navigation. Here is an example from vaadin manual


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Class to be rerouted to..
@Route("no-items")
public class NoItemsView extends Div {
    public NoItemsView() {
        setText("No items found.");
    }
}

//Sender class..
@Route("blog")
public class BlogList extends Div implements BeforeEnterObserver {
    @Override
    public void beforeEnter(BeforeEnterEvent event) {
        // implementation omitted
        Object record = getItem();
        if (record == null)  event.rerouteTo(NoItemsView.class);
     }

    private Object getItem() {
        // no-op implementation
        return null;
    }
}

2.3. After navigating to a page ...

After navigating to a page can be detected by means of "AfterNavigationEvent" only if
  1. The UI implements the "AfterNavigationObserverinterface (preferred) or
  2. The UI executes the method addAfterNavigationListener(AfterNavigationListener)
This enables to updating various parts of the UI once the actual navigation has been completed. Here is an example from vaadin manual


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class SideMenu extends Div implements AfterNavigationObserver {
    Anchor blog = new Anchor("blog", "Blog");

    @Override
    public void afterNavigation(AfterNavigationEvent event) {
        boolean active = event.getLocation().getFirstSegment()
                .equals(blog.getHref());
        blog.getElement().getClassList().set("active", active);
    }
}

The order of the events are:

  1. BeforeLeaveEvent
  2. BeforeEnterEvent
  3. AfterNavigationEvent

3. Router Layouts

A RouterLayout component:

  1. Should implement RouterLayout interface
  2. Supplies his <body> for containing other components that have the property "layout" of the @Route annotation pointing to the parent component (the one implementing the  RouterLayout interface)
  3. Can nest multiple components, displaying only one child, when navigating from one child to another.

Here is the vaadin manual example of a component that uses a RouterLayout as parent. Notice the "@Route(layout property)"


1
2
3
4
@Tag("div")
@Route(value="company", layout=MainLayout.class)
public class CompanyComponent extends Component { 
}

3.1 Nested Router Layouts

There can be a MainLayout (implementing the RouterLayout interface) that can nest other RouterLayouts. The nested RouterLayouts uses the @ParentLayout that points to its parent Here is the vaadin tutorial example


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//1. Main RouterLayout 
public class MainLayout extends Div implements RouterLayout {
}

//2. Nested Routed Layout in the MainLayout
@ParentLayout(MainLayout.class)
public class MenuBar extends Div implements RouterLayout {
    public MenuBar() {
        addMenuElement(TutorialView.class, "Tutorial");
        addMenuElement(IconsView.class, "Icons");
    }
    private void addMenuElement(Class<? extends Component> navigationTarget,
            String name) {
        // implementation omitted
    }
}

//2.1 Nested layout 
@Route(value = "tutorial", layout = MenuBar.class)
public class TutorialView extends Div {
}

//2.2 Another nested layout
@Route(value="icons", layout = MenuBar.class)
public class IconsView extends Div {
}

3.2 Route prefixes to RouterLayouts

This is accomplished using the @RoutePrefix("prefix-route") to the RouterLayout. In this example from vaadin manual, the child component can be accessed by the complete route "some/path"


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//Access this component with route to "some/path"
@Route(value = "path", layout = SomeParent.class)
public class PathComponent extends Div {
    // Implementation omitted
}

// Adds the prefix "some" to the routes of its nested components
@RoutePrefix("some")
public class SomeParent extends Div implements RouterLayout {
    // Implementation omitted
}

3.3 Absolute routes

Setting the property absolute = true to

  1. @Route annotation or
  2. @RoutePrefix

can obviate parents' route. Here are the examples from vaadin tutorial


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
//Main layout access by route "some"
@RoutePrefix("some")
public class SomeParent extends Div implements RouterLayout {
    // Implementation omitted
}
//Access this component with route to "content" and NOT to "some/content"
@Route(value = "content", layout = SomeParent.class, absolute = true)
public class MyContent extends Div {
    // Implementation omitted
}
//Access by route "framework" and NOT to "some/framework" 
@RoutePrefix(value = "framework", absolute = true)
@ParentLayout(SomeParent.class)
public class FrameworkSite extends Div implements RouterLayout {
    // Implementation omitted
}
// No absolute route so it is "framework/tutorial" and NOT "some/framework/tutorial"
@Route(value = "tutorial", layout = FrameworkSite.class)
public class Tutorials extends Div {
    // Implementation omitted
}

4. URL Parameters

4.1 Parameters

A class can accept parameters in the URL if extends the HasUrlParameter interface

In this example from vaadin tutorial, executing in the browser "http:8080/MyProject/greet/Emily" it displays "Hello, Emily" where Emily is the parameter


1
2
3
4
5
6
7
8
9
@Route(value = "greet")
public class GreetingComponent extends Div
        implements HasUrlParameter<String> {

    @Override
    public void setParameter(BeforeEvent event, String parameter) {
        setText(String.format("Hello, %s!", parameter));
    }
}

However, this URL will fail if the last parameter (Emily) is not included. It is solved using the @OptionalParameter annotation. The example code is


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Route("greet")
public class OptionalGreeting extends Div
        implements HasUrlParameter<String> {

    @Override
    public void setParameter(BeforeEvent event,
            @OptionalParameter String parameter) {
        //Will not fail with no parameters
        if (parameter == null) {
            setText("Welcome anonymous.");
        // Uses the parameter if included
        } else {
            setText(String.format("Welcome %s.", parameter));
        }
    }
}

If more than one parameter is sent, replace the @OptionalParameter annotation with @WildcardParameter 

4.2 Query parameters

We can use query parameters:
?name1=value1&name2=value2

To use it:
  1. Let's locate the method setParameter that is overridden from HasUrlParameter interface
  2. Get a Location instance from the BeforeEvent
  3. Get a QueryParameters instance from the Location instance
Here is the example


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Route("greet")
public class OptionalGreeting extends Div
        implements HasUrlParameter<String> {

    @Override
    public void setParameter(BeforeEvent event,
        @OptionalParameter String parameter) {

    Location location = event.getLocation();
    QueryParameters queryParameters = location.getQueryParameters();

    Map<String, List<String>> parametersMap = queryParameters.getParameters();
}

5. URL Generation

To get or produce the URL of any component (that implements the HasUrlParameter) you only have to use this statement for the case it has no parameters:

String myRoute=UI.getCurrent().getRouter.getUrl(MyComponent.class)

Note that the getUrl method applies to a Router instance

In case that there are some parameters, the statement is similar (here it is one parameter):

String myRoute=UI.getCurrent().getRouter.getUrl(MyComponent.class,"parameter_to_pass")

Now we can make a link to that URL  by means of an Anchor instance

Anchor link= new Anchor (myRoute,"Text to display in the link")

6. Navigation between routes

6.1 routerLink

With the RouterLink we can navigate to another component without knowing its URL route, only knowing its class.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Route(value = "rlexample")
//No need to use the route to the link, only the class name!!
public class RouterLinkExample extends VerticalLayout{
 public RouterLinkExample() {
     Div menu = new Div();
            // link to a component (MainView2) without parameters
     menu.add(new RouterLink("Option-1" , MainView2.class));
            // link to the component GreetingComponent with a parameter
     menu.add(new RouterLink("Option-2 ", GreetingComponent.class, "Ximo"));
            // link to the component GreetingComponent with a parameter
     menu.add(new RouterLink("Option-3 ", GreetingComponent.class, "Paco"));
     add(menu);
 }
}

//A component class to be routed.
@Route(value = "greet")
public class GreetingComponent extends Div
        implements HasUrlParameter<String> {
    @Override
    public void setParameter(BeforeEvent event, String parameter) {
        setText(String.format("Hello, %s!", parameter));
    }
}

When clicking "Option-2" we get "Hello, Ximo" and when clicking  "Option-3" we get "Hello, Paco"

6.2 Using html "<a href="company">"

RouterLink does not reload the page, but <a href ..> does!
<a router-link href="company"> also reloads the page

6.3 Navigation from the server side

There are several options:

  1. UI.navigate(String) that navegates to the route inidicated in the string 
  2. UI.navigate(MyComponent.class) if no parameters
  3. UI.navigate(MyComponent.class, "myParameter") if there is aparameter
Here is an example, the Greetingcomponent class is the same as the one listed peviously

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Route(value = "rlexample")
public class RouterLinkExample extends VerticalLayout{
 public RouterLinkExample() {
     Div menu = new Div();
     menu.add(new RouterLink("Option-1" , MainView2.class));
     menu.add(new RouterLink("Option-2 ", GreetingComponent.class, "Ximo"));
     menu.add(new RouterLink("Option-3 ", GreetingComponent.class, "Paco"));
     add(menu);
     
     //Server routing
     NativeButton button = new NativeButton("Navigate to Greeting component with no params version 1");
     button.addClickListener( e-> {
          button.getUI().ifPresent(ui -> ui.navigate("greet"));
     });
     add(button);
     
     NativeButton button1 = new NativeButton("Navigate to Greeting component with no params version 2");
     button1.addClickListener( e-> {
          button1.getUI().ifPresent(ui -> ui.navigate(GreetingComponent.class));
     });
     add(button1);
     
     NativeButton button2 = new NativeButton("Navigate to Greeting component with params");
     button2.addClickListener( e-> {
          button2.getUI().ifPresent(ui -> ui.navigate(GreetingComponent.class, "Paco"));
     });
     add(button2);
 }
}

6.4 Getting the routes registered in the application

The Router has the method getRoutes() that retrieves all the registered routes.
The RouteData object contains the information form a route.
To obtain the routes by its parent, use getRoutesByParent method


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 01. Get the Router instance
Router router = UI.getCurrent().getRouter();

// 02. Get the list of routes
List<RouteData> routes = router.getRoutes();

// 03. Get all routes by parent
Map<Class<? extends RouterLayout>, List<RouteData>> routesByParent = router.getRoutesByParent();

// 04. Filter the routes of one parent 
List<RouteData> myRoutes = routesByParent.get(MyParentLayout.class);

7. Page Title on Navigation

To update the page title during navigation can be done with 2 ways:

  1. Using @PageTitle("Title name") annotation at the top of the class or
  2. Implementing HasDynamicTitle interface and using the overidden method getPageTitle()

8. Router Exception Handling

There are implemented vaadin by default classes to whom are redirected when this exception occurs:

  1. Class: RouteNotFoundError for NotFoundException with a 404 code
  2. Class: InternalServerError for java.lang.Exception with 500 code

By extending these classes one can customize the results here is a simple example for the first class:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@ParentLayout(MainLayout.class)
public class CustomNotFoundTarget extends RouteNotFoundError {

    @Override
    public int setErrorParameter(BeforeEnterEvent event,
            ErrorParameter<NotFoundException> parameter) {
        getElement().setText("My custom not found class!");
        return HttpServletResponse.SC_NOT_FOUND;
    }
}

In a dashboard with multiple elements, we can protect an element from unauthorised access raising an AccessDeniedException, but for the moment it is a bit ou of my pretensions. See the tutorial



martes, 1 de enero de 2019

02. Simple form, grid, data providers and bindings

1. Introduction

Vaadin is rather simple to use if you want to create a simple application, but if you want to use it with a high level of abstraction, you have to deal with high knowledge of Java programming that can tempt you to decline. To overcome this task. the code is stuffed with the @SupressWarnings annotations. If anyone can help me to avoid this annotation, will be welcome! But I think it is a challenge. Let's go

2. Using a Grid component "at design time".

if you want to bind the grid structure to the fields of a known class at design time the code may be simple. Here is the main view class using a grid and a data provider service. Notice how the columns are created.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package org.ximodante.vaadin.blog;

@Route("")
public class MainView extends VerticalLayout {//DON'T USE IT!!!!!!!

    private CustomerService service = CustomerService.getInstance();
    private Grid<Customer> grid = new Grid<>();

    public MainView() {
     grid.setSizeFull();

     // Create the columns from the reference of the class methods
     grid.addColumn(Customer::getFirstName).setHeader("First name");
     grid.addColumn(Customer::getLastName).setHeader("Last name");
     grid.addColumn(Customer::getStatus).setHeader("Status");

     add(grid);
     setHeight("100vh");
     grid.setItems(service.findAll());
    }
}

In line 7 we should create the grid that wraps a Customer class (by means of generics)
In lines 13 to 15, the columns are bound to the class Customer using references to the class methods ( see the "::" operator)

Nice... but it cannot be used if we don't know the class to show at the design time!

3. Using a Grid component "dynamically at runtime".

Now the thing requires a higher level of abstraction and some lambda knowledge.

How we replace references to class methods?

For that purpose we need to fulfil these steps:

  1. Add additional dependencies to pom.xml
  2. Create an entity base class (Base.class) that every entity should extend
  3. Create a utility class for managing "lambdas".
  4. Replace references to class methods that are using the "::" operator

3.1 Additional dependencies to pom.xml

I am a fan of Lombok as it avoids a lot of boilerplate code by means of annotations.  Also apache commons. Add these dependencies:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.4</version>
      <scope>provided</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.8.1</version>
    </dependency>

3.2 Entity class Base.class:

Here is the code. Note the "id" field and some other methods like "clone", "equals" etc.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package org.ximodante.vaadin.entities;

import java.io.Serializable;

import lombok.Getter;
import lombok.Setter;

@SuppressWarnings("serial")
public class Base implements Serializable, Cloneable{
 @Getter @Setter
 private Long id=(long) 0;
 
 public Base (Long id) {
  super();
  this.id=id;
 }
 
 public Base () {
  super();
 }
 
 public boolean isPersisted() {
  return id != null;
 }
 
 @Override
 public boolean equals(Object obj) {
  if (this == obj) return true;
  if (this.id == null) return false;
  
  if (obj instanceof Base && obj.getClass().equals(getClass())) 
   return this.id.equals(((Base) obj).id);
  
  return false;
 }

 @Override
 public int hashCode() {
  int hash = 5;
  hash = 43 * hash + (id == null ? 0 : id.hashCode());
  return hash;
 }

 @Override
 public Base clone() throws CloneNotSupportedException {
  return (Base) super.clone();
 }

 @Override
 public String toString() {
  return "id = " + id;
 }
}

3.3 Utility class for managing lambdas LambdaUtils.class


The Apache commons library is very helpful in this matter.

The first method is the replacement of the getters methods of a class. To use it we need to have the  Field object that represents the attribute of the class. The getter is considered by Vaadin a lambda that extends ValueProvider<?,?> interface (that is a functional interface), but as we are using entities that extend Base.class the generic <T extends Base> is used

The second method is the replacement of the setter methods of a class, in this case, the lambda supplied must implement the Setter<?,?> interface (that is another functional interface)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package org.ximodante.vaadin.utils;

import java.lang.reflect.Field;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.ximodante.vaadin.entities.Base;

import com.vaadin.flow.data.binder.Setter;
import com.vaadin.flow.function.ValueProvider;

public class LambdaUtils {
 
 /**
  * Implementation of a Lambda of type ValueProvider that is similat to a bean getter
  * Replacement of ClassName::MethodName required by Vaadin for:
  *     1. grid.addColumn()
  *     2. Binder.bind()
  *     3. Etc
  * @param fld
  * @return
  */
 public static  <T extends Base> ValueProvider<T,?> getter(Field fld) {
  return bean->{
   try {
    return FieldUtils.readField(fld, bean, true);
   } catch (IllegalAccessException e) {
    e.printStackTrace();
    return null;
   }
  }; 
 }
 
 /**
  * Implementation of a Lambda Setter that is similar to a bean setter
  * @param fld
  * @return
  */
 public static <T extends Base> Setter<T,?> setter(Field fld) {
  return (bean, value ) -> {
   try {
    FieldUtils.writeField(fld, bean, value, true);
   } catch (IllegalAccessException e) {
    e.printStackTrace();
   }
  }; 
 }

}

3.4 Replacing the getter reference method

Now, this code:

grid.addColumn(Customer::getFirstName)

may be replaced by

grid.addColumn(LambdaUtils.getter(fld))

where "fld" is the field (java.lang.reflect.Field) that represent the firstName attribute of the Customer class. There is a utility class that manages this grid definition.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package org.ximodante.vaadin.utils;

import java.lang.reflect.Field;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.ximodante.vaadin.entities.Base;
import com.vaadin.flow.component.grid.Grid;

public class GridUtils {
 /**
  * Creates a Vaadin Grid prepared to receive data of class clazz type 
  *    and only the fields indicated in the fldNames[] parameter
  *    captions or headers or the grid can be provided or not   
  * @param clazz (class to fill)
  * @param fldNames (name of the fields to populate)
  * @param captions (If an optional caption is desired)
  * @return a vaadin grid 
  */
 public static <T extends Base> Grid<T> newGrid(Class<T> clazz, String[] fldNames, String[] captions) {
  Grid<T> grd=new Grid<>();
  if (captions==null) captions=fldNames;
  if (captions.length!=fldNames.length) 
   throw new java.lang.RuntimeException("Error in creating a vaadin Grid:There are different number of Fields and Captions ");
  int i=0;
  for (String fldName: fldNames) {
   Field fld=FieldUtils.getField(clazz, fldName, true);
   grd.addColumn(LambdaUtils.getter(fld))
    .setHeader(captions[i++]);
  }
  return grd;
 }
 
 
 /**
  * Delegate to LambdaUtils
  * Creates a Vaadin Grid prepared to receive data of class clazz type 
  *   displaying all the fields of the class
  * Delegate the Lambda creation to LambdaUtils  
  * @param clazz (class to fill)
  * @return a vaadin grid 
  */
 public static Grid<Base> newGrid(Class<? extends Base> clazz) {
  var grd=new Grid<Base>();
    
     for (Field fld:FieldUtils.getAllFields(clazz)) 
      grd.addColumn(LambdaUtils.getter(fld))
       .setHeader(fld.getName());

     return grd;
 }
}


4. Binding

By means of the binding process, someone can assign a component to an attribute from a bean, thus any input produced in a control automatically affects the bean that has been bound.

In a form component, the steps for binding are:
  1. Create a Binder instance to the bean class to bind
  2. Bind every control to the bean property by means of the getters and setters (And we will make use of the LambdaUtils class seen previously). It is important to distinguish if the attribute needs conversion to String, as the binding may use another method. (                binder.bind(component, "fldName") or binder.forField(component).withConverter(converter).bind(LambdaUtils.getter(field), LambdaUtils.setter(field))
  3.  Use the method "bindInstanceFields(this)" from the Binder instance to bind the components to the bean instance. (NOTE that in the first step the binder was associated with the class and this step associates the binder to the class instance!)

5. A basic form for updating a bean. Controls and binding

In this form, we will search for all the attributes of a bean and create dynamically a component for each one. The control will vary depending on the class of the attribute. For dates, we will use a "DatePicker", for numbers and strings a "TextBox". Conversion is needed when dealing with numbers so the process of binding control may vary. Here is the class:


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package org.ximodante.vaadin;

import java.lang.reflect.Field;

import org.apache.commons.lang3.reflect.FieldUtils;
import org.ximodante.vaadin.dataproviders.BaseProvider;
import org.ximodante.vaadin.entities.Base;
import org.ximodante.vaadin.utils.ComponentSelector;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;

/**
 * 
 * @author ximo dante
 * 
 * This is a scketch of the UI 
 * 
 * +----------------------------------------------+
 * |        Form for updating customers           |                       
 * |                                              |
 * | label1                                       |
 * | +-----------------------------------------+  |
 * | | Field 1                                 |  |
 * | +-----------------------------------------+  |
 * | label2                                       |
 * | +-----------------------------------------+  |
 * | | Field 2                                 |  |
 * | +-----------------------------------------+  |
 * |   ...........                                |
 * |                                              |
 * | +-------------+   +---------------+          |
 * | | Save button |   | Delete button |          |
 * | +-------------+   +---------------+          |
 * +----------------------------------------------+
 */

@SuppressWarnings("serial")
public class BaseForm<T extends Base> extends FormLayout {
 
 //private HasValue<?,?>[] comps=null;
 private Object[] comps=null;
 private Button save = null;
 private Button delete = null;
 private HorizontalLayout buttons=null;
 private BaseProvider<T> service; 
 private T bean;
 private MainView mainView;
 private Binder<T> binder = null;
 
 @SuppressWarnings({ "unchecked", "rawtypes" })
 public BaseForm(MainView mainView, T bean) {
     // Init general params
     this.mainView = mainView;
     this.bean=bean;
     service= BaseProvider.getInstance(bean.getClass());
     binder=new Binder(bean.getClass()); //Bind a "bean"
     
     // Create Components
     createFields();
     createButtons();
     add(buttons);
     
     // Binds the bean with the the form's controls.
     binder.bindInstanceFields(this);
     setBean(null); // Initialize the bean 
 }
 
 /**
  * Create field components dynamically and binds to the bean
  */
 private void createFields() {
  Field[] flds=FieldUtils.getAllFields(bean.getClass());
  comps=new Object[flds.length];
  int i=0;
  for (Field fld:flds) {
   // Create component and binds to the property dynamically
   comps[i]=ComponentCreator.newComponent(fld, binder);
   add((Component)comps[i]); //Need casting !!!
   i++;
  }
 }
 
 private void createButtons() {
  save = new Button("Save");
  save.getElement().setAttribute("theme", "primary");
  save.addClickListener(e -> this.save());
  
  delete = new Button("Delete");
  delete.addClickListener(e -> this.delete());
  
  buttons = new HorizontalLayout(save, delete);
 }
 
 /**
  * By means of this method, the bean is assigned to the form
  * @param bean
  */
 public void setBean(T bean) {
     this.bean = bean;
     binder.setBean(bean);
     boolean enabled = bean != null;
     save.setEnabled(enabled);
     delete.setEnabled(enabled);
     if (enabled) ((TextField)comps[0]).focus(); // Set focus to first component 
 }

 /**
  * Delete action of the button
  */
 private void delete() {
     service.delete(bean);
     mainView.updateList();
     setBean(null);
 }


 /**
  * Save action of the button
  */
 private void save() {
     service.save(bean);
     mainView.updateList();
     setBean(null);
 }
}

We should notice:

  1. The class is wrapping <T extends Base> which means that all the entities that will be displayed in this form should extend Base.class
  2. There are 2 buttons and an array of objects that represent the components to create dynamically
  3. A bean that will be managed in this form
  4. A service that provides de bean
  5. A binder that wraps a class of the generic type T
  6. Creation of components (by means of a helper class ComponentCreator that creates the components by reflection of the fields of the class and binds each component to the attributes of the class)
  7. Creation of buttons and assigning an action to each one
  8. The binding of the elements of the form to the instance of the T class.
  9. The implementation of the actions of the buttons

5.1 Helper class for creating controls and binding to class attributes


Here is the code notice:

  1. Component creation (lines 59, 62, 72, ..)
  2. Converter creators (lines 80, 84, 88, ...)
  3. The binding without conversion (line 116)
  4. The binding with conversion of types (lines 117-120)


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package org.ximodante.vaadin.utils;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

import com.vaadin.flow.component.HasValue;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.datepicker.DatePicker;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.converter.Converter;
import com.vaadin.flow.data.converter.StringToBigDecimalConverter;
import com.vaadin.flow.data.converter.StringToBigIntegerConverter;
import com.vaadin.flow.data.converter.StringToBooleanConverter;
import com.vaadin.flow.data.converter.StringToDoubleConverter;
import com.vaadin.flow.data.converter.StringToFloatConverter;
import com.vaadin.flow.data.converter.StringToIntegerConverter;
import com.vaadin.flow.data.converter.StringToLongConverter;

public class ComponentCreator {
 
 /**
  * Gets the enum values for combos
  * @param cls
  * @return
  * @throws NoSuchFieldException
  * @throws IllegalAccessException
  */
 private static List<?> getEnumValues (Class<?> cls) 
  throws NoSuchFieldException, IllegalAccessException {
  Object[] possibleValues = cls.getEnumConstants();
  return Arrays.asList(possibleValues);
    }
 
 /**
  * Create components based on the class of the attribute and binds the componets to the bean 
  * @param fld
  * @param binder
  * @return
  */
 @SuppressWarnings({ "unchecked", "rawtypes" })
 public static HasValue<?, ?> newComponent(Field fld, Binder binder) {
  HasValue<?, ?> cmp = null;
  Class<?>cls=fld.getType();
  var fldName=fld.getName();
  Converter converter=null;
  System.out.println(fldName);
  System.out.println(cls.getName());
  
  //@see Olli Tietäväinen in: 
     //  https://vaadin.com/forum/thread/15385912/binding-from-integer-not-working
     
  if (cls==String.class)  cmp=new TextField(fldName);
  
  else if (cls.isEnum()) {
   cmp=new ComboBox(fldName);
   List<?> lst=null;
   try {
    lst = getEnumValues(cls);
   } catch (NoSuchFieldException | IllegalAccessException e) {
    e.printStackTrace();
   }
   ((ComboBox) cmp).setItems(lst);
     
  }else if (cls==LocalDate.class ) {
   cmp=new DatePicker();
   ((DatePicker)cmp).setLabel(fldName);
   ((DatePicker)cmp).setLocale(new Locale("es"));
   ((DatePicker)cmp).addValueChangeListener(
           event -> Notification.show("EA->" + event.getValue()));
     
  }else if (cls==Long.class) {
      cmp=new TextField(fldName);
      converter=new StringToLongConverter("must be long");
  
  }else if (cls==Integer.class) {
       cmp=new TextField(fldName);
       converter=new StringToIntegerConverter("must be integer");
     
  }else if (cls==Byte.class) {
       cmp=new TextField(fldName);
       converter=new StringToIntegerConverter("must be byte");
  
  }else if (cls==BigInteger.class) {
       cmp=new TextField(fldName);
       converter=new StringToBigIntegerConverter("must be biginteger");
  
  }else if (cls==Float.class) {
       cmp=new TextField(fldName);
       converter=new StringToFloatConverter("must be float");
  
  }else if (cls==Double.class) {
       cmp=new TextField(fldName);
       converter=new StringToDoubleConverter("must be double");
  
  }else if (cls==BigDecimal.class) {
       cmp=new TextField(fldName);
       converter=new StringToBigDecimalConverter("must be bigdecimal");
  
  }else if (cls==Boolean.class) {
       cmp=new TextField(fldName);
       converter=new StringToBooleanConverter("must be bigdecimal");
  
  }else {
       System.out.println("Field Type not found.");
       cmp=new TextField(fldName);
     }
     
  // BINDING CONTROLS TO BEAN
  if (converter==null) binder.bind(cmp,fldName);
  else binder.forField(cmp)
         .withConverter(converter)
         //.bind(Item::getItemCount, Item::setItemCount);
         .bind(LambdaUtils.getter(fld), LambdaUtils.setter(fld));
       
  return cmp;
 }

}


6. A mock service for providing data.

We should use a database for managing information, but this mock service may do the trick. Notice the use of synchronized keyword.
Some other entities that extend Base.class can be added to the "ensureTestData" method.
Notice also that this service is created as a singleton in the old way. I think that it is better creating this singleton using CDI annotations ... but that's for a future post.


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
package org.ximodante.vaadin.dataproviders;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.ximodante.vaadin.entities.Base;
import org.ximodante.vaadin.entities.Customer;
import org.ximodante.vaadin.enums.CustomerStatus;

public class BaseProvider <T extends Base>{
 
 @SuppressWarnings("rawtypes")
 private static BaseProvider instance;
 private static final Logger LOGGER = Logger.getLogger(BaseProvider.class.getName());
 private final HashMap<Long, T> contacts = new HashMap<>();
 private long nextId = 0;
 private Class<T> clazz;

 private BaseProvider(Class<T> clazz) {
  this.clazz=clazz;
 }

 /**
  * @return a reference to an example facade for Customer objects.
  */
 @SuppressWarnings({ "rawtypes", "unchecked" })
 public static BaseProvider getInstance(Class<?> clazz) {
  if (instance == null) {
   instance = new BaseProvider(clazz);
   instance.ensureTestData();
  }
  return instance;
 }

 /**
  * @return all available Customer objects.
  */
 public synchronized List<T> findAll() {
  return findAll(null);
 }

 /**
  * Finds all Customer's that match given filter and limits the resultset.
  *
  * @param stringFilter:
  *            filter that returned objects should match or null/empty string
  *            if all objects should be returned.
  * @param start :     the index of first result
  * @param maxresults: maximum result count
  * @return list a Customer objects
  */
 //public synchronized List<Customer> findAll(String stringFilter, int start, int maxresults) {
 @SuppressWarnings("unchecked")
 public synchronized List<T> findAll(String stringFilter, int start, int maxresults) {
  ArrayList<T> arrayList = new ArrayList<>();
  for (T contact : contacts.values()) {
   try {
    boolean passesFilter = (stringFilter == null || stringFilter.isEmpty())
      || contact.toString().toLowerCase().contains(stringFilter.toLowerCase());
    if (passesFilter) {
     arrayList.add((T) contact.clone());
    }
   } catch (CloneNotSupportedException ex) {
    Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, null, ex);
   }
  }
  Collections.sort(arrayList, new BaseComparator<T>());
  if (start<0 || maxresults<0) {
   start=0;
   maxresults=arrayList.size();
  }
  int end = start + maxresults;
  if (end > arrayList.size()) {
   end = arrayList.size();
  }
  return arrayList.subList(start, end);
 }

 /**
  * Finds all Customer's that match given filter.
  *
  * @param stringFilter
  *            filter that returned objects should match or null/empty string
  *            if all objects should be returned.
  * @return list a Customer objects
  */
 public synchronized List<T> findAll(String stringFilter) {
  return findAll(stringFilter, -1, -1); //Return all elements
 }
 /**
  * @return the amount of all customers in the system
  */
 public synchronized long count() {
  return contacts.size();
 }

 /**
  * Deletes a customer from a system
  * @param value
  *            the Customer to be deleted
  */
 public synchronized void delete(T value) {
  contacts.remove(value.getId());
 }

 /**
  * Persists or updates customer in the system. Also assigns an identifier
  * for new Customer instances.
  * @param entry
  */
 @SuppressWarnings("unchecked")
 public synchronized void save(T entry) {
  if (entry == null) {
   LOGGER.log(Level.SEVERE,
     "Customer is null. Are you sure you have connected your form to the application as described in tutorial chapter 7?");
   return;
  }
  if (entry.getId() == null || entry.getId() == 0) {
   entry.setId(nextId++);
  }
  try {
   entry = (T) entry.clone();
  } catch (Exception ex) {
   throw new RuntimeException(ex);
  }
  contacts.put(entry.getId(), entry);
 }

 /**
  * Sample data generation
  */
 public void ensureTestData() {
  if (findAll().isEmpty()) {
   
   // Data supplier for Customer class
   if (this.clazz.isAssignableFrom(Customer.class)) {
    final String[] names = new String[] { "Gabrielle Patel", "Brian Robinson", "Eduardo Haugen",
     "Koen Johansen", "Alejandro Macdonald", "Angel Karlsson", "Yahir Gustavsson", "Haiden Svensson",
     "Emily Stewart", "Corinne Davis", "Ryann Davis", "Yurem Jackson", "Kelly Gustavsson",
     "Eileen Walker", "Katelyn Martin", "Israel Carlsson", "Quinn Hansson", "Makena Smith",
     "Danielle Watson", "Leland Harris", "Gunner Karlsen", "Jamar Olsson", "Lara Martin",
     "Ann Andersson", "Remington Andersson", "Rene Carlsson", "Elvis Olsen", "Solomon Olsen",
     "Jaydan Jackson", "Bernard Nilsen" };
    Random r = new Random(0);
    //Long i=0L;
    for (String name : names) {
     String[] split = name.split(" ");
     @SuppressWarnings("unchecked")
     T c = (T) new Customer();
     ((Customer)c).setFirstName(split[0]);
     ((Customer)c).setLastName(split[1]);
     ((Customer)c).setStatus(CustomerStatus.values()[r.nextInt(CustomerStatus.values().length)]);
     save(c);
     //System.out.println(i);
    }
   } // else a supplier for other classes   
  }
 }
}


7. The bean class: Customer.class

we could also create other entities


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package org.ximodante.vaadin.entities;

import java.time.LocalDate;
import org.ximodante.vaadin.enums.CustomerStatus;

import lombok.Getter;
import lombok.Setter;

/**
 * A entity object, like in any other Java application. In a typical real world
 * application this could for example be a JPA entity.
 */
@SuppressWarnings("serial")
public class Customer extends Base {

 @Getter @Setter
 private String firstName = "";

 @Getter @Setter
 private String lastName = "";

 @Getter @Setter
 private LocalDate birthDate;
 //private Date birthDate;

 @Getter @Setter
 private CustomerStatus status;

 @Getter @Setter
 private String email = "";

 @Override
 public Customer clone() throws CloneNotSupportedException {
  return (Customer) super.clone();
 }
 @Override
 public String toString() {
  return firstName + " " + lastName;
 }
 public Customer() {
  super();
 }
 public Customer(Long id) {
  super(id);
 }
}

8. The MainView class

Don't use the first MainView displayed in this post, as it is not valid for dynamic creation of components. Use this one:


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package org.ximodante.vaadin;

import org.ximodante.vaadin.dataproviders.BaseProvider;
import org.ximodante.vaadin.entities.Base;
import org.ximodante.vaadin.entities.Customer;
import org.ximodante.vaadin.utils.GridUtils;

import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.server.PWA;

/**
 * 
 * @author ximo dante
 * 
 * This is a scketch of the UI 
 * 
 * +---------------------------------------------------------------------------------------+
 * |  TOOLBAR
 * |  +-------------------------+   +---------------------+  +-------------------------+   |
 * |  | Filter customer textbox |   | Clear Filter Button |  | Add new customer button |   |
 * |  +-------------------------+   +---------------------+  +-------------------------+   |
 * +----------------------------------------+----------------------------------------------+
 * |    Grid with customers values          |        Form for updating customers           |                       
 * |                                        |                                              |
 * | +-----------------------------------+  | label1                                       |
 * | | customer 1                        |  | +-----------------------------------------+  |
 * | +-----------------------------------+  | | Field 1                                 |  |
 * | | customer 2                        |  | +-----------------------------------------+  |
 * | +-----------------------------------+  | label2                                       |
 * | | customer 3                        |  | +-----------------------------------------+  |
 * | +-----------------------------------+  | | Field 2                                 |  |
 * | |                                   |  | +-----------------------------------------+  |
 * | |   .....                           |  |   ...........                                |
 * | +-----------------------------------+  |                                              |
 * |                                        | +-------------+   +---------------+          |
 * |                                        | | Save button |   | Delete button |          |
 * |                                        | +-------------+   +---------------+          |
 * +----------------------------------------+----------------------------------------------+
 */
@SuppressWarnings("serial")
@Route("")
@PWA(name = "Project Base for Vaadin Flow", shortName = "Project Base")
public class MainView extends VerticalLayout {
 
 @SuppressWarnings("rawtypes")
 private BaseProvider service = BaseProvider.getInstance(Customer.class);
 
 @SuppressWarnings("rawtypes")
 private Grid grid =null;
 
 private TextField filterText = null;
 private Button clearFilterTextBtn = null;
 private HorizontalLayout filtering = null;
 private BaseForm<Base> form= null;
 private HorizontalLayout main=null;
 private Button addCustomerBtn = null;
 private HorizontalLayout toolbar = null;
    
 public MainView() {
 
  createGrid();       // Add columns and headers to the grid assigning class attributes
  createBaseForm(); // Create the editing form
  createToolbar();    // Create the tool-bar with the filter, delete filter button and new customer buton
  createMainLayout(); // Display the grid and the form
  
  
  add(toolbar, main);  // Place the tool-bar over the main panel
  setHeight("100vh");
     
     updateList();        // Fill the grid with data
    }
 
 @SuppressWarnings("unchecked")
 private void createGrid() {
  grid=GridUtils.newGrid(Customer.class);
     grid.setSizeFull();
     
     // When a record is selected, it is transmitted to the form
     grid.asSingleSelect().addValueChangeListener(event -> {
      form.setBean((Base) event.getValue());
  });
 }
 
 /**
  * Creates the edit form
  */
 @SuppressWarnings({ "unchecked", "rawtypes" })
 private void createBaseForm() {
  form = new BaseForm(this, new Customer());
 }
 
 /**
  * Display filter textbox, clear filter button and add customer button
  */
 private void createToolbar() {
  // Filter text
  filterText = new TextField();
  filterText.setPlaceholder("Filter by name...");
        filterText.setValueChangeMode(ValueChangeMode.EAGER);
        filterText.addValueChangeListener(e -> updateList());
        
        // Clear filter text
        clearFilterTextBtn = new Button(new Icon(VaadinIcon.CLOSE_CIRCLE));
        clearFilterTextBtn.addClickListener(e -> filterText.clear());
        
        filtering = new HorizontalLayout(filterText, clearFilterTextBtn);
        
        addCustomerBtn = new Button("Add new customer");
        addCustomerBtn.addClickListener(e -> {
                grid.asSingleSelect().clear();
                form.setBean(new Customer());
        });
        toolbar = new HorizontalLayout(filtering, addCustomerBtn);
 }
 
 private void createMainLayout() {
  main = new HorizontalLayout(grid, form);
  main.setAlignItems(Alignment.START);
  main.setSizeFull();
 }
 
 /**
  * Update the grid with the values supplied by the data provider service
  */
 @SuppressWarnings("unchecked")
 public void updateList() {
     grid.setItems(service.findAll(filterText.getValue()));
 }
} 


9. The execution

Here is an image of the project


Right-click on the project and Run as- Run on server,  and here you are...



Happy coding!