├── .gitignore
├── demo-services
├── pom.xml
└── src
│ └── main
│ └── java
│ └── org
│ └── vaadin
│ └── marcus
│ ├── entity
│ ├── LineItem.java
│ ├── Order.java
│ ├── Product.java
│ └── User.java
│ └── service
│ ├── LoginService.java
│ └── OrderService.java
├── demo-ui
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ └── org
│ │ │ └── vaadin
│ │ │ └── marcus
│ │ │ ├── DemoUI.java
│ │ │ ├── ui
│ │ │ ├── AwesomeApp.java
│ │ │ ├── NavBar.java
│ │ │ ├── components
│ │ │ │ ├── OrdersGrid.java
│ │ │ │ └── VerticalSpacedLayout.java
│ │ │ └── views
│ │ │ │ ├── AsyncOrdersView.java
│ │ │ │ ├── ErrorView.java
│ │ │ │ ├── HeapDestroyerView.java
│ │ │ │ ├── HomeView.java
│ │ │ │ ├── LayoutView.java
│ │ │ │ ├── LazyOrdersView.java
│ │ │ │ ├── OrdersView.java
│ │ │ │ ├── form
│ │ │ │ ├── FormLayout.java
│ │ │ │ ├── FormPresenter.java
│ │ │ │ ├── FormView.java
│ │ │ │ └── LineItemField.java
│ │ │ │ ├── login
│ │ │ │ ├── LoginBox.java
│ │ │ │ ├── LoginEvent.java
│ │ │ │ └── LoginView.java
│ │ │ │ └── render
│ │ │ │ ├── OrderLayout.java
│ │ │ │ ├── QuickOrderLayout.java
│ │ │ │ └── SlowRenderingView.java
│ │ │ └── util
│ │ │ ├── CurrentUser.java
│ │ │ ├── FieldGroupUtil.java
│ │ │ ├── LazyProvider.java
│ │ │ ├── MyTheme.java
│ │ │ ├── NonEmptyCollectionValidator.java
│ │ │ ├── PageTitleUpdater.java
│ │ │ ├── ViewConfig.java
│ │ │ ├── converter
│ │ │ ├── CurrencyConverter.java
│ │ │ ├── DateTimeConverter.java
│ │ │ ├── LineItemConverter.java
│ │ │ └── PercentageConverter.java
│ │ │ └── event
│ │ │ ├── EventBus.java
│ │ │ ├── LogoutEvent.java
│ │ │ └── NavigationEvent.java
│ ├── resources
│ │ └── org
│ │ │ └── vaadin
│ │ │ └── marcus
│ │ │ └── MyAppWidgetset.gwt.xml
│ └── webapp
│ │ └── VAADIN
│ │ └── themes
│ │ └── mytheme
│ │ ├── addons.scss
│ │ ├── favicon.ico
│ │ ├── mytheme.scss
│ │ ├── styles.scss
│ │ └── views
│ │ └── render.scss
│ └── test
│ └── java
│ └── org
│ └── vaadin
│ └── marcus
│ └── ui
│ ├── pageobjects
│ ├── LoginViewPO.java
│ ├── MainViewPO.java
│ └── TBUtils.java
│ ├── regression
│ ├── FormRegressionIT.java
│ └── RenderingSpeedIT.java
│ └── views
│ └── form
│ └── FormPresenterTest.java
├── pom.xml
└── readme.md
/.gitignore:
--------------------------------------------------------------------------------
1 | rebel.xml
2 | *.iml
3 | .idea
4 | target
--------------------------------------------------------------------------------
/demo-services/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | vaadin-tips
7 | org.vaadin.marcus
8 | 1.0-SNAPSHOT
9 |
10 | 4.0.0
11 |
12 | demo-services
13 |
14 |
15 |
16 | com.google.guava
17 | guava
18 |
19 |
20 |
21 | joda-time
22 | joda-time
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/demo-services/src/main/java/org/vaadin/marcus/entity/LineItem.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.entity;
2 |
3 | public class LineItem {
4 | private Product product;
5 | private int quantity = 1;
6 | private double discount = 0.0;
7 |
8 | public LineItem(Product product, int quantity, double discount) {
9 | this.product = product;
10 | this.quantity = quantity;
11 | this.discount = discount;
12 | }
13 |
14 | public LineItem() {
15 | }
16 |
17 | public Product getProduct() {
18 | return product;
19 | }
20 |
21 | public void setProduct(Product product) {
22 | this.product = product;
23 | }
24 |
25 | public int getQuantity() {
26 | return quantity;
27 | }
28 |
29 | public void setQuantity(int quantity) {
30 | this.quantity = quantity;
31 | }
32 |
33 | public double getDiscount() {
34 | return discount;
35 | }
36 |
37 | public void setDiscount(double discount) {
38 | this.discount = discount;
39 | }
40 |
41 | @Override
42 | public String toString() {
43 | return getQuantity() + " x " + getProduct();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/demo-services/src/main/java/org/vaadin/marcus/entity/Order.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.entity;
2 |
3 | import com.google.common.collect.Lists;
4 | import org.joda.time.DateTime;
5 |
6 | import java.util.Arrays;
7 | import java.util.List;
8 | import java.util.UUID;
9 |
10 | public class Order {
11 | public enum Status {RECEIVED, PROCESSING, CHARGED, PREPARING_FOR_SHIPMENT, SHIPPED}
12 |
13 | private UUID id;
14 | private DateTime orderTime;
15 | private Status status;
16 | private List lineItems = Lists.newLinkedList();
17 |
18 | public Order() {
19 | id = UUID.randomUUID();
20 | }
21 |
22 | public Order(DateTime orderTime, Status status, List lineItems) {
23 | this();
24 | this.orderTime = orderTime;
25 | this.status = status;
26 | this.lineItems = lineItems;
27 | }
28 |
29 | public UUID getId() {
30 | return id;
31 | }
32 |
33 | public DateTime getOrderTime() {
34 | return orderTime;
35 | }
36 |
37 | public Status getStatus() {
38 | return status;
39 | }
40 |
41 | public void setStatus(Status status) {
42 | this.status = status;
43 | }
44 |
45 | public List getLineItems() {
46 | return lineItems;
47 | }
48 |
49 | public void setLineItems(List lineItems) {
50 | this.lineItems = lineItems;
51 | }
52 |
53 | public void addProduct(Product product, int quantity, double discount) {
54 | getLineItems().add(new LineItem(product, quantity, discount));
55 | }
56 |
57 | public double getOrderTotal() {
58 | return getLineItems()
59 | .stream()
60 | .mapToDouble(lineItem ->
61 | lineItem.getQuantity()
62 | * lineItem.getProduct().getPrice()
63 | * (1-lineItem.getDiscount()))
64 | .sum();
65 | }
66 |
67 | public double getProgress(){
68 | List statuses = Arrays.asList(Status.values());
69 | return ((double) statuses.indexOf(getStatus())+1) / statuses.size();
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/demo-services/src/main/java/org/vaadin/marcus/entity/Product.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.entity;
2 |
3 | import java.text.NumberFormat;
4 |
5 | public class Product {
6 | private String name;
7 | private String description;
8 | // Don't ever use double for prices in real life or you'll have a bad time.
9 | // The BigDecimal API is just so horrible to work with for a demo.
10 | private double price;
11 |
12 | public Product(String name, double price) {
13 | this.name = name;
14 | this.price = price;
15 | }
16 |
17 |
18 | public String getName() {
19 | return name;
20 | }
21 |
22 | public void setName(String name) {
23 | this.name = name;
24 | }
25 |
26 | public String getDescription() {
27 | return description;
28 | }
29 |
30 | public void setDescription(String description) {
31 | this.description = description;
32 | }
33 |
34 | public double getPrice() {
35 | return price;
36 | }
37 |
38 | public void setPrice(double price) {
39 | this.price = price;
40 | }
41 |
42 | @Override
43 | public String toString() {
44 | return name + " (" + NumberFormat.getCurrencyInstance().format(price) + ")";
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/demo-services/src/main/java/org/vaadin/marcus/entity/User.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.entity;
2 |
3 | public class User {
4 |
5 | private String username;
6 |
7 | public User(String username) {
8 | this.username = username;
9 | }
10 |
11 | public String getUsername() {
12 | return username;
13 | }
14 |
15 | public void setUsername(String username) {
16 | this.username = username;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/demo-services/src/main/java/org/vaadin/marcus/service/LoginService.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.service;
2 |
3 | import org.vaadin.marcus.entity.User;
4 |
5 | import javax.security.auth.login.LoginException;
6 |
7 | public class LoginService {
8 |
9 | public User login(String username, String password) throws LoginException {
10 | if (!username.isEmpty() && !password.isEmpty()) {
11 | return new User(username);
12 | }
13 | throw new LoginException();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/demo-services/src/main/java/org/vaadin/marcus/service/OrderService.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.service;
2 |
3 | import com.google.common.collect.Lists;
4 | import com.google.common.util.concurrent.ListenableFuture;
5 | import com.google.common.util.concurrent.ListeningExecutorService;
6 | import com.google.common.util.concurrent.MoreExecutors;
7 | import org.joda.time.DateTime;
8 | import org.vaadin.marcus.entity.LineItem;
9 | import org.vaadin.marcus.entity.Order;
10 | import org.vaadin.marcus.entity.Product;
11 |
12 | import java.util.LinkedList;
13 | import java.util.List;
14 | import java.util.Random;
15 | import java.util.UUID;
16 | import java.util.concurrent.Executors;
17 |
18 | public class OrderService {
19 | // To keep the tech stack simple, I opted to avoid using a container like JavaEE or Spring
20 | // In most cases, I would have the Service managed by the container and inject it where needed.
21 | private static final OrderService INSTANCE = new OrderService();
22 | private final List products;
23 |
24 | public static OrderService get() {
25 | return INSTANCE;
26 | }
27 |
28 | private static final int NUM_ORDERS = 10000;
29 | private Random randy = new Random();
30 | private final LinkedList orders;
31 |
32 | private ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(5));
33 |
34 | private OrderService() {
35 | products = createProducts();
36 | orders = initOrders();
37 | }
38 |
39 | private List createProducts() {
40 | return Lists.newArrayList(
41 | new Product("Apple", 1),
42 | new Product("Orange", 2),
43 | new Product("Banana", 3));
44 | }
45 |
46 | /**
47 | * Synchronous method that returns all orders and is slow.
48 | */
49 | public List getOrders() {
50 | try {
51 | Thread.sleep(5000);
52 | } catch (InterruptedException e) {
53 | e.printStackTrace();
54 | }
55 | return orders;
56 | }
57 |
58 | /**
59 | * Async method for getting orders.
60 | */
61 | public ListenableFuture> getOrdersAsync() {
62 | return service.submit(OrderService.this::getOrders);
63 | }
64 |
65 | /**
66 | * Returns a max of 45 (LazyList default page size) orders starting at the given index.
67 | *
68 | * @param startIndex
69 | * @return
70 | */
71 | public List fetchOrders(int startIndex) {
72 | try {
73 | Thread.sleep(50);
74 | } catch (InterruptedException e) {
75 | e.printStackTrace();
76 | }
77 | return fetchOrders(startIndex, 45);
78 | }
79 |
80 | /**
81 | * Returns a slice of orders
82 | *
83 | * @param startIndex
84 | * @param num
85 | * @return
86 | */
87 | public List fetchOrders(int startIndex, int num) {
88 | int endIndex = startIndex + num > orders.size() ? orders.size() : startIndex + num;
89 | return orders.subList(startIndex, endIndex);
90 | }
91 |
92 | /**
93 | * @return the totan number of orders
94 | */
95 | public int getOrderCount() {
96 | return orders.size();
97 | }
98 |
99 | /**
100 | * Save a new order
101 | *
102 | * @param order
103 | */
104 | public void saveOrder(Order order) {
105 | orders.add(0, order);
106 | }
107 |
108 | public List getProducts() {
109 | return products;
110 | }
111 |
112 | /**
113 | * Generates some random orders
114 | *
115 | * @return
116 | */
117 | private LinkedList initOrders() {
118 | LinkedList orders;
119 | orders = new LinkedList<>();
120 | for (int i = 0; i < NUM_ORDERS; i++) {
121 | LinkedList lineItems = new LinkedList<>();
122 | for (int j = 0; j <= (1 + randy.nextInt(2)); j++) {
123 | lineItems.add(generateLineItem());
124 | }
125 | DateTime orderTime = DateTime.now().minusDays(randy.nextInt(30)).minusHours(randy.nextInt(23)).minusMinutes(randy.nextInt(59));
126 | orders.add(new Order(orderTime, getRandomStatus(), lineItems));
127 | }
128 | return orders;
129 | }
130 |
131 | public Order findById(UUID uuid) {
132 | return orders.stream().filter(order -> order.getId().equals(uuid)).findFirst().orElse(null);
133 | }
134 |
135 | private Order.Status getRandomStatus() {
136 | Order.Status[] statuses = Order.Status.values();
137 | return statuses[randy.nextInt(statuses.length)];
138 | }
139 |
140 | private LineItem generateLineItem() {
141 | return new LineItem(getRandomProduct(), (1 + randy.nextInt(2)), randy.nextDouble());
142 | }
143 |
144 | private Product getRandomProduct() {
145 | return products.get(randy.nextInt(2));
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/demo-ui/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | vaadin-tips
6 | org.vaadin.marcus
7 | 1.0-SNAPSHOT
8 |
9 | 4.0.0
10 |
11 | demo-ui
12 | war
13 | demo-ui
14 |
15 |
16 |
17 |
18 | javax.servlet
19 | javax.servlet-api
20 |
21 |
22 | com.vaadin
23 | vaadin-server
24 |
25 |
26 | com.vaadin
27 | vaadin-push
28 |
29 |
30 | com.vaadin
31 | vaadin-client
32 | provided
33 |
34 |
35 | com.vaadin
36 | vaadin-themes
37 |
38 |
39 | org.vaadin.marcus
40 | demo-services
41 |
42 |
43 | joda-time
44 | joda-time
45 |
46 |
47 | org.vaadin
48 | viritin
49 |
50 |
51 | com.google.guava
52 | guava
53 |
54 |
55 | org.mockito
56 | mockito-all
57 |
58 |
59 | junit
60 | junit
61 |
62 |
63 | com.vaadin
64 | vaadin-testbench
65 |
66 |
67 |
68 |
69 |
70 |
71 | org.apache.maven.plugins
72 | maven-resources-plugin
73 | 2.6
74 |
75 | ${project.encoding}
76 |
77 |
78 |
79 | org.apache.maven.plugins
80 | maven-war-plugin
81 | 2.3
82 |
83 | false
84 |
85 | WEB-INF/classes/VAADIN/gwt-unitCache/**,
86 | WEB-INF/classes/VAADIN/widgetsets/WEB-INF/**
87 |
88 |
89 |
90 |
91 | com.vaadin
92 | vaadin-maven-plugin
93 | ${vaadin.plugin.version}
94 |
95 | -Xmx512M -Xss1024k
96 | ${basedir}/target/classes/VAADIN/widgetsets
97 | false
98 | false
99 |
100 | true
101 |
102 |
103 |
104 |
105 | update-widgetset
106 | compile
107 | compile-theme
108 |
109 |
110 |
111 |
112 |
113 | org.apache.maven.plugins
114 | maven-source-plugin
115 | 2.4
116 |
117 |
118 | org.apache.maven.plugins
119 | maven-clean-plugin
120 | 2.6.1
121 |
122 |
123 |
124 |
125 | src/main/webapp/VAADIN/themes
126 |
127 | **/styles.css
128 | **/styles.scss.cache
129 |
130 |
131 |
132 |
133 |
134 |
135 |
137 |
138 | org.eclipse.jetty
139 | jetty-maven-plugin
140 | ${jetty.plugin.version}
141 |
142 | 2
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 | it
151 |
152 |
153 |
154 | org.eclipse.jetty
155 | jetty-maven-plugin
156 | 9.3.8.v20160314
157 |
158 |
159 | start-jetty
160 | pre-integration-test
161 |
162 | start
163 |
164 |
165 | 0
166 | true
167 |
168 |
169 |
170 | stop-jetty
171 | post-integration-test
172 |
173 | stop
174 |
175 |
176 |
177 |
178 |
179 | org.apache.maven.plugins
180 | maven-failsafe-plugin
181 | 2.12
182 |
183 |
184 |
185 | integration-test
186 | verify
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/DemoUI.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus;
2 |
3 | import com.google.common.eventbus.EventBus;
4 | import com.google.common.eventbus.Subscribe;
5 | import com.vaadin.annotations.Theme;
6 | import com.vaadin.annotations.Title;
7 | import com.vaadin.annotations.VaadinServletConfiguration;
8 | import com.vaadin.annotations.Widgetset;
9 | import com.vaadin.server.Page;
10 | import com.vaadin.server.VaadinRequest;
11 | import com.vaadin.server.VaadinServlet;
12 | import com.vaadin.server.VaadinSession;
13 | import com.vaadin.ui.UI;
14 | import org.vaadin.marcus.ui.AwesomeApp;
15 | import org.vaadin.marcus.ui.views.login.LoginEvent;
16 | import org.vaadin.marcus.ui.views.login.LoginView;
17 | import org.vaadin.marcus.util.CurrentUser;
18 | import org.vaadin.marcus.util.event.LogoutEvent;
19 | import org.vaadin.marcus.util.event.NavigationEvent;
20 |
21 | import javax.servlet.annotation.WebServlet;
22 |
23 | @Theme("mytheme")
24 | @Title("AwesomeApp")
25 | @Widgetset("org.vaadin.marcus.MyAppWidgetset")
26 | public class DemoUI extends UI {
27 | private EventBus eventBus;
28 |
29 | @Override
30 | protected void init(VaadinRequest vaadinRequest) {
31 | setupEventBus();
32 |
33 | if (CurrentUser.isLoggedIn()) {
34 | setContent(new AwesomeApp());
35 | } else {
36 | setContent(new LoginView());
37 | }
38 | }
39 |
40 | @Subscribe
41 | public void userLoggedIn(LoginEvent event) {
42 | CurrentUser.set(event.getUser());
43 | setContent(new AwesomeApp());
44 | }
45 |
46 | @Subscribe
47 | public void navigateTo(NavigationEvent view) {
48 | getNavigator().navigateTo(view.getViewName());
49 | }
50 |
51 | @Subscribe
52 | public void logout(LogoutEvent logoutEvent) {
53 | // Don't invalidate the underlying HTTP session if you are using it for something else
54 | VaadinSession.getCurrent().getSession().invalidate();
55 | VaadinSession.getCurrent().close();
56 | Page.getCurrent().reload();
57 |
58 | }
59 |
60 | private void setupEventBus() {
61 | eventBus = new EventBus((throwable, subscriberExceptionContext) -> {
62 | // log error
63 | throwable.printStackTrace();
64 | });
65 | eventBus.register(this);
66 | }
67 |
68 | public static DemoUI getCurrent() {
69 | return (DemoUI) UI.getCurrent();
70 | }
71 |
72 | public static EventBus getEventBus() {
73 | return getCurrent().eventBus;
74 | }
75 |
76 | @WebServlet(urlPatterns = "/*", name = "MyUIServlet", asyncSupported = true)
77 | @VaadinServletConfiguration(ui = DemoUI.class, productionMode = false)
78 | public static class MyUIServlet extends VaadinServlet {
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/AwesomeApp.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui;
2 |
3 | import com.vaadin.navigator.Navigator;
4 | import com.vaadin.navigator.View;
5 | import com.vaadin.ui.HorizontalLayout;
6 | import com.vaadin.ui.Panel;
7 | import org.vaadin.marcus.DemoUI;
8 | import org.vaadin.marcus.ui.views.*;
9 | import org.vaadin.marcus.util.MyTheme;
10 | import org.vaadin.marcus.util.PageTitleUpdater;
11 | import org.vaadin.marcus.util.LazyProvider;
12 | import org.vaadin.marcus.util.ViewConfig;
13 | import org.vaadin.marcus.ui.views.AsyncOrdersView;
14 | import org.vaadin.marcus.ui.views.form.FormView;
15 | import org.vaadin.marcus.ui.views.OrdersView;
16 | import org.vaadin.marcus.ui.views.render.SlowRenderingView;
17 |
18 | public class AwesomeApp extends HorizontalLayout {
19 |
20 | private NavBar navBar;
21 | private Panel content;
22 | private Navigator navigator;
23 |
24 | public AwesomeApp() {
25 | addStyleName(MyTheme.MAIN_LAYOUT);
26 |
27 | setSizeFull();
28 |
29 | initLayouts();
30 | setupNavigator();
31 | }
32 |
33 | private void initLayouts() {
34 | navBar = new NavBar();
35 | // Use panel as main content container to allow it's content to scroll
36 | content = new Panel();
37 | content.setSizeFull();
38 | content.addStyleName(MyTheme.PANEL_BORDERLESS);
39 |
40 | addComponents(navBar, content);
41 | setExpandRatio(content, 1);
42 | }
43 |
44 | private void setupNavigator() {
45 | navigator = new Navigator(DemoUI.getCurrent(), content);
46 |
47 | registerViews();
48 |
49 | // Add view change listeners so we can do things like select the correct menu item and update the page title
50 | navigator.addViewChangeListener(navBar);
51 | navigator.addViewChangeListener(new PageTitleUpdater());
52 |
53 | navigator.navigateTo(navigator.getState());
54 | }
55 |
56 | private void registerViews() {
57 | addView(HomeView.class);
58 | addView(LayoutView.class);
59 | addView(OrdersView.class);
60 | addView(AsyncOrdersView.class);
61 | addView(LazyOrdersView.class);
62 | addView(FormView.class);
63 | addView(SlowRenderingView.class);
64 | addView(HeapDestroyerView.class);
65 | navigator.setErrorView(ErrorView.class);
66 | }
67 |
68 | /**
69 | * Registers av given view to the navigator and adds it to the NavBar
70 | */
71 | private void addView(Class extends View> viewClass) {
72 | ViewConfig viewConfig = viewClass.getAnnotation(ViewConfig.class);
73 |
74 | switch (viewConfig.createMode()) {
75 | case ALWAYS_NEW:
76 | navigator.addView(viewConfig.uri(), viewClass);
77 | break;
78 | case LAZY_INIT:
79 | navigator.addProvider(new LazyProvider(viewConfig.uri(), viewClass));
80 | break;
81 | case EAGER_INIT:
82 | try {
83 | navigator.addView(viewConfig.uri(), viewClass.newInstance());
84 | } catch (Exception e) {
85 | e.printStackTrace();
86 | }
87 | }
88 | navBar.addView(viewConfig.uri(), viewConfig.displayName());
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/NavBar.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui;
2 |
3 | import com.vaadin.navigator.ViewChangeListener;
4 | import com.vaadin.server.FontAwesome;
5 | import com.vaadin.shared.ui.label.ContentMode;
6 | import com.vaadin.ui.Button;
7 | import com.vaadin.ui.CssLayout;
8 | import com.vaadin.ui.Label;
9 | import org.vaadin.marcus.util.MyTheme;
10 | import org.vaadin.marcus.util.event.EventBus;
11 | import org.vaadin.marcus.util.event.LogoutEvent;
12 | import org.vaadin.marcus.util.event.NavigationEvent;
13 |
14 | import java.util.HashMap;
15 | import java.util.Map;
16 |
17 | public class NavBar extends CssLayout implements ViewChangeListener {
18 |
19 | private Map buttonMap = new HashMap<>();
20 |
21 | public NavBar() {
22 | setHeight("100%");
23 | addStyleName(MyTheme.MENU_ROOT);
24 | addStyleName(MyTheme.NAVBAR);
25 |
26 | Label logo = new Label("AwesomeApp", ContentMode.HTML);
27 | logo.addStyleName(MyTheme.MENU_TITLE);
28 | addComponent(logo);
29 |
30 | addLogoutButton();
31 | }
32 |
33 | private void addLogoutButton() {
34 | Button logout = new Button("Log out", click -> EventBus.post(new LogoutEvent()));
35 | addComponent(logout);
36 |
37 | logout.addStyleName(MyTheme.BUTTON_LOGOUT);
38 | logout.addStyleName(MyTheme.BUTTON_BORDERLESS);
39 | logout.setIcon(FontAwesome.SIGN_OUT);
40 | }
41 |
42 | public void addView(String uri, String displayName) {
43 | Button viewButton = new Button(displayName,
44 | click -> EventBus.post(new NavigationEvent(uri)));
45 | viewButton.addStyleName(MyTheme.MENU_ITEM);
46 | viewButton.addStyleName(MyTheme.BUTTON_BORDERLESS);
47 | buttonMap.put(uri, viewButton);
48 |
49 | addComponent(viewButton, components.size() - 1);
50 | }
51 |
52 | @Override
53 | public boolean beforeViewChange(ViewChangeEvent event) {
54 | return true; // false blocks navigation, always return true here
55 | }
56 |
57 | @Override
58 | public void afterViewChange(ViewChangeEvent event) {
59 | buttonMap.values().forEach(button -> button.removeStyleName(MyTheme.SELECTED));
60 | Button button = buttonMap.get(event.getViewName());
61 | if (button != null) button.addStyleName(MyTheme.SELECTED);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/components/OrdersGrid.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.components;
2 |
3 |
4 | import com.vaadin.data.Container;
5 | import com.vaadin.ui.Grid;
6 | import com.vaadin.ui.renderers.ProgressBarRenderer;
7 | import org.vaadin.marcus.util.converter.CurrencyConverter;
8 | import org.vaadin.marcus.util.converter.DateTimeConverter;
9 | import org.vaadin.marcus.util.converter.LineItemConverter;
10 | import org.vaadin.marcus.util.converter.PercentageConverter;
11 |
12 | public class OrdersGrid extends Grid {
13 |
14 | public void setContainer(Container.Indexed container) {
15 | setContainerDataSource(container);
16 |
17 | removeColumn("id");
18 |
19 | setColumnOrder("orderTime", "progress", "lineItems", "orderTotal", "status");
20 |
21 | getColumn("progress").setConverter(new PercentageConverter());
22 | getColumn("progress").setRenderer(new ProgressBarRenderer());
23 |
24 | getColumn("orderTime").setConverter(new DateTimeConverter());
25 | getColumn("lineItems").setConverter(new LineItemConverter());
26 | getColumn("lineItems").setMaximumWidth(300);
27 | getColumn("orderTotal").setConverter(new CurrencyConverter());
28 |
29 | getColumn("orderTime").setLastFrozenColumn();
30 | }
31 |
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/components/VerticalSpacedLayout.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.components;
2 |
3 | import com.vaadin.ui.VerticalLayout;
4 |
5 | /**
6 | * Vertical layout with spacing and margin on by default
7 | */
8 | public class VerticalSpacedLayout extends VerticalLayout {
9 |
10 | public VerticalSpacedLayout() {
11 | setMargin(true);
12 | setSpacing(true);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/views/AsyncOrdersView.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.views;
2 |
3 | import com.google.common.util.concurrent.FutureCallback;
4 | import com.google.common.util.concurrent.Futures;
5 | import com.vaadin.data.util.BeanItemContainer;
6 | import com.vaadin.navigator.View;
7 | import com.vaadin.navigator.ViewChangeListener;
8 | import com.vaadin.ui.Alignment;
9 | import com.vaadin.ui.HorizontalLayout;
10 | import com.vaadin.ui.Label;
11 | import com.vaadin.ui.ProgressBar;
12 | import org.vaadin.marcus.ui.components.OrdersGrid;
13 | import org.vaadin.marcus.ui.components.VerticalSpacedLayout;
14 | import org.vaadin.marcus.entity.Order;
15 | import org.vaadin.marcus.service.OrderService;
16 | import org.vaadin.marcus.util.MyTheme;
17 | import org.vaadin.marcus.util.ViewConfig;
18 |
19 | import java.util.List;
20 |
21 | @ViewConfig(uri = "async", displayName = "Orders (async)")
22 | public class AsyncOrdersView extends VerticalSpacedLayout implements View {
23 |
24 | private OrderService orderService = OrderService.get();
25 | private OrdersGrid ordersTable;
26 | private ProgressBar progressBar;
27 | private HorizontalLayout loadingNotificaton;
28 |
29 | public AsyncOrdersView() {
30 | setSizeFull();
31 |
32 | createLayout();
33 | }
34 |
35 | private void createLayout() {
36 | Label header = new Label("Orders, asynchronous");
37 | ordersTable = new OrdersGrid();
38 | addComponent(header);
39 | addLoadingNotification();
40 |
41 | header.addStyleName(MyTheme.LABEL_H1);
42 |
43 | ordersTable.setSizeFull();
44 |
45 |
46 | }
47 |
48 | private void addLoadingNotification() {
49 | progressBar = new ProgressBar();
50 | progressBar.setIndeterminate(true);
51 | loadingNotificaton = new HorizontalLayout();
52 | loadingNotificaton.setSpacing(true);
53 | loadingNotificaton.addComponents(progressBar, new Label("Hang on, fetching orders..."));
54 | addComponents(loadingNotificaton);
55 | setComponentAlignment(loadingNotificaton, Alignment.TOP_CENTER);
56 | }
57 |
58 | @Override
59 | public void enter(ViewChangeListener.ViewChangeEvent event) {
60 | fetchOrders();
61 | }
62 |
63 | protected void fetchOrders() {
64 | getUI().setPollInterval(200);
65 | // Do not block on call. Instead we use a future that notifies us when it's done.
66 | // Note that we need to have either Push or polling in use to be able to initiate changes from the server.
67 | Futures.addCallback(orderService.getOrdersAsync(), new FutureCallback>() {
68 | @Override
69 | public void onSuccess(List orders) {
70 | showOrders(orders);
71 | }
72 |
73 | @Override
74 | public void onFailure(Throwable throwable) {
75 | // Do reasonable things to handle failure
76 | }
77 | });
78 | }
79 |
80 | protected void showOrders(List orders) {
81 |
82 | //Note getUI() vs UI.getCurrent(). The latter is not accessible from the background thread.
83 | getUI().access(() -> {
84 | ordersTable.setContainer(new BeanItemContainer<>(Order.class, orders));
85 | replaceComponent(loadingNotificaton, ordersTable);
86 | setExpandRatio(ordersTable, 1);
87 | getUI().setPollInterval(-1);
88 | });
89 |
90 | }
91 |
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/views/ErrorView.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.views;
2 |
3 | import com.vaadin.navigator.View;
4 | import com.vaadin.navigator.ViewChangeListener;
5 | import com.vaadin.ui.Alignment;
6 | import com.vaadin.ui.Label;
7 | import com.vaadin.ui.VerticalLayout;
8 | import org.vaadin.marcus.util.MyTheme;
9 |
10 | public class ErrorView extends VerticalLayout implements View {
11 | @Override
12 | public void enter(ViewChangeListener.ViewChangeEvent event) {
13 | setSizeFull();
14 | setMargin(true);
15 | Label label = new Label("Could not find a view with that name. You are most likely doing it wrong.");
16 | label.addStyleName(MyTheme.LABEL_FAILURE);
17 |
18 | addComponent(label);
19 | setComponentAlignment(label, Alignment.MIDDLE_CENTER);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/views/HeapDestroyerView.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.views;
2 |
3 |
4 | import com.vaadin.navigator.View;
5 | import com.vaadin.navigator.ViewChangeListener;
6 | import com.vaadin.ui.Label;
7 | import com.vaadin.ui.VerticalLayout;
8 | import org.vaadin.marcus.util.ViewConfig;
9 |
10 | @ViewConfig(
11 | uri="heap-destroyer",
12 | displayName = "Heap Destroyer",
13 | createMode = ViewConfig.CreateMode.ALWAYS_NEW)
14 | public class HeapDestroyerView extends VerticalLayout implements View {
15 |
16 | public static final int BIGNUM = 10000;
17 |
18 | private long[][] largeArray = new long[BIGNUM][BIGNUM];
19 |
20 | public HeapDestroyerView() {
21 | setMargin(true);
22 | for (int i = 0; i < BIGNUM; i++) {
23 | for (int j = 0; j < BIGNUM; j++) {
24 | largeArray[i][j] = i * j;
25 | }
26 | }
27 | try {
28 | Thread.sleep(5000);
29 | } catch (InterruptedException e) {
30 | e.printStackTrace();
31 | }
32 |
33 | addComponent(new Label("Nom nom nom, your heap was delicious."));
34 | }
35 |
36 | @Override
37 | public void enter(ViewChangeListener.ViewChangeEvent viewChangeEvent) {
38 | addComponent(new Label("Loaded view"));
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/views/HomeView.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.views;
2 |
3 | import com.vaadin.navigator.View;
4 | import com.vaadin.navigator.ViewChangeListener;
5 | import com.vaadin.shared.ui.label.ContentMode;
6 | import com.vaadin.ui.Label;
7 | import org.vaadin.marcus.ui.components.VerticalSpacedLayout;
8 | import org.vaadin.marcus.util.CurrentUser;
9 | import org.vaadin.marcus.util.MyTheme;
10 | import org.vaadin.marcus.util.ViewConfig;
11 |
12 | @ViewConfig(uri = "", displayName = "Home")
13 | public class HomeView extends VerticalSpacedLayout implements View {
14 |
15 | public HomeView() {
16 | Label caption = new Label("Welcome, " + CurrentUser.get().getUsername());
17 | Label description = new Label(
18 | "This project contains a collection of tips and tricks that will hopefully make it easier and more fun for you to work with Vaadin. Please read the readme file at https://github.com/vaadin-marcus/vaadin-tips.", ContentMode.HTML);
19 |
20 | addComponents(caption, description);
21 |
22 | caption.addStyleName(MyTheme.LABEL_HUGE);
23 | description.addStyleName(MyTheme.LABEL_LARGE);
24 |
25 | }
26 |
27 | @Override
28 | public void enter(ViewChangeListener.ViewChangeEvent event) {
29 |
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/views/LayoutView.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.views;
2 |
3 | import com.vaadin.navigator.View;
4 | import com.vaadin.navigator.ViewChangeListener;
5 | import com.vaadin.ui.*;
6 | import org.vaadin.marcus.util.MyTheme;
7 | import org.vaadin.marcus.util.ViewConfig;
8 |
9 | @ViewConfig(uri = "layouts", displayName = "Layouts")
10 | public class LayoutView extends VerticalLayout implements View {
11 |
12 | public LayoutView() {
13 | addStyleName(MyTheme.LAYOUT_VIEW);
14 | setMargin(true);
15 | setSizeFull();
16 |
17 | createLayout();
18 | }
19 |
20 | private void createLayout() {
21 | HorizontalLayout layout = new HorizontalLayout();
22 | Label label = new Label("Hello");
23 | Button button = new Button("World");
24 | layout.addComponents(label, button);
25 | addComponent(layout);
26 |
27 | // label.setWidth("80%");
28 | // button.setWidth("20%");
29 |
30 | // layout.setWidth("100%");
31 |
32 | // Visualize the layout size and slot sizes. (Please don't ever use this to set styles in a real application)
33 | // Page.getCurrent().getStyles().add(".layout-view .v-horizontallayout {border: 1px solid blue; padding-right: 4px;} .layout-view .v-horizontallayout .v-slot {border: 1px solid red;}");
34 |
35 | // layout.setExpandRatio(label, 1.0f);
36 | // button.setSizeUndefined();
37 |
38 |
39 | // layout.setExpandRatio(label, 0.8f);
40 | // layout.setExpandRatio(button, 0.2f);
41 | // button.setWidth("100%");
42 |
43 | // layout.setComponentAlignment(label, Alignment.MIDDLE_CENTER);
44 | // label.setSizeUndefined();
45 | }
46 |
47 | @Override
48 | public void enter(ViewChangeListener.ViewChangeEvent event) {
49 |
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/views/LazyOrdersView.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.views;
2 |
3 | import com.vaadin.navigator.View;
4 | import com.vaadin.navigator.ViewChangeListener;
5 | import com.vaadin.ui.Label;
6 | import com.vaadin.ui.VerticalLayout;
7 | import org.vaadin.marcus.util.MyTheme;
8 | import org.vaadin.marcus.ui.components.OrdersGrid;
9 | import org.vaadin.marcus.service.OrderService;
10 | import org.vaadin.marcus.util.ViewConfig;
11 | import org.vaadin.viritin.LazyList;
12 | import org.vaadin.viritin.ListContainer;
13 |
14 | @ViewConfig(uri = "lazy-orders", displayName = "Orders (lazy)")
15 | public class LazyOrdersView extends VerticalLayout implements View {
16 |
17 | OrderService orderService = OrderService.get();
18 | private OrdersGrid ordersTable;
19 |
20 | public LazyOrdersView() {
21 |
22 | setSizeFull();
23 | setSpacing(true);
24 | setMargin(true);
25 |
26 | createLayout();
27 | }
28 |
29 | private void createLayout() {
30 | Label header = new Label("Orders, lazy loaded");
31 | ordersTable = new OrdersGrid();
32 | header.addStyleName(MyTheme.LABEL_H1);
33 | addComponents(header, ordersTable);
34 |
35 | ordersTable.setSizeFull();
36 | setExpandRatio(ordersTable, 1);
37 | }
38 |
39 | @Override
40 | public void enter(ViewChangeListener.ViewChangeEvent event) {
41 | // Instead of fetching any orders upfront, just tell the container where it can find data when it needs some
42 | ordersTable.setContainer(new ListContainer<>(new LazyList<>(orderService::fetchOrders, orderService::getOrderCount)));
43 | }
44 |
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/views/OrdersView.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.views;
2 |
3 | import com.vaadin.data.util.BeanItemContainer;
4 | import com.vaadin.navigator.View;
5 | import com.vaadin.navigator.ViewChangeListener;
6 | import com.vaadin.ui.Label;
7 | import com.vaadin.ui.VerticalLayout;
8 | import org.vaadin.marcus.service.OrderService;
9 | import org.vaadin.marcus.util.MyTheme;
10 | import org.vaadin.marcus.ui.components.OrdersGrid;
11 | import org.vaadin.marcus.entity.Order;
12 | import org.vaadin.marcus.util.ViewConfig;
13 |
14 | import java.util.List;
15 |
16 | @ViewConfig(uri = "orders", displayName = "Orders")
17 | public class OrdersView extends VerticalLayout implements View {
18 | private OrderService orderService = OrderService.get();
19 | private OrdersGrid ordersTable;
20 |
21 | public OrdersView() {
22 |
23 | setSizeFull();
24 | setSpacing(true);
25 | setMargin(true);
26 |
27 | createLayout();
28 | }
29 |
30 | private void createLayout() {
31 | Label header = new Label("Orders, synchronous");
32 | ordersTable = new OrdersGrid();
33 | addComponents(header, ordersTable);
34 |
35 | header.addStyleName(MyTheme.LABEL_H1);
36 | ordersTable.setSizeFull();
37 | setExpandRatio(ordersTable, 1);
38 | }
39 |
40 | @Override
41 | public void enter(ViewChangeListener.ViewChangeEvent event) {
42 | fetchOrders();
43 | }
44 |
45 | private void fetchOrders() {
46 | showOrders(orderService.getOrders());
47 | }
48 |
49 | public void showOrders(List orders) {
50 | ordersTable.setContainer(new BeanItemContainer<>(Order.class, orders));
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/views/form/FormLayout.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.views.form;
2 |
3 | import com.vaadin.data.fieldgroup.PropertyId;
4 | import com.vaadin.ui.ComboBox;
5 | import org.vaadin.marcus.entity.Order;
6 | import org.vaadin.marcus.util.NonEmptyCollectionValidator;
7 |
8 | import java.util.Arrays;
9 |
10 | public class FormLayout extends com.vaadin.ui.FormLayout {
11 |
12 | // If your field names are not the same as the property ids, remember to annotate them.
13 | @PropertyId("status")
14 | private final ComboBox statusField;
15 | @PropertyId("lineItems")
16 | private final LineItemField lineItemsField;
17 |
18 | public FormLayout() {
19 | setSpacing(true);
20 | statusField = new ComboBox("Order status");
21 | lineItemsField = new LineItemField("Line items");
22 | addComponents(statusField, lineItemsField);
23 |
24 | statusField.setNullSelectionAllowed(false);
25 | statusField.setRequired(true);
26 | Arrays.asList(Order.Status.values()).forEach(statusField::addItem);
27 |
28 | lineItemsField.setRequired(true);
29 | lineItemsField.addValidator(new NonEmptyCollectionValidator("An order must contain products"));
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/views/form/FormPresenter.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.views.form;
2 |
3 | import com.vaadin.data.fieldgroup.FieldGroup;
4 | import org.vaadin.marcus.entity.Order;
5 | import org.vaadin.marcus.service.OrderService;
6 |
7 | public class FormPresenter {
8 |
9 | protected FormView view;
10 | protected Order order;
11 | protected OrderService orderService = OrderService.get();
12 |
13 | public FormPresenter(FormView view) {
14 | this.view = view;
15 | }
16 |
17 | public void viewShown() {
18 | clearForm();
19 | }
20 |
21 | public void clearPressed(){
22 | clearForm();
23 | }
24 |
25 | public void savePressed() {
26 | try {
27 | view.commit();
28 | orderService.saveOrder(order);
29 | view.showSuccess();
30 | clearForm();
31 | }
32 | // Note that our View is leaking its internal implementation by throwing a FieldGroup.CommitException.
33 | // I opted for a simple an short solution, but purists may want to avoid.
34 | catch (FieldGroup.CommitException e) {
35 | view.showFailure();
36 | }
37 |
38 | }
39 |
40 | private void clearForm() {
41 | order = new Order();
42 | view.setOrder(order);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/views/form/FormView.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.views.form;
2 |
3 |
4 | import com.vaadin.data.fieldgroup.FieldGroup;
5 | import com.vaadin.data.util.BeanItem;
6 | import com.vaadin.event.ShortcutAction;
7 | import com.vaadin.navigator.View;
8 | import com.vaadin.navigator.ViewChangeListener;
9 | import com.vaadin.ui.*;
10 | import org.vaadin.marcus.entity.Order;
11 | import org.vaadin.marcus.ui.components.VerticalSpacedLayout;
12 | import org.vaadin.marcus.util.FieldGroupUtil;
13 | import org.vaadin.marcus.util.MyTheme;
14 | import org.vaadin.marcus.util.ViewConfig;
15 |
16 | @ViewConfig(uri = "form", displayName = "Form")
17 | public class FormView extends VerticalSpacedLayout implements View {
18 |
19 | private final FormPresenter presenter;
20 | private FormLayout formLayout;
21 | private FieldGroup fieldGroup;
22 | private Button clear;
23 | private Button save;
24 |
25 | public FormView() {
26 | presenter = new FormPresenter(this);
27 |
28 | addCaption();
29 | addForm();
30 | }
31 |
32 | private void addCaption() {
33 | Label caption = new Label("Add new order");
34 | caption.addStyleName(MyTheme.LABEL_H1);
35 | addComponent(caption);
36 | }
37 |
38 | private void addForm() {
39 | formLayout = new FormLayout();
40 | addComponents(formLayout, createButtonsLayout());
41 | }
42 |
43 | private HorizontalLayout createButtonsLayout() {
44 | HorizontalLayout buttonsLayout = new HorizontalLayout();
45 | clear = new Button("Clear", click -> presenter.clearPressed());
46 | save = new Button("Add Order", click -> presenter.savePressed());
47 | buttonsLayout.addComponents(clear, save);
48 |
49 | buttonsLayout.setSpacing(true);
50 | buttonsLayout.setWidth("100%");
51 |
52 | clear.setClickShortcut(ShortcutAction.KeyCode.ESCAPE);
53 |
54 | save.setClickShortcut(ShortcutAction.KeyCode.ENTER);
55 | save.addStyleName(MyTheme.BUTTON_PRIMARY);
56 |
57 | buttonsLayout.setExpandRatio(clear, 1);
58 | buttonsLayout.setComponentAlignment(clear, Alignment.TOP_RIGHT);
59 |
60 |
61 | return buttonsLayout;
62 | }
63 |
64 | public void setOrder(Order order) {
65 | fieldGroup = new FieldGroup(new BeanItem<>(order));
66 | fieldGroup.bindMemberFields(formLayout);
67 | FieldGroupUtil.improveUX(fieldGroup, save, clear);
68 | }
69 |
70 | @Override
71 | public void enter(ViewChangeListener.ViewChangeEvent event) {
72 | presenter.viewShown();
73 | }
74 |
75 | public void showSuccess() {
76 | Notification.show("Saved successfully");
77 | }
78 |
79 | public void showFailure() {
80 | Notification.show("You are doing it wrong.", Notification.Type.ERROR_MESSAGE);
81 | }
82 |
83 | public void commit() throws FieldGroup.CommitException {
84 | fieldGroup.commit();
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/views/form/LineItemField.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.views.form;
2 |
3 | import com.google.common.collect.Lists;
4 | import com.vaadin.data.fieldgroup.BeanFieldGroup;
5 | import com.vaadin.data.fieldgroup.FieldGroup;
6 | import com.vaadin.data.util.BeanItem;
7 | import com.vaadin.data.util.BeanItemContainer;
8 | import com.vaadin.data.validator.DoubleRangeValidator;
9 | import com.vaadin.data.validator.IntegerRangeValidator;
10 | import com.vaadin.ui.*;
11 | import org.vaadin.marcus.entity.LineItem;
12 | import org.vaadin.marcus.service.OrderService;
13 | import org.vaadin.marcus.util.FieldGroupUtil;
14 | import org.vaadin.marcus.util.MyTheme;
15 | import org.vaadin.marcus.util.converter.PercentageConverter;
16 |
17 | import java.util.List;
18 |
19 | public class LineItemField extends CustomField {
20 |
21 | private Grid lineItemsGrid;
22 | private BeanItemContainer container;
23 | private boolean spacing = true;
24 |
25 | public LineItemField(String caption) {
26 | setCaption(caption);
27 | }
28 |
29 | @Override
30 | protected Component initContent() {
31 | setWidth("100%");
32 |
33 | VerticalLayout rootLayout = new VerticalLayout();
34 | LineItemForm lineItemForm = new LineItemForm();
35 | lineItemsGrid = new Grid();
36 | rootLayout.addComponents(lineItemForm, lineItemsGrid);
37 |
38 | rootLayout.setWidth("100%");
39 | rootLayout.setSpacing(spacing);
40 |
41 | lineItemsGrid.setWidth("100%");
42 | container = new BeanItemContainer<>(LineItem.class, Lists.newArrayList());
43 | lineItemsGrid.setContainerDataSource(container);
44 | lineItemsGrid.setColumnOrder("product", "quantity", "discount");
45 |
46 | return rootLayout;
47 | }
48 |
49 |
50 | @Override
51 | protected void setInternalValue(List newValue) {
52 | // We override set internal value to get the internal value of the field to show in the Grid.
53 | if (lineItemsGrid != null) {
54 | container = new BeanItemContainer<>(LineItem.class, (List) newValue);
55 | lineItemsGrid.setContainerDataSource(container);
56 | }
57 | super.setInternalValue(newValue);
58 | }
59 |
60 | private class LineItemForm extends HorizontalLayout {
61 | private ComboBox product;
62 | private TextField quantity;
63 | private TextField discount;
64 | private BeanFieldGroup fieldGroup;
65 | private Button addButton;
66 |
67 | public LineItemForm() {
68 | setSpacing(spacing);
69 | setWidth("100%");
70 |
71 | initLayout();
72 | initFieldGroup();
73 | }
74 |
75 | private void initFieldGroup() {
76 | fieldGroup = new BeanFieldGroup<>(LineItem.class);
77 | fieldGroup.bindMemberFields(this);
78 | FieldGroupUtil.improveUX(fieldGroup, addButton, null);
79 | resetDataSource();
80 | }
81 |
82 | private void resetDataSource() {
83 | fieldGroup.setItemDataSource(new BeanItem<>(new LineItem()));
84 | }
85 |
86 | private void initLayout() {
87 | // Note that our field names are equal to the propertyIds of our item
88 | product = new ComboBox("Product");
89 | quantity = new TextField("Quantity");
90 | discount = new TextField("Discount");
91 | addButton = new Button("Add", click -> add());
92 | addComponents(product, quantity, discount, addButton);
93 |
94 | OrderService.get().getProducts().forEach(product::addItem);
95 | product.setWidth("100%");
96 | product.setRequired(true);
97 | product.setNullSelectionAllowed(false);
98 |
99 | quantity.setWidth("80px");
100 | quantity.setRequired(true);
101 | quantity.addValidator(new IntegerRangeValidator("Must be between 1 and 1", 1, 10));
102 |
103 | discount.setWidth("80px");
104 | discount.setRequired(true);
105 | discount.addValidator(new DoubleRangeValidator("Must be between 0 and 50%", 0.0, 0.5));
106 | discount.setConverter(new PercentageConverter());
107 |
108 | addButton.addStyleName(MyTheme.BUTTON_PRIMARY);
109 |
110 | setComponentAlignment(addButton, Alignment.BOTTOM_RIGHT);
111 | setExpandRatio(product, 1);
112 | }
113 |
114 | private void add(){
115 | try {
116 | fieldGroup.commit();
117 | addLineItem(fieldGroup.getItemDataSource().getBean());
118 | resetDataSource();
119 | } catch (FieldGroup.CommitException e) {
120 | Notification.show("Check your input");
121 | }
122 | }
123 |
124 | }
125 |
126 | private void addLineItem(LineItem lineItem) {
127 | List currentLineItems = Lists.newArrayList(getValue());
128 | currentLineItems.add(lineItem);
129 |
130 | // Call set value to ensure that all listeners get triggered
131 | setValue(currentLineItems);
132 | }
133 |
134 | @Override
135 | public Class extends List> getType() {
136 | return List.class;
137 | }
138 |
139 | }
140 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/views/login/LoginBox.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.views.login;
2 |
3 | import com.vaadin.event.ShortcutAction;
4 | import com.vaadin.ui.*;
5 | import org.vaadin.marcus.entity.User;
6 | import org.vaadin.marcus.service.LoginService;
7 | import org.vaadin.marcus.util.MyTheme;
8 | import org.vaadin.marcus.util.event.EventBus;
9 |
10 | import javax.security.auth.login.LoginException;
11 |
12 | public class LoginBox extends VerticalLayout {
13 |
14 | private LoginService loginService = new LoginService();
15 | private TextField username;
16 | private PasswordField password;
17 |
18 | public LoginBox() {
19 | setWidth("400px");
20 | addStyleName(MyTheme.LOGIN_BOX);
21 | setSpacing(true);
22 | setMargin(true);
23 |
24 | addCaption();
25 | addForm();
26 | addButtons();
27 | }
28 |
29 | private void addCaption() {
30 | Label caption = new Label("Login to system");
31 | addComponent(caption);
32 |
33 | caption.addStyleName(MyTheme.LABEL_H1);
34 | }
35 |
36 | private void addForm() {
37 | FormLayout loginForm = new FormLayout();
38 | username = new TextField("Username");
39 | password = new PasswordField("Password");
40 | loginForm.addComponents(username, password);
41 | addComponent(loginForm);
42 |
43 | loginForm.setSpacing(true);
44 | loginForm.forEach(component -> component.setWidth("100%"));
45 |
46 | username.focus();
47 | }
48 |
49 | private void addButtons() {
50 | HorizontalLayout buttonsLayout = new HorizontalLayout();
51 | Button forgotButton = new Button("Forgot", click -> Notification.show("Not implemented", Notification.Type.TRAY_NOTIFICATION));
52 | Button loginButton = new Button("Login", click -> login());
53 | buttonsLayout.addComponents(forgotButton, loginButton);
54 | addComponent(buttonsLayout);
55 |
56 | buttonsLayout.setSpacing(true);
57 |
58 | forgotButton.addStyleName(MyTheme.BUTTON_LINK);
59 |
60 | loginButton.addStyleName(MyTheme.BUTTON_PRIMARY);
61 | loginButton.setClickShortcut(ShortcutAction.KeyCode.ENTER);
62 |
63 | setComponentAlignment(buttonsLayout, Alignment.BOTTOM_RIGHT);
64 | }
65 |
66 | private void login() {
67 | try {
68 | User user = loginService.login(username.getValue(), password.getValue());
69 | EventBus.post(new LoginEvent(user));
70 | } catch (LoginException e) {
71 | Notification.show("Login failed.", "Hint: use any non-empty strings", Notification.Type.WARNING_MESSAGE);
72 | username.focus();
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/views/login/LoginEvent.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.views.login;
2 |
3 | import org.vaadin.marcus.entity.User;
4 |
5 | public class LoginEvent {
6 | private User user;
7 |
8 | public LoginEvent(User user) {
9 | this.user = user;
10 | }
11 |
12 | public User getUser() {
13 | return user;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/views/login/LoginView.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.views.login;
2 |
3 | import com.vaadin.ui.Alignment;
4 | import com.vaadin.ui.VerticalLayout;
5 |
6 | public class LoginView extends VerticalLayout {
7 |
8 | public LoginView() {
9 | setSizeFull();
10 | setDefaultComponentAlignment(Alignment.MIDDLE_CENTER);
11 | addComponent(new LoginBox());
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/views/render/OrderLayout.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.views.render;
2 |
3 | import com.vaadin.ui.*;
4 | import org.joda.time.format.DateTimeFormat;
5 | import org.vaadin.marcus.entity.Order;
6 | import org.vaadin.marcus.util.MyTheme;
7 |
8 | import java.text.NumberFormat;
9 |
10 |
11 | public class OrderLayout extends CustomComponent {
12 |
13 | public OrderLayout(Order order) {
14 | Panel panel = new Panel();
15 | HorizontalLayout layout = new HorizontalLayout();
16 | panel.setContent(layout);
17 | setCompositionRoot(panel);
18 |
19 | Label orderTime = new Label();
20 | VerticalLayout lineItemsLayout = new VerticalLayout();
21 | Label lineItemsCaption = new Label("Line items:");
22 | Label orderTotal = new Label("");
23 |
24 | lineItemsLayout.addComponent(lineItemsCaption);
25 | layout.addComponents(orderTime, lineItemsLayout, orderTotal);
26 |
27 | panel.setCaption("Order " + order.getId());
28 |
29 | layout.setSpacing(true);
30 | layout.setMargin(true);
31 | layout.setWidth("100%");
32 |
33 | if(order.getOrderTime() != null) {
34 | orderTime.setValue(order.getOrderTime().toString(DateTimeFormat.mediumDateTime()));
35 | }
36 | orderTime.setSizeUndefined();
37 | orderTime.addStyleName(MyTheme.LABEL_BOLD);
38 |
39 | lineItemsCaption.addStyleName(MyTheme.LABEL_BOLD);
40 | order.getLineItems().forEach(lineItem -> lineItemsLayout.addComponent(
41 | new Label(lineItem.getQuantity() + " " + lineItem.getProduct().getName() + " – " + NumberFormat.getCurrencyInstance().format(lineItem.getProduct().getPrice()))
42 | ));
43 |
44 | orderTotal.setSizeUndefined();
45 | orderTotal.setValue(NumberFormat.getCurrencyInstance().format(order.getOrderTotal()));
46 | orderTotal.addStyleName(MyTheme.LABEL_BOLD);
47 |
48 | layout.setExpandRatio(lineItemsLayout, 1);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/views/render/QuickOrderLayout.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.views.render;
2 |
3 | import com.vaadin.shared.ui.label.ContentMode;
4 | import com.vaadin.ui.CssLayout;
5 | import com.vaadin.ui.Label;
6 | import org.joda.time.format.DateTimeFormat;
7 | import org.vaadin.marcus.entity.Order;
8 | import org.vaadin.marcus.util.MyTheme;
9 |
10 | import java.text.NumberFormat;
11 |
12 | public class QuickOrderLayout extends CssLayout {
13 |
14 | public QuickOrderLayout(Order order) {
15 | addStyleName(MyTheme.ORDER_LAYOUT);
16 | setWidth("100%");
17 | Label orderIdLabel = new Label("Order " + order.getId());
18 | Label orderTime = new Label();
19 | Label lineItems = new Label();
20 | Label orderTotal = new Label();
21 | addComponents(orderIdLabel, orderTime, lineItems, orderTotal);
22 |
23 | orderIdLabel.addStyleName(MyTheme.ORDER_ID);
24 |
25 | orderTime.setWidth("30%");
26 | lineItems.setWidth("55%");
27 | orderTotal.setWidth("15%");
28 |
29 | orderTime.setValue(order.getOrderTime().toString(DateTimeFormat.mediumDateTime()));
30 | orderTime.setSizeUndefined();
31 | orderTime.addStyleName(MyTheme.LABEL_BOLD);
32 |
33 | lineItems.setContentMode(ContentMode.HTML);
34 | StringBuilder lineItemsContent = new StringBuilder();
35 | lineItemsContent.append("Line items: ");
36 |
37 | order.getLineItems().forEach(lineItem -> {
38 | lineItemsContent.append(lineItem.getQuantity());
39 | lineItemsContent.append(" ");
40 | lineItemsContent.append(lineItem.getProduct().getName());
41 | lineItemsContent.append(" – ");
42 | lineItemsContent.append(NumberFormat.getCurrencyInstance().format(lineItem.getProduct().getPrice()));
43 | lineItemsContent.append(" ");
44 | });
45 | lineItems.setValue(lineItemsContent.toString());
46 |
47 | orderTotal.setSizeUndefined();
48 | orderTotal.setValue(NumberFormat.getCurrencyInstance().format(order.getOrderTotal()));
49 | orderTotal.addStyleName(MyTheme.LABEL_BOLD);
50 | orderTotal.addStyleName(MyTheme.ORDER_TOTAL);
51 |
52 |
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/ui/views/render/SlowRenderingView.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.views.render;
2 |
3 | import com.vaadin.navigator.View;
4 | import com.vaadin.navigator.ViewChangeListener;
5 | import org.vaadin.marcus.ui.components.VerticalSpacedLayout;
6 | import org.vaadin.marcus.service.OrderService;
7 | import org.vaadin.marcus.util.ViewConfig;
8 |
9 | @ViewConfig(uri="slow-render", displayName = "Slow rendering view")
10 | public class SlowRenderingView extends VerticalSpacedLayout implements View {
11 |
12 | @Override
13 | public void enter(ViewChangeListener.ViewChangeEvent event) {
14 | OrderService.get()
15 | .fetchOrders(0, 200)
16 | .forEach(order -> addComponent(new OrderLayout(order)));
17 | }
18 | }
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/util/CurrentUser.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.util;
2 |
3 | import com.vaadin.server.VaadinSession;
4 | import org.vaadin.marcus.entity.User;
5 |
6 | /**
7 | * Convenience wrapper for storing and retreiving a user from the VaadinSession
8 | */
9 | public class CurrentUser {
10 |
11 | private static final String KEY = "currentser";
12 |
13 | public static void set(User user) {
14 | VaadinSession.getCurrent().setAttribute(KEY, user);
15 | }
16 |
17 | public static User get() {
18 | return (User) VaadinSession.getCurrent().getAttribute(KEY);
19 | }
20 |
21 | public static boolean isLoggedIn() {
22 | return get() != null;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/util/FieldGroupUtil.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.util;
2 |
3 | import com.vaadin.data.Property;
4 | import com.vaadin.data.fieldgroup.FieldGroup;
5 | import com.vaadin.ui.AbstractField;
6 | import com.vaadin.ui.Button;
7 |
8 |
9 | public class FieldGroupUtil {
10 |
11 | /**
12 | * Improves validation UX and enables/disables save/reset buttons depending on FieldGroup state.
13 | *
14 | * NOTE: This is not a fully complete/foolproof version. Please see AbstractForm, MBeanFieldGroup, and MTextField
15 | * Viritin for a more complete solution. https://github.com/viritin/viritin
16 | */
17 | public static void improveUX(FieldGroup fieldGroup, Button saveButton, Button clearButton) {
18 |
19 | saveButton.setEnabled(false);
20 |
21 | if (clearButton != null) {
22 | clearButton.setEnabled(false);
23 | }
24 |
25 | Property.ValueChangeListener buttonStateListener = event -> {
26 | // Only enable clearing if user has changed something
27 | if (clearButton != null) {
28 | clearButton.setEnabled(true);
29 | }
30 | // Only enable save if the fieldgroup is valid
31 | saveButton.setEnabled(fieldGroup.isValid());
32 | };
33 |
34 | fieldGroup.getFields().forEach(field -> {
35 | AbstractField f = (AbstractField) field;
36 |
37 | // Validate on the fly
38 | f.setImmediate(true);
39 | f.addValueChangeListener(buttonStateListener);
40 |
41 | if(!f.getValidators().isEmpty()) {
42 |
43 | // Don't tell the user they've done something wrong if they haven't even tried filling the form yet
44 | f.setValidationVisible(false);
45 | f.addValueChangeListener(new Property.ValueChangeListener() {
46 |
47 | @Override
48 | public void valueChange(Property.ValueChangeEvent event) {
49 | f.setValidationVisible(true);
50 | f.removeValueChangeListener(this);
51 | }
52 | });
53 | }
54 | });
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/util/LazyProvider.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.util;
2 |
3 | import com.vaadin.navigator.Navigator;
4 | import com.vaadin.navigator.View;
5 |
6 | /**
7 | * Lazily initializes a view when it's first accessed, then always returns the
8 | * same instance on subsequent calls.
9 | */
10 | public class LazyProvider extends Navigator.ClassBasedViewProvider {
11 | private View view;
12 |
13 | public LazyProvider(String viewName, Class extends View> viewClass) {
14 | super(viewName, viewClass);
15 | }
16 |
17 | @Override
18 | public View getView(String viewName) {
19 | if (view == null) {
20 | view = super.getView(viewName);
21 | }
22 | return view;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/util/MyTheme.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.util;
2 |
3 | import com.vaadin.ui.themes.ValoTheme;
4 |
5 | /**
6 | * Helper for theme. Less typos in CSS style names and easier to find usages in project.
7 | */
8 | public class MyTheme extends ValoTheme {
9 |
10 | public static final String MAIN_LAYOUT = "main-layout";
11 | public static final String NAVBAR = "navbar";
12 | public static final String SELECTED = "selected";
13 | public static final String LOGIN_BOX = "login-box";
14 |
15 |
16 | public static final String BUTTON_LOGOUT = "logout";
17 | public static final String ORDER_LAYOUT = "order-layout";
18 | public static final String ORDER_ID = "order-id";
19 | public static final String ORDER_TOTAL = "order-total";
20 |
21 | public static final String LAYOUT_VIEW = "layout-view";
22 | }
23 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/util/NonEmptyCollectionValidator.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.util;
2 |
3 | import com.vaadin.data.validator.AbstractValidator;
4 |
5 | import java.util.Collection;
6 |
7 | /**
8 | * Setting a field as required will not prevent empty lists from being submitted.
9 | */
10 | public class NonEmptyCollectionValidator extends AbstractValidator {
11 |
12 | public NonEmptyCollectionValidator(String errorMessage) {
13 | super(errorMessage);
14 | }
15 |
16 | @Override
17 | protected boolean isValidValue(Collection value) {
18 | return value != null && !value.isEmpty();
19 | }
20 |
21 | @Override
22 | public Class getType() {
23 | return Collection.class;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/util/PageTitleUpdater.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.util;
2 |
3 | import com.vaadin.navigator.View;
4 | import com.vaadin.navigator.ViewChangeListener;
5 | import com.vaadin.server.Page;
6 |
7 | public class PageTitleUpdater implements ViewChangeListener {
8 | @Override
9 | public boolean beforeViewChange(ViewChangeEvent event) {
10 | return true;
11 | }
12 |
13 | @Override
14 | public void afterViewChange(ViewChangeEvent event) {
15 |
16 | View view = event.getNewView();
17 | ViewConfig viewConfig = view.getClass().getAnnotation(ViewConfig.class);
18 |
19 | if (viewConfig != null) {
20 | Page.getCurrent().setTitle(viewConfig.displayName());
21 | }
22 |
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/util/ViewConfig.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.util;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | /**
9 | * Annotation for easily collecting View meta info. Less typos, more awesome.
10 | */
11 | @Target(ElementType.TYPE)
12 | @Retention(RetentionPolicy.RUNTIME)
13 | public @interface ViewConfig {
14 | enum CreateMode {ALWAYS_NEW, LAZY_INIT, EAGER_INIT}
15 |
16 | String uri();
17 | String displayName();
18 | CreateMode createMode() default CreateMode.ALWAYS_NEW;
19 | }
20 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/util/converter/CurrencyConverter.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.util.converter;
2 |
3 | import com.vaadin.data.util.converter.Converter;
4 |
5 | import java.text.NumberFormat;
6 | import java.util.Locale;
7 |
8 | public class CurrencyConverter implements Converter {
9 | NumberFormat formatter = NumberFormat.getCurrencyInstance();
10 |
11 | @Override
12 | public Double convertToModel(String value, Class extends Double> targetType, Locale locale) throws ConversionException {
13 | return null;
14 | }
15 |
16 | @Override
17 | public String convertToPresentation(Double value, Class extends String> targetType, Locale locale) throws ConversionException {
18 | return formatter.format(value);
19 | }
20 |
21 | @Override
22 | public Class getModelType() {
23 | return Double.class;
24 | }
25 |
26 | @Override
27 | public Class getPresentationType() {
28 | return String.class;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/util/converter/DateTimeConverter.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.util.converter;
2 |
3 | import com.vaadin.data.util.converter.Converter;
4 | import org.joda.time.DateTime;
5 | import org.joda.time.format.DateTimeFormat;
6 | import org.joda.time.format.DateTimeFormatter;
7 |
8 | import java.util.Locale;
9 |
10 | public class DateTimeConverter implements Converter {
11 | DateTimeFormatter format = DateTimeFormat.mediumDateTime();
12 |
13 | @Override
14 | public DateTime convertToModel(String value, Class extends DateTime> targetType, Locale locale) throws ConversionException {
15 | return null;
16 | }
17 |
18 | @Override
19 | public String convertToPresentation(DateTime value, Class extends String> targetType, Locale locale) throws ConversionException {
20 | return format.print(value);
21 | }
22 |
23 | @Override
24 | public Class getModelType() {
25 | return DateTime.class;
26 | }
27 |
28 | @Override
29 | public Class getPresentationType() {
30 | return String.class;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/util/converter/LineItemConverter.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.util.converter;
2 |
3 | import com.vaadin.data.util.converter.Converter;
4 | import org.vaadin.marcus.entity.LineItem;
5 |
6 | import java.util.List;
7 | import java.util.Locale;
8 | import java.util.stream.Collectors;
9 |
10 | public class LineItemConverter implements Converter {
11 | @Override
12 | public List convertToModel(String value, Class extends List> targetType, Locale locale) throws ConversionException {
13 | return null;
14 | }
15 |
16 | @Override
17 | public String convertToPresentation(List value, Class extends String> targetType, Locale locale) throws ConversionException {
18 | return String.join(", ", ((List)value)
19 | .stream()
20 | .map(LineItem::toString)
21 | .collect(Collectors.toList()));
22 | }
23 |
24 | @Override
25 | public Class getModelType() {
26 | return List.class;
27 | }
28 |
29 | @Override
30 | public Class getPresentationType() {
31 | return String.class;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/util/converter/PercentageConverter.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.util.converter;
2 |
3 | import com.vaadin.data.util.converter.Converter;
4 |
5 | import java.util.Locale;
6 |
7 | public class PercentageConverter implements Converter {
8 | @Override
9 | public Double convertToModel(String value, Class extends Double> targetType, Locale locale) throws ConversionException {
10 | double val = 0.0;
11 | try {
12 | val = new Double(value.replaceAll("[^\\d.]", "")) / 100;
13 | } catch (NumberFormatException nex) {
14 | // ignore invalid input
15 | }
16 | return val;
17 | }
18 |
19 | @Override
20 | public String convertToPresentation(Double value, Class extends String> targetType, Locale locale) throws ConversionException {
21 | return String.valueOf(value * 100) + "%";
22 | }
23 |
24 | @Override
25 | public Class getModelType() {
26 | return Double.class;
27 | }
28 |
29 | @Override
30 | public Class getPresentationType() {
31 | return String.class;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/util/event/EventBus.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.util.event;
2 |
3 | import org.vaadin.marcus.DemoUI;
4 |
5 | /**
6 | * Convenience class for accessing the _UI Scoped_ EventBus. If you are using something like the CDI event
7 | * bus, you don't need a class like this.
8 | */
9 | public class EventBus {
10 |
11 | public static void register(final Object listener) {
12 | DemoUI.getEventBus().register(listener);
13 | }
14 |
15 | public static void unregister(final Object listener) {
16 | DemoUI.getEventBus().unregister(listener);
17 | }
18 |
19 | public static void post(final Object event) {
20 | DemoUI.getEventBus().post(event);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/util/event/LogoutEvent.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.util.event;
2 |
3 | public class LogoutEvent {
4 | }
5 |
--------------------------------------------------------------------------------
/demo-ui/src/main/java/org/vaadin/marcus/util/event/NavigationEvent.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.util.event;
2 |
3 |
4 | public class NavigationEvent {
5 | private String viewName;
6 |
7 | public NavigationEvent(String viewName) {
8 | this.viewName = viewName;
9 | }
10 |
11 | public String getViewName() {
12 | return viewName;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/demo-ui/src/main/resources/org/vaadin/marcus/MyAppWidgetset.gwt.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/demo-ui/src/main/webapp/VAADIN/themes/mytheme/addons.scss:
--------------------------------------------------------------------------------
1 | /* This file is automatically managed and will be overwritten from time to time. */
2 | /* Do not manually edit this file. */
3 |
4 |
5 | /* Import and include this mixin into your project theme to include the addon themes */
6 | @mixin addons {
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/demo-ui/src/main/webapp/VAADIN/themes/mytheme/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/marcushellberg/vaadin-tips/ff66978a1b6a3e645a8544dee0e630286f38fd9f/demo-ui/src/main/webapp/VAADIN/themes/mytheme/favicon.ico
--------------------------------------------------------------------------------
/demo-ui/src/main/webapp/VAADIN/themes/mytheme/mytheme.scss:
--------------------------------------------------------------------------------
1 | // Global variable overrides. Must be declared before importing Valo.
2 |
3 | // Defines the plaintext font size, weight and family. Font size affects general component sizing.
4 | //$v-font-size: 16px;
5 | //$v-font-weight: 300;
6 | //$v-font-family: "Open Sans", sans-serif;
7 |
8 | // Defines the border used by all components.
9 | //$v-border: 1px solid (v-shade 0.7);
10 | //$v-border-radius: 4px;
11 |
12 | // Affects the color of some component elements, e.g Button, Panel title, etc
13 | //$v-background-color: hsl(210, 0%, 18.67%);
14 | // Affects the color of content areas, e.g Panel and Window content, TextField input etc
15 | //$v-app-background-color: $v-background-color;
16 |
17 | // Affects the visual appearance of all components
18 | //$v-gradient: v-linear 8%;
19 | //$v-bevel-depth: 30%;
20 | //$v-shadow-opacity: 5%;
21 |
22 | // Defines colors for indicating status (focus, success, failure)
23 | //$v-focus-color: valo-focus-color(); // Calculates a suitable color automatically
24 | //$v-friendly-color: #2c9720;
25 | //$v-error-indicator-color: #ed473b;
26 |
27 | // For more information, see: https://vaadin.com/book/-/page/themes.valo.html
28 | // Example variants can be copy/pasted from https://vaadin.com/wiki/-/wiki/Main/Valo+Examples
29 |
30 | @import "../valo/valo.scss";
31 | @import "views/render.scss";
32 |
33 | @mixin mytheme {
34 | @include valo;
35 | @include render;
36 |
37 | .login-box {
38 | width: 400px;
39 | }
40 |
41 | .navbar {
42 | width: 250px;
43 | position: relative;
44 |
45 | .valo-menu-item,
46 | .valo-menu-title {
47 | text-align: left;
48 | }
49 |
50 | .logout {
51 | position: absolute;
52 | bottom: 10px;
53 | }
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/demo-ui/src/main/webapp/VAADIN/themes/mytheme/styles.scss:
--------------------------------------------------------------------------------
1 | @import "mytheme.scss";
2 | @import "addons.scss";
3 |
4 | // This file prefixes all rules with the theme name to avoid causing conflicts with other themes.
5 | // The actual styles should be defined in mytheme.scss
6 |
7 | .mytheme {
8 | @include addons;
9 | @include mytheme;
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/demo-ui/src/main/webapp/VAADIN/themes/mytheme/views/render.scss:
--------------------------------------------------------------------------------
1 | @mixin render {
2 | .order-layout {
3 | background: #fff;
4 | border: 1px solid #ddd;
5 | border-radius: 4px;
6 | box-sizing: border-box;
7 |
8 | .v-label {
9 | padding: 5px;
10 | }
11 |
12 | b {
13 | font-weight: 500;
14 | }
15 |
16 | .order-id {
17 | @include valo-gradient();
18 | border-bottom: 1px solid #ddd;
19 | font-weight: 300;
20 | }
21 |
22 | .order-total {
23 | float: right;
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/demo-ui/src/test/java/org/vaadin/marcus/ui/pageobjects/LoginViewPO.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.pageobjects;
2 |
3 | import com.vaadin.testbench.TestBenchTestCase;
4 | import com.vaadin.testbench.elements.*;
5 | import org.openqa.selenium.WebDriver;
6 |
7 | import static junit.framework.Assert.assertTrue;
8 |
9 | public class LoginViewPO extends TestBenchTestCase {
10 | public LoginViewPO(WebDriver driver) {
11 | setDriver(driver);
12 | }
13 |
14 | public MainViewPO login() {
15 | TextFieldElement usernameTextField = $(TextFieldElement.class).caption("Username").first();
16 | PasswordFieldElement passwordPasswordField = $(PasswordFieldElement.class).caption("Password").first();
17 | ButtonElement loginButton = $(ButtonElement.class).caption("Login").first();
18 |
19 | String name = "Test";
20 | usernameTextField.sendKeys(name);
21 | passwordPasswordField.sendKeys("test");
22 | loginButton.click();
23 |
24 | MainViewPO mainViewPO = new MainViewPO(driver);
25 | assertTrue(mainViewPO.isDisplayed());
26 |
27 | return mainViewPO;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/demo-ui/src/test/java/org/vaadin/marcus/ui/pageobjects/MainViewPO.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.pageobjects;
2 |
3 | import com.vaadin.testbench.TestBenchTestCase;
4 | import org.openqa.selenium.By;
5 | import org.openqa.selenium.WebDriver;
6 |
7 | public class MainViewPO extends TestBenchTestCase {
8 | public MainViewPO(WebDriver driver) {
9 | setDriver(driver);
10 | }
11 |
12 | public boolean isDisplayed() {
13 | return getDriver().findElement(By.className("valo-menu-title")).isDisplayed();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/demo-ui/src/test/java/org/vaadin/marcus/ui/pageobjects/TBUtils.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.pageobjects;
2 |
3 | import com.vaadin.testbench.TestBench;
4 | import org.openqa.selenium.WebDriver;
5 | import org.openqa.selenium.firefox.FirefoxDriver;
6 |
7 | public class TBUtils {
8 |
9 | private static final String TARGET_URL = "http://localhost:8080/demo-ui/?restartApplication=true";
10 |
11 | public static LoginViewPO openInitialView() {
12 | WebDriver driver = TestBench.createDriver(new FirefoxDriver());
13 | driver.get(TARGET_URL);
14 | LoginViewPO initialView = new LoginViewPO(driver);
15 | return initialView;
16 | }
17 | }
--------------------------------------------------------------------------------
/demo-ui/src/test/java/org/vaadin/marcus/ui/regression/FormRegressionIT.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.regression;
2 |
3 | import com.vaadin.testbench.TestBenchTestCase;
4 | import com.vaadin.testbench.elements.ButtonElement;
5 | import com.vaadin.testbench.elements.ComboBoxElement;
6 | import com.vaadin.testbench.elements.GridElement;
7 | import com.vaadin.testbench.elements.NotificationElement;
8 | import org.junit.After;
9 | import org.junit.Before;
10 | import org.junit.Test;
11 | import org.vaadin.marcus.ui.pageobjects.MainViewPO;
12 | import org.vaadin.marcus.ui.pageobjects.TBUtils;
13 |
14 | import static org.junit.Assert.assertTrue;
15 |
16 | public class FormRegressionIT extends TestBenchTestCase {
17 |
18 | private MainViewPO mainViewPO;
19 |
20 | @Before
21 | public void setUp() {
22 | mainViewPO = TBUtils.openInitialView().login();
23 |
24 | // our context is now in the main view
25 | setDriver(mainViewPO.getDriver());
26 | }
27 |
28 | @Test
29 | public void testForm() {
30 | // Inlining is fine for the first test, but you should pretty quickly
31 | // refactor this into a page object for the FormView
32 | $(ButtonElement.class).caption("Form").first().click();
33 |
34 | ComboBoxElement orderStatusComboBox = $(ComboBoxElement.class).caption("Order status").first();
35 | ComboBoxElement productComboBox = $(ComboBoxElement.class).caption("Product").first();
36 | GridElement listItemsGrid = $(GridElement.class).first();
37 | ButtonElement addButton = $(ButtonElement.class).caption("Add").first();
38 | ButtonElement addOrderButton = $(ButtonElement.class).caption("Add Order").first();
39 |
40 | orderStatusComboBox.openPopup();
41 | orderStatusComboBox.selectByText(orderStatusComboBox.getPopupSuggestions().get(0));
42 |
43 | productComboBox.openPopup();
44 | String selectedProduct = productComboBox.getPopupSuggestions().get(0);
45 | productComboBox.selectByText(selectedProduct);
46 |
47 | addButton.click();
48 |
49 | assertTrue(listItemsGrid.getRow(0).getText().contains(selectedProduct));
50 |
51 | addOrderButton.click();
52 |
53 | NotificationElement notification = $(NotificationElement.class).first();
54 | assertTrue(notification.getCaption().contains("Saved"));
55 | notification.close();
56 | }
57 |
58 | @After
59 | public void tearDown() {
60 | mainViewPO.getDriver().quit();
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/demo-ui/src/test/java/org/vaadin/marcus/ui/regression/RenderingSpeedIT.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.regression;
2 |
3 | import com.vaadin.testbench.TestBenchTestCase;
4 | import com.vaadin.testbench.elements.ButtonElement;
5 | import org.junit.After;
6 | import org.junit.Before;
7 | import org.junit.Test;
8 | import org.vaadin.marcus.ui.pageobjects.MainViewPO;
9 | import org.vaadin.marcus.ui.pageobjects.TBUtils;
10 |
11 | import static org.junit.Assert.*;
12 |
13 | public class RenderingSpeedIT extends TestBenchTestCase {
14 |
15 | private MainViewPO mainViewPO;
16 |
17 | @Before
18 | public void setUp() {
19 | mainViewPO = TBUtils.openInitialView().login();
20 | }
21 |
22 | @Test
23 | public void testRenderingSpeed() {
24 | mainViewPO.$(ButtonElement.class).caption("Slow rendering view").first().click();
25 |
26 | int maxAllowedRenderTime = 1000;
27 | long renderTime = mainViewPO.testBench().timeSpentRenderingLastRequest();
28 | // This fails on purpose here, the view is made to be slow for the demo
29 | assertTrue("Render took " + renderTime + "ms, max allowed is " + maxAllowedRenderTime + "ms", renderTime < maxAllowedRenderTime);
30 | }
31 |
32 | @After
33 | public void tearDown() {
34 | mainViewPO.getDriver().close();
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/demo-ui/src/test/java/org/vaadin/marcus/ui/views/form/FormPresenterTest.java:
--------------------------------------------------------------------------------
1 | package org.vaadin.marcus.ui.views.form;
2 |
3 | import com.vaadin.data.fieldgroup.FieldGroup;
4 | import org.junit.Before;
5 | import org.junit.Test;
6 | import org.mockito.Mock;
7 | import org.mockito.MockitoAnnotations;
8 | import org.vaadin.marcus.entity.Order;
9 | import org.vaadin.marcus.service.OrderService;
10 |
11 | import static junit.framework.Assert.assertNotNull;
12 | import static org.mockito.Mockito.doThrow;
13 | import static org.mockito.Mockito.verify;
14 |
15 | public class FormPresenterTest {
16 |
17 | @Mock
18 | OrderService orderService;
19 |
20 | @Mock
21 | FormView view;
22 |
23 | private FormPresenter presenter;
24 |
25 | @Before
26 | public void setUp() {
27 | MockitoAnnotations.initMocks(this);
28 |
29 | presenter = new FormPresenter(view);
30 |
31 | // Notice: here, we're using a mocked service to unit test the presenter logic in isolation
32 | // If you want to do an integration test, wire up the actual service class so you can
33 | // exercise the full stack
34 | presenter.orderService = orderService;
35 | }
36 |
37 | @Test
38 | public void testInit() {
39 | presenter.viewShown();
40 | assertNotNull("A new order did not get created", presenter.order);
41 | verify(view).setOrder(presenter.order);
42 | }
43 |
44 |
45 | @Test
46 | public void testSuccessfulSave() throws FieldGroup.CommitException {
47 | Order order = new Order();
48 | presenter.order = order;
49 |
50 | presenter.savePressed();
51 |
52 | verify(view).commit();
53 | verify(orderService).saveOrder(order);
54 | verify(view).showSuccess();
55 |
56 | }
57 |
58 | @Test
59 | public void testFailedSave() throws FieldGroup.CommitException {
60 | doThrow(new FieldGroup.CommitException()).when(view).commit();
61 | presenter.savePressed();
62 | verify(view).showFailure();
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | org.vaadin.marcus
8 | vaadin-tips
9 | pom
10 | 1.0-SNAPSHOT
11 |
12 | demo-services
13 | demo-ui
14 |
15 |
16 |
17 | UTF-8
18 |
19 | 7.6.4
20 | ${vaadin.version}
21 | [2.0.0,)
22 |
23 | 9.2.3.v20140905
24 | 1.9.5
25 | 4.11
26 |
27 |
28 |
29 |
30 |
31 | vaadin-addons
32 | http://maven.vaadin.com/vaadin-addons
33 |
34 |
35 | vaadin-snapshots
36 | https://oss.sonatype.org/content/repositories/vaadin-snapshots/
37 |
38 | false
39 |
40 |
41 | true
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | com.vaadin
50 | vaadin-bom
51 | ${vaadin.version}
52 | pom
53 | import
54 |
55 |
56 |
57 | javax.servlet
58 | javax.servlet-api
59 | 3.0.1
60 | provided
61 |
62 |
63 |
64 | org.vaadin.marcus
65 | demo-services
66 | ${project.version}
67 |
68 |
69 |
70 | org.vaadin
71 | viritin
72 | 1.49
73 |
74 |
75 |
76 | joda-time
77 | joda-time
78 | 2.7
79 |
80 |
81 |
82 | com.google.guava
83 | guava
84 | [16.0,)
85 |
86 |
87 |
88 | org.mockito
89 | mockito-all
90 | ${mockito.version}
91 | test
92 |
93 |
94 | junit
95 | junit
96 | ${junit.version}
97 | test
98 |
99 |
100 |
101 | com.vaadin
102 | vaadin-testbench
103 | 4.1.0.alpha2
104 | test
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 | org.apache.maven.plugins
114 | maven-compiler-plugin
115 | 3.5.1
116 |
117 | 1.8
118 | 1.8
119 |
120 |
121 |
122 |
123 |
124 |
125 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Vaadin tips and tricks
2 |
3 | This project and document contains a collection of tips and tricks that will hopefully make it easier and more fun for you to work with Vaadin.
4 |
5 | Note, this is not a challenge. Please do not try to incorporate everything you see here into your project all at once unless it makes sense for your project. As always, use common sense and strive for the simplest solution that solves your problem.
6 |
7 |
8 | ## Use dependency management
9 |
10 | Unless you find some sort of twisted pleasure in scouring Google for jar files, I suggest that you use a tool like Maven to handle your dependencies. It may be fairly straight forward to get your project up and running without, but maintenance will become a big headache in the long run.
11 |
12 | If you are using Maven, you can additionally benefit from the archetypes we provide to kickstart your project:
13 |
14 | * vaadin-archetype-application - basic application skeleton
15 | * vaadin-archetype-application-multimodule - multimodule project with production and development profiles
16 | * vaadin-archetype-application-example - an example CRUD app with a responsive layout
17 | * vaadin-archetype-widget - startingpoint for building your own widgets
18 |
19 | All the archetypes are listed [here](https://vaadin.com/maven#archetypes).
20 |
21 | ## Use the Navigator
22 |
23 | Vaadin comes with a built in `Navigator` helper that maps between Views and uri fragments. This allows you to create bookmarkable views within your application. Correctly used, the `Navigator` will also help you keep your application memory usage in check.
24 |
25 | ### Login and initializing the Navigator
26 |
27 | Don't initialize the Navigator unless a user is logged in. Keep your login view separate from the rest of your navigation, and only create the Navigator once the user has authenticated. This way, you do not inadvertently expose any content to unauthorized users, and you may select which views to register for a certain user (for instance only register an admin view if an admin logged in). See `DemoUI.init()` and `AwesomeApp#setupNavigator()`.
28 |
29 | Once the user is logged in, you want to call `navigator.navigateTo(navigator.getState())` to make sure the user is forwarded to the page they were trying to access before getting redirected to the login page.
30 |
31 | ### Use constants for View names
32 |
33 | Define your view names in constants to avoid making typos. In this project, I created a custom annotation `ViewConfig` that I used to store the view name, display name (for the menu and page title) and the creation modes (I'll cover creation modes shortly). The main thing to keep in mind is that **you do not want to be typing view names by hand in every place you use them.**
34 |
35 | See `OrdersView` for an example use of how I've used the `ViewConfig` annotation. The metadata is then used in `AwesomeApp.addView` to both register the view to the `Navigator` and to add it to the navigation bar.
36 |
37 | ### Keep memory use and load times in check with correct View registration
38 |
39 | When registering a view, be very careful how you do it. Doing it wrong may cause your application to use significantly more memory than needed. By default, there are two ways of doing this:
40 |
41 | 1. `navigator.addView("viewname", View.class)`
42 | 2. `navigator.addView("viewname", new View())`
43 |
44 | The difference between these, is that #1 will always give you a new instance of a View when you navigate to it, whereas #2 always returns the same instance. **Unless you have very compelling reasons to use #2, you should always use #1**. If you use #2, the Navigator needs to keep a reference to the View in order to return it later. This means that no Views can be garbage collected and so registering views this way will significantly increase the memory used per session.
45 |
46 | If you have a case where you need the same instance to always be returned, you should defer the instantiation of that View until it is actually needed. This way you don't increase the initial startup time of your application. `LazyProvider` contains an example implementation of how to do this.
47 |
48 | In this demo project, we define the instantiation mode through the `@ViewConfig` annotation. You can see the actual view registration in `AwesomeApp.addView`.
49 |
50 | ### Use ViewChangeListeners wisely
51 |
52 | `ViewChangeListener` is a very handy way of attaching logic to view transitions. In this project, we use them to update the selected menu item in the navigation bar and for setting the Page title. See `AwesomeApp.setupNavigator` for the registration of the listeners and `NavBar#afterViewChange` and `PageTitleUpdater` for implementations.
53 |
54 | You can also implement access control by doing checks in a `ViewChangeListener` and returning `false` from the `beforeViewChange` method. Please remember though that the best way of preventing access to views is to not register views in the first place for a user that doesn't have access.
55 |
56 | ### Always define an error view
57 |
58 | **Always!** This is the equivalent to a 404 page for your Vaadin application. Without this, any user navigating to an incorrect URL will get an ugly stacktrace.
59 |
60 | ### Use the enter method for initializing a View
61 |
62 | Especially if you are eagerly instantiating your Views, you should defer any actual data lookup and initialization to the enter method that is shown when the user actually opens the view.
63 |
64 |
65 | ## Components and layouting
66 |
67 | Vaadin is a component based framework. Because everything in Vaadin is a component and defined in Java, you can easily create your own reusable compositions.
68 |
69 | For instance -- in this project, instead of having to set up a Grid from scratch in each of the Order examples, I opted to extend Grid and create a more specialized OrderGrid, that already contained the needed column setup and converters I needed. This way, I could keep the View implementations simple and to the point.
70 |
71 | ### CustomComponent or direct inheritance?
72 |
73 | When creating a composition, you have two options: using `CustomComponent` or just extending an existing component. The benefit of using `CustomComponent` is that it hides all internal implementation details and allows you to specify the Component API. The disadvantage of using `CustomComponent` is that it will increase complexity, both in your code and in the DOM, which can in some cases have an impact on rendering speed.
74 |
75 | In general, I prefer directly extending a component to keep things simple and lightweight. If you are creating a component for someone else to use, you may consider using a CustomComponent to wrap it.
76 |
77 | ### Spacing and margins
78 |
79 | One often overlooked feature of the Vaadin layouts is `setMargin` and `setSpacing`. In most cases, turning these on will give you a much more visually pleasing result, as all your components aren't squished together. In fact, I use this so often that I created a custom `VerticalSpacedLayout` that defaults to having them enabled.
80 |
81 | ### Sizing and positioning
82 | Vaadin has a very powerful layout engine. If you understand how it works, you will have a very powerful tool for creating fluid layouts, but used wrong it will cause a lot of headaches.
83 |
84 | There are two different sizes you need to take into account when working with the default Vaadin layouts (Vertical, Horizontal, Grid): the size of a component and the size of the layout slot it is in. A component's size, what you get when you call `setSize()` is the size of the component in relation to the layout slot the component is in. The size of the layout slot is determined by calling `setExpandRatio()` on the containing layout. ExpandRatio will determine how any unused space should be divided between the components. If you do not specify any values, all slots will be equally sized. The position of a component is also relative to the slot it is in.
85 |
86 | Another common mistake I see in Vaadin projects is having relatively sized components inside layouts without a defined size. So if you for have a VerticalLayout that by default has an undefined height (it will grow as needed with the content), you cannot put a component with a 100% height inside of it. Vaadin will not be able to calculate 100% of null, so the component will not be shown. You can easily locate these types of issues by running your application with the debug window open (append `?debug` to your application URL) and runnin the analyze layouts tool.
87 |
88 | ## Theme
89 |
90 | Vaadin comes with a powerful Sass based theme builder called Valo. Valo allows you to customize the look and feel of your application by configuring a set of high-level parameters. You can make large modifications to your application with just a single change, as unspecified values will be automatically populated with reasonable values. For instance, if you change the background color to a dark color, Valo will change text and highlight colors to a lighter color to ensure sufficient contrast.
91 |
92 | When working with themes, it is usually a good idea to store CSS class names in a helper class as constants. In this application, we extend the Valo theme, so our theme helper class `MyTheme` extends the `ValoTheme` helper, to give us access to all the already defined classnames. See `LoginBox` for an example of using the helper class.
93 |
94 | When building your application, you usually want to add reasonable CSS classnames to relevant layouts and components. This will make changing the look and feel much easier later on.
95 |
96 | When styling your application, try to stay away from writing selectors against any `.v-` prefixed classnames, as this will make your CSS more bound to the specific implementing class. So, instead of styling `.v-vertical-layout`, give your layout a meaningful name with `addStylename()` and write your selector for that. This way, you have more freedom in changing the implementing layout without affecting your styles.
97 |
98 | Take advantage of the fact that Sass allows you to split up your stylesheets into separate files without incurring any runtime overhead. You will usually want to keep styles for different views/components in separate files for easier maintenance. When compiled, all your CSS will be in one file.
99 |
100 | **Remember that you need to manually compile the SCSS before moving to production, as the automatic compilation is disabled in production mode!**
101 |
102 | ## Dealing with large data sets
103 |
104 | A common question in Vaadin development is how one should handle large data sets. In this application, I've created three examples to show the typical approaches to solving the problem. We're working with a order system that has around 10,000 orders and is quite slow. Let's look at the pros and cons of each approach below.
105 |
106 | ### First option: Load all data and put it in a container
107 |
108 | The most straight-forward solution is of course that we just load all the Orders, put them in a Container, and tell our Grid to display the data. This example can be found in `OrdersView`.
109 |
110 | The problem with loading all data upfront is that the application will feel very sluggish to the user. We will also end up using a significant amount of memory on the server. For systems with a few concurrent users, this may not be an issue worth addressing. But for systems with a lot of concurrent users it will make a difference.
111 |
112 | The benefit of this approach is of course it's simplicity. If you are OK with the page taking a while to load and only have a few users on a server concurrently, this simple approach is just fine.
113 |
114 | ### Second option: Load data asynchronously
115 |
116 | In order to make the application more responsive, we'll improve our solution by delegating the long running application to a background thread and show the user a message and a spinner while we fetch the data.
117 |
118 | By immediately loading the page and informing the user what we're doing, the system will feel much more responsive. Users tend to tolerate much longer delays if you inform them what is happening instead of just having the UI hang. Once the data are available, we'll again populate a container and swap out a Grid instead of our placeholder spinner. Source code for this step can be found in `AsyncOrdersView`.
119 |
120 | **Note:** When updating the UI from a background thread, we need to use either polling or push to allow the server to initiate a UI update. Remember to use
121 | the `getUI().access()` helper to properly handle locking while updating the UI. Also note that helpers such as `UI.getCurrent()` which depend on `ThreadLocal`s will not work in background threads, so be sure that you pass your background thread all the needed info.
122 |
123 | This version is already much better from a usability standpoint, but will still suffer from the same memory usage problem as our first attempt.
124 |
125 | ### Third option: Lazily loading data as it's needed
126 |
127 | In order to address both initial load speed and memory usage, our third option (found in `LazyOrdersView`) will only query as much data as it can show in the grid when you open the page, and will fetch more data as needed when you scroll down.
128 |
129 | The third option is clearly better than the two previous from a memory consumption perspective. The downside is that the implementation is much more complex to implement, especially if you need to support sorting and filtering. In this example, I based the solution on `LazyList` from [Viritin](https://vaadin.com/directory#!addon/viritin). There are other lazy loading container implementations such as the JPAContainer, SQLContainer, and [LazyQueryContainer](https://vaadin.com/directory#!addon/lazy-query-container) that can help you with the implementation.
130 |
131 | ### But do you really need all that data?
132 |
133 | Before spending too much time on figuring out how to display 100,000 rows of data in a table, I sincerely suggest that you take a step back and think about what you are doing. Showing thousands of rows of data to a user is rarely the ideal way of presenting data. How often do you look up the business hours of a restaurant by searching 'restaurant' in Google and clicking yourself all the way to page 12,043?
134 |
135 | If possible, try to offer your user better search tools or filters so you don't need to deal with huge data sets in the first place. If you only need to show 50 rows instead of 100,000, putting them in an in-memory container won't be an issue anymore.
136 |
137 | ## Forms
138 |
139 | Another very common question from Vaadin developers is how to create forms easily. I've tried to show you some tips in the `FormView` class.
140 |
141 | Usually, it is easiest to keep your form in a Component of its own. This way, you can easily tell `FieldGroup` to scan the component for your fields to bind. Remember that the names of your fields need to exactly match the property ids in your `Item`, otherwise you'll need to annotate them with `@PropertyId`, like I did in `FormLayout`.
142 |
143 | ### Use custom fields to handle lists and nested objects
144 |
145 | Unless your form only contains strings and numbers, there's a good chance you'll want to create your own Fields to handle the editing of those. In our example, I've created a custom field for editing the list of line items in an Order. You can see the implementation in `LineItemField`. The custom field is essentially just an embedded `FieldGroup` for editing line items in our order.
146 |
147 | By creating our own fields, we can keep the main form simple, and we're able to reuse the implementation in other parts of the software.
148 |
149 | ### Validation and improving the user experience
150 |
151 | Unfortunately, validating forms in Vaadin is not very nice. If you attach a validator to a field, it will already tell the user there's an error before the user has even started filling in the form.
152 |
153 | Improving the validation is a bit of work, so I created a helper found in `FieldGroupUtil.improveUX`. Calling this method will turn on live validation and make sure that we don't bug our users with validation errors before they've even tried to populate a field.
154 |
155 | In addition, the utility will help improve UX by toggling the enabled state of your save/cancel buttons. The cancel button won't be active until a user has made a change to the form, and the save button will only be active once all validations pass.
156 |
157 | Please note that this is a slightly simplified solution to keep the code understandable, please refer to AbstractForm, MBeanFieldGroup, and MTextField in [Viritin](https://github.com/viritin/viritin) for a more complete solution.
158 |
159 | ## Design patterns
160 |
161 | Rather than getting myself into an ideological warfare over the One Right Way™ of building software, I'll share a couple of practices that can hopefully help you improve your code quality and productivity.
162 |
163 | The two things I'll focus on are decoupling and separation concerns.
164 |
165 | ### Decoupling your code
166 |
167 | As you have seen, Vaadin allows you to easily create custom components and compositions so you can work with components that are tailored to your needs. All the reusability benefits of building these compositions fly out the window immediately if your `UsefulForm` depends directly on `VerySpecificView`. One solution for this is to use an observer pattern and using callback interfaces. This is how Vaadin components work. In order to have a `Button` do your work, you give it a `ClickListener` and it'll let you know when it was triggered. This is a pattern that you can also use in your own components.
168 |
169 | When communicating between different parts of your application, a generic EventBus can be incredibly valuable. An event bus allows you to publish any type of event to a central dispatcher that will then call all registered listeners. So in essence, the same idea as with the `ClickListener`, just with a broker in between.
170 |
171 | In this sample application you can for instance see that I'm firing `NavigationEvent`s from the NavBar. `DemoUI` is subscribed to these events and will make sure we get navigated where we need to go. `NavBar` is fully unaware of the fact that it is the `DemoUI` that implements the functionality. To keep the tech stack as simple as possible in this project, I've used the [Google Guava](https://code.google.com/p/guava-libraries/) event bus. CDI events in Java EE are also a great way of accomplishing the same.
172 |
173 | Please don't go overboard with the use of an event bus. The downside of this level of decoupling is that understanding program flow can become very difficult. So again, use your judgement and use the tools you have responsibly.
174 |
175 | ### Separation of concerns
176 |
177 | Another important topic is separation of concerns. In practice, this tends to manifest itself as a Model View Whatever (MVP, MVC, MVVM) pattern where you separate the UI logic from the layouts and components. This enables us to better test our UI code and makes it easier to find issues in our code as the logic is not split into tens of different UI components.
178 |
179 | You can see one very simple MVP example in `FormView`/`FormPresenter` (`Order` is the M in this case). In a real world application you will most likely end up creating abstract View and Presenter classes to handle common use cases like registering views and showing success/error messages.
180 |
181 | The actual implementation is in my opinion less important than the fact that you keep the concerns separated.
182 |
183 | Be sure to read [this excellent article on using MVP and Vaadin](https://vaadin.com/blog/-/blogs/is-mvp-a-best-practice-) written by our main architect at Vaadin.
184 |
185 | ## Testing
186 |
187 | There are a few levels of testing that we can do with our Vaadin application. Here, we'll look at unit/integration testing and regression testing.
188 |
189 | ## Unit testing our UI logic
190 |
191 | Since we split our `FormView` into a View/Presenter pair, we can easily write normal JUnit tests for the Presenter without having to deal with instantiating Vaadin components or figuring out things like how to stub out static calls to VaadinSession.
192 |
193 | `FormPresenterTest` shows a simple test that uses mock objects instead of both `OrderService` and `FormView`. This way we have full control over the tests and can exercise things like exception handling without having to manipulate an external system.
194 |
195 | Note that by changing at what level you stub out the rest of the system, you can also write integration tests in the same way. We could for instance have used an actual OrderService implementation instead of the mock to exercise the entire system.
196 |
197 | ## Full stack regression testing
198 |
199 | On top of the more low-level unit tests, it's good to write some broader regression tests to ensure that you don't break your app by mistake while working on something else. [Vaadin TestBench](https://vaadin.com/add-ons/testbench) is a tool that is built for creating regression tests for Vaadin apps. It will allow us to build tests that run our application through an actual browser.
200 |
201 | `FormRegressionIT` shows an example of a test that fills in our order form and submits it. TestBench also allows us to measure performance. In `RenderingSpeedIT` we have built a test that ensures a view is rendered quicker than a given threshold.
202 |
203 | **Note** Vaadin TestBench is a commercial add-on for Vaadin. You can obtain free licenses for non-profit use on our web page. There is also a 30 day trial available. In order to run the tests in this project you'll need to have [Firefox ESR](https://www.mozilla.org/en-US/firefox/organizations/all/) installed.
204 |
205 | Note that integration tests are not typically executed all the time during development or basic development build - as the require a considerably more time than simple unit tests and they require you to have a server up and running. You can run integration tests as JUnit tests form your IDE (when you have the project running in a server). Alternatively you can execute them with Maven using "it" profile. That profile also starts and automatically deploys the application using a Jetty server. Typically you'd have this profile enabled in your CI server. The maven command that runs integration tests:
206 |
207 | mvn verify -Pit
208 |
209 | ## Optimization
210 |
211 | We've already seen that the speed of our application can play a big role in how our users perceive our app. In the previous example, we were able to remedy the issue by not loading the entire large dataset upfront. What if the cause for slowness isn't as apparent?
212 |
213 | Before we can start fixing the problem, we need to figure out what the problem is. One good starting point is to see whether the slowness is on the server side or in the browser. You can use the Vaadin debug window to do this. Launch your application with the `?debug` parameter and look at the console output. We are interested in two things:
214 |
215 | 1. Server visit took **XXms**
216 | 2. Processing time was **XXms** for NNN characters of JSON
217 |
218 | The first tells us how long we spent on the server processing the request, the second tells us how long the browser spent rendering the result. If the time taken on the server is significantly lower than the time for rendering, we'll have to start looking at what might cause rendering slowness.
219 |
220 | ## Optimizing slow rendering
221 |
222 | Slow rendering is almost always caused by unnecessarily complex and deep component hierarchies. If you have complex layouts, see if you could get rid of some wrapping layouts, Panels or CustomComponents.
223 |
224 | Try changing the implementation used in `SlowRenderingView` to `QuickOrderLayout` and you'll see that the render time drops to about a third of what it was initially.
225 |
226 | Again, please start with a simple solution and only optimize if you actually identify a problem. Flattening our hierarchy by inlining HTML is similar to optimizing a function in assembler in a C. You will trade code clarity and strong typing for optimization.
227 |
228 | ## Profiling the server
229 |
230 | If the slowness is on the server, you'll want to use a profiler to see where in the code you're spending most time. Typically it will be in fetching data. You may want to consider asynchronous loading if the delay is very noticeable and you cannot optimize it by tweaking DB indexes or optimizing your service level code.
231 |
232 | You will also want to check on your application memory usage to keep things running smoothly in production. Download [VisualVM](https://visualvm.github.io/) and attach to your running server. See the difference in how memory is freed when changing how you instantiate your Views. Try changing the create mode of `HeapDestroyer` from LAZY to ALWAYS_NEW and you'll notice that the second option allows us to immediately free the memory once the user is no longer on the view.
233 |
234 | ## Keep things simple, refactor constantly
235 |
236 | Always try to keep your project as simple as possible. Don't introduce any new technology/pattern/whatever unless there is really a need for it.
237 |
238 | Also remember to refactor constantly. Instead of trying to figure out and plan for all possible reusable components up front, start by building concrete implementations. When you feel like you are building the same thing for the second time, it's time to refactor the commonalities into a common class. I feel that working this way will keep you moving quickly and help avoid unnecessary complexity.
239 |
240 | ## Ask for help before you're in trouble
241 |
242 | Often having a quick 15 minute talk with a Vaadin expert before you start programming can save you a lot of debugging and refactoring later. We're here to help you with anything form short problem solving tasks to long term consulting, [just give us a call or email us](https://vaadin.com/company).
243 |
--------------------------------------------------------------------------------