-
Notifications
You must be signed in to change notification settings - Fork 197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The Vaadin Way of building the Presentation Layer #3516
base: latest
Are you sure you want to change the base?
Changes from 29 commits
a9eeeef
2e72f55
9d77684
1f7aa14
a1e80df
2945e5d
e376a7e
54de9b2
d3bfc62
0450b65
f249827
8865ab3
90e931f
9b61ad4
563a000
80e2528
a76442c
14e3c84
e73c03d
6f81752
53900c1
44e82b0
7de6d06
a21fdba
ed1fa83
0055b75
4b5e87c
ef5e595
b614c89
9164352
5528c7b
e7adf50
26ad8a6
8073e85
4dd52a2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -85,6 +85,7 @@ exceptions: | |
- URL | ||
- USB | ||
- UTF | ||
- UUID | ||
- WAR | ||
- WCAG | ||
- XML | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
title: Presentation Layer | ||
description: Learn how to build the presentation layer of your Vaadin app. | ||
order: 30 | ||
--- |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
--- | ||
title: View Composition | ||
description: TODO write description | ||
order: 10 | ||
--- | ||
|
||
# View Composition | ||
|
||
You build Vaadin user interfaces by composing UI components together. Some UI components are provided out-of-the-box by Vaadin, others you have to write yourself. Some components are generic and reusable in different settings, others are designed for one specific use case and should not be reused at all. Knowing how to identify and implement these components is key to succeeding in building complex Vaadin user interfaces. | ||
|
||
You are now going to learn the basics of view composition in Vaadin. The starting point is the following mock-up of a fictional business application: | ||
|
||
image:images/application.png[alt=A mock-up of a fictional business application,width=800] | ||
|
||
Step by step, you are going to learn how to split it up into smaller parts until you are ready to implement it. | ||
|
||
## Main Layout | ||
|
||
The first step is to identify which parts of the user interface belong to the _layout_ and which belong to the _view_. A layout contains elements that are always visible in the user interface, such as the title of the application, navigation links and information about the current user. A view is one page of the user interface and is typically rendered inside a layout. As the user navigates between views, the layout remains the same. It is also possible to nest layouts inside each other. | ||
|
||
The main layout of the mock-up is the following: | ||
|
||
image:images/main-layout.png[width=800] | ||
|
||
It consists of a header and a content area. The content are contains whatever the current view is. The header consists of three different components: the application's logo, navigation links to the _Employees_, _Teams_ and _Locations_ views, and the current user's avatar. | ||
|
||
*The main layout is a UI component you have to implement.* The header is so simple that it can be nested inside the layout. | ||
|
||
## The View | ||
|
||
Having discovered the main layout, you can remove those parts from the mock-up. This leaves you with the actual Teams view, which looks like this: | ||
|
||
image:images/view.png[width=800] | ||
|
||
It is a good idea to look for common user interface design patterns. In this view, the most obvious one is the _Master-Detail_ pattern. On the left, there is a list of teams that the user can select from. When the user selects a team, its details show up on the right. | ||
|
||
From the mock-up it is unclear what should happen if there are no teams in the list or no team is selected. You should find this out before you start implementing. | ||
|
||
### Team Selection Panel | ||
|
||
The master-part of the master-detail is the team selection panel: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think |
||
|
||
image:images/view-master.png[width=200] | ||
|
||
At the top, there is a header containing the name of the Teams view, a button for creating new teams and a text field for filtering the list of teams. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are many views on this matter, but personally I prefer Oxford comma -- i.e. a comma before each 'and' in lists of three or more items. I find lists easier to read with the Oxford comma, especially when each item contains a longer bit of text, like this one does. But of course it's also important to be consistent, so if the comma is added here, it should be added to all the other lists as well. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We prefer Oxford comma in our guidelines: https://vaadin.com/docs/latest/contributing/docs/styleguide#lists And we have a Vale rule to check for that as well: https://github.com/vaadin/docs/blob/latest/.github/styles/Vaadin/OxfordComma.yml |
||
|
||
The list of teams itself consists of panels, one for each team. Each team panel contains the name and description of the team. *The team panel is a UI component you _may_ have to implement*, depending on how you choose to implement the list itself. | ||
|
||
You select a team by clicking on the panel. From the mock-up it is unclear whether keyboard navigation should be supported or not. It is also unclear whether the team selection panel is resizable or not, although the lack of a splitter indicates it has a fixed width. These are things you should find out before you start implementing, as it affects which components you can use. | ||
|
||
*The team selection panel is a UI component you have to implement*. The header is so simple that it can be nested inside the panel. | ||
|
||
### Team Details Panel | ||
|
||
The detail-part of the master-detail is the team details panel: | ||
|
||
image:images/view-detail.png[width=800] | ||
|
||
At the top, there is again a header. It contains the name and description of the team, and buttons for editing, sharing and deleting the team. | ||
|
||
To the left, there is a sidebar with two sections: one with general information about the team and another with a list of managers of the team. From the mock-up it is unclear whether the managers are clickable or not. It is also unclear whether the sidebar is resizable or not, although the lack of a splitter indicates it has a fixed width. Again, these are things you should find out before you start implementing, as it affects which components you can use. | ||
|
||
To the right, there are tabs that control the contents of the rest of the panel. This indicates that the entire team details panel is in fact a nested layout with three sub-views: _Employees_, _Salaries_, and _Documents_. In fact, the team details panel looks like this: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two 'in fact' in close succession. I might switch the second one to 'In other words', or even tweak the final sentence all the way to |
||
|
||
image:images/view-detail-panel.png[width=800] | ||
|
||
*The team details panel is a UI component you have to implement.* The sidebar and header are so simple that they can be nested inside the panel. | ||
|
||
#### Employees Sub-View | ||
|
||
The first, and only sub-view visible in the mock-up, is the Employees sub-view: | ||
|
||
image:images/employees-sub-view.png[width=600] | ||
|
||
As with ordinary views, it is a good idea to look for common design patterns. In this case, you have another Master-Detail. At the top, there is a grid of team members. When you select a team member, its details show up in a bottom panel. The splitter between the grid and the bottom panel indicates the bottom panel is resizable. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it intentional, that the team member selection doesn't show in the grid? Took me a moment to find Cody Fisher in the list. |
||
|
||
From the mock-up it is unclear what should happen if there are no employees in the grid or no employee is selected. You should find this out before you start implementing. | ||
|
||
The employee details panel is read-only, which makes it simple to implement. It contains general information about the employee. | ||
|
||
*The employees sub-view is a UI component you have to implement. The employee details panel is a UI component you _may_ have to implement*, depending on how you choose to implement the sub-view itself. | ||
|
||
## Generic Components | ||
|
||
You have now split the mock-up into a layout, a view, a nested layout and a sub-view. In addition, you have identified some panels that you need to implement. The final step before you can start to actually write code is to identify any generic components that are re-used in multiple places. In this mock-up, there are at least two. | ||
|
||
The first generic component is the person panel: | ||
|
||
image:images/person-panel.png[width=300] | ||
|
||
It is used both in the list of managers and in the employees sub-view. It contains the person's picture or avatar, name, and title. From the mock-up, you can see that the panel is smaller in the list and larger in the sub-view. | ||
|
||
The second generic component is the item with icon: | ||
|
||
image:images/items.png[width=300] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if it would be better to have these items as separate files rather than all in one, or to otherwise separate them visually somehow (a grey background behind white items, perhaps)? It took me a moment to figure out that this wasn't the entire general information item set from underneath the Summary header, but three separate items that were just shown together for comparison. |
||
|
||
It is used both in the team summary and in the employees sub-view. From the mock-up it is unclear whether the phone number and email should be clickable. You should find this out before you start implementing. | ||
|
||
As you start to implement the view, you may discover more generic components. This is especially the case with Flow, as laying out components in an imperative way is rather verbose. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
title: View Implementation with Flow | ||
description: TODO write description | ||
order: 30 | ||
--- | ||
|
||
// TODO Write me! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
title: View Structure | ||
description: TODO write description | ||
order: 10 | ||
--- |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
title: View Implementation with React | ||
description: TODO write description | ||
order: 40 | ||
--- | ||
|
||
// TODO Write me! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
--- | ||
title: URL Design | ||
description: TODO write description | ||
order: 20 | ||
--- | ||
|
||
# URL Design | ||
|
||
On the <<composition#,previous>> page, you learned how to split this mock-up into UI components: | ||
|
||
image::images/application.png[] | ||
|
||
You are now going to learn how to design the URL of the mock-up based on how a user is intended to navigate the view. The main focus is on deciding what _view state_ to store in the URL as path or query parameters. Now why is this important? | ||
|
||
Say you are working with the application, and want one of your colleagues to have a look at something. Now could ask them to open the application and tell them how to look up the information you want to show. However, if the relevant state was stored in the URL, you could send it to your colleague. The colleague would then only need to click the link to end up with the same view as you (after authenticating, of course). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
'You could', or 'Now, you could' (possibly works without a comma too)
I might go with 'send that' instead |
||
|
||
## Path Parameters | ||
|
||
Path parameters are, as their name suggests, a part of the URL path: `/view/[parameter1]/[parameter2]`. Use path parameters for navigating to a specific view, or sub-view within a view. | ||
|
||
In the fictional application, the root path of the _Teams_ view is `/teams`. If you navigate to this path, you end up with this: | ||
|
||
image::images/no-selection.png[] | ||
|
||
The first path parameter is the ID of the team: `/teams/[teamId]`. For example, the team _Research and Development (R&D)_ could have the path `/teams/8zguABh6u`. | ||
|
||
It is quite common to use incrementing long integers as database primary keys. If you use such a key in the URL, for example `/teams/1`, it is easy for an attacker to deduce that there may also be a `/teams/2`, a `/teams/3` and so on. Even if you have secured the application so that only authorized users get access regardless of whether they know the URL or not, the URL is exposing internal information. This information can be used to attack the application in another way. Instead, use a random ID such as `/teams/8zguABh6u`. Under the hood, you can still use long incrementing integers for primary and foreign keys, as they are faster than strings. | ||
|
||
The Teams view has three sub-views, visible as tabs: _Employees_, _Salaries_, and _Documents_. The selected tab is the second path parameter: `/teams/[teamId]/[tab]`. For example, the sub-views could have the following paths: | ||
|
||
* Employees: `/teams/8zguABh6u/employees` | ||
* Salaries: `/teams/8zguABh6u/salaries` | ||
* Documents: `/teams/8zguABh6u/documents` | ||
|
||
Because the Employees tab should be selected by default, the path `/teams/8zguABh6u` should be considered an alias of `/teams/8zguABh6u/employees`. If you navigate to either of these paths, you end up with this: | ||
|
||
image::images/no-employee-selection.png[] | ||
|
||
The Employees sub-view allows you to select an employee to view their details. This selection is the third path parameter and it is only valid when the Employees tab is selected: `/teams/[teamId]/employees/[employeeId]`. For example, _Cody Fisher_ could have the path `/teams/8zguABh6u/employees/zxPIVBqJ2`. If you navigate to this path, you end up with the state of the original mock-up: | ||
|
||
image::images/application.png[] | ||
|
||
## Query Parameters | ||
|
||
Query parameters are added to the end of a URL after a `?` symbol. You can specify multiple query parameters by separating them with `&` symbols: `/view?param1=foo¶m2=bar`. Use query parameters if you want to be able to share or bookmark a view while it is in a specific state. Such a state could, for example, be a particular sort order or filter. | ||
|
||
You don't have to use query parameters unless there is a clear benefit to do so. Looking at the mock-up, the view is already quite usable without query parameters. That said, it still has some candidates which you are going to look at next, for the sake of demonstration. | ||
|
||
The Teams view has a text field for filtering the list of teams. This is the first candidate for a query parameter: `/teams/8zguABh6u?search=foobar` | ||
|
||
The Employees sub-view has a grid that can be sorted by clicking at the column headers. This is the second candidate for a query parameter: `/teams/8zguABh6u/employees/?orderEmployeesBy=firstName,lastName` | ||
|
||
You can have query parameters and path parameters for both the view and the sub-view at the same time. For example, look at this URL: | ||
|
||
[source] | ||
---- | ||
/teams/8zguABh6u/employees/zxPIVBqJ2?search=research&orderEmployeesBy=firstName,lastName | ||
---- | ||
|
||
You can extract the following information: | ||
|
||
* The current view is the Teams view. | ||
* The team whose public ID is `8zguABh6u` is selected. | ||
* The Employees sub-view is visible. | ||
* The employee whose public ID is `zxPIVBqJ2` is selected. | ||
* The team list is filtered using the search term `research`. | ||
* The employee grid is sorted first by first name, then by last name. | ||
|
||
## String ID:s in URL:s | ||
|
||
The examples above used random strings as public ID:s. When you generate a random ID, you should pay attention to its usability. Since the ID is public, people may want to copy and paste it, either on its own or as a part of the URL. That means the ID must be URL-friendly, short enough and easy to copy. | ||
|
||
An easy way of generating random string ID:s in Java is to use Universally Unique IDentifiers (UUID). Java has a [classname]`UUID` class in the [packagename]`java.util` package. You can use it to generate random UUID:s by calling [methodname]`UUID.randomUUID()`. However, there is a problem with using UUID:s as public ID:s in URL:s. | ||
|
||
Try to select this UUID by double-clicking on it: | ||
|
||
[source] | ||
---- | ||
78f98876-b150-4e08-8d7e-41bb5e0f7e72 | ||
---- | ||
|
||
Chances are you ended up selecting only a part of the ID instead of the whole string. This is because of the hyphens. You can fix this by either removing the hyphens or replacing them with underscores. Try to select these ID:s by double-clicking on them: | ||
|
||
[source] | ||
---- | ||
78f98876_b150_4e08_8d7e_41bb5e0f7e72 | ||
78f98876b1504e088d7e41bb5e0f7e72 | ||
---- | ||
|
||
You should now be able to select the whole string. However, the string is still quite long and when used as URL parameters, the URL becomes quite long too: | ||
|
||
[source] | ||
---- | ||
https://myapp.example.com/teams/78f98876b1504e088d7e41bb5e0f7e72/employees/5191cfa1823e40858b0f0e10ce50c28e | ||
---- | ||
|
||
This is not a user friendly URL. You can copy-paste it, but it is difficult to read for the human eye. Telling it to somebody over the phone is next to impossible. | ||
|
||
A better alternative for public random string ID:s is _NanoId_. NanoId:s allow you to pick both the length and the alphabet. By default, the length is 21 characters and the alphabet numbers 0--9, letters A-Z and a-z, and the symbols - (hyphen) and _ (underscore). | ||
|
||
If you use 9 character NanoId:s generated from an alphabet consisting of only numbers, uppercase and lowercase letters, you end up with a URL like this: | ||
|
||
[source] | ||
---- | ||
https://myapp.example.com/teams/aftD6ZXp6/employees/m4wqAas1Q | ||
---- | ||
|
||
This is both shorter and more user friendly. | ||
|
||
NanoId has been implemented in different programming languages, among others https://github.com/aventrix/jnanoid[Java] and https://github.com/ai/nanoid[JavaScript]. | ||
|
||
## Back Button Behavior | ||
|
||
Once you are ready with the URL design, there is one more thing to consider: the browser's back button. | ||
|
||
When you navigate to a URL in a web browser, a new entry is pushed to the browser's history stack. When you click the back button, the browser goes back one step in the history stack. You can continue to click the button until the stack is empty. | ||
|
||
When a user uses a web application, it is the application itself that handles the navigation in respond to the user's actions. For example, in the mock-up application, the user would not modify the URL itself to select a team or a tab. Rather, the user would click on the team and the tab and the application would be responsible for updating the URL. | ||
|
||
When a web application updates the URL, it can do it in wo ways: either by pushing new entries to the history stack, or by replacing the current entry in the history stack. This affects the behavior of the browser's back button. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
wo -> two |
||
|
||
Consider the following use case: the user opens the application, looks for a team, selects a team, sorts the employee grid and selects an employee. If every URL change was pushed to the history stack, the history stack would look like the following, sorted from oldest to newest: | ||
|
||
* `/teams` | ||
* `/teams?search=research` | ||
* `/teams/aftD6ZXp6?search=research` | ||
* `/teams/aftD6ZXp6?search=research&orderEmployeesBy=lastName` | ||
* `/teams/aftD6ZXp6/employees/zxPIVBqJ2?search=research&orderEmployeesBy=lastName` | ||
|
||
If the user clicked the back button now, they would end up with a sorted employee grid without a selection. Another click on the button would take the user to the same employee grid, but with its default sort order. A third click on the button would deselect the team and show the search results. A final click on the button would show the list of all teams, without any filters applied. | ||
|
||
Since the search fields has no extra button, the query is executed as soon as the user stops typing. If the user is typing slowly, you may end up with entries like the following in the history stack: | ||
|
||
* `/teams?search=re` | ||
* `/teams?search=resea` | ||
* `/teams?search=research` | ||
|
||
If the user now clicked the back button, they would be confused. | ||
|
||
A good rule of thumb is to push new entries to the history stack whenever the path of the URL changes, and replace the current entry whenever query parameters change. With this behavior, the use case described earlier would result in the following history stack, sorted from oldest to newest: | ||
|
||
* `/teams?search=research` | ||
* `/teams/aftD6ZXp6?search=research&orderEmployeesBy=lastName` | ||
* `/teams/aftD6ZXp6/employees/zxPIVBqJ2?search=research&orderEmployeesBy=lastName` | ||
|
||
If the user clicked the back button now, they would end up with a sorted employee grid without a selection. Another click on the button would deselect the team and show the search results. | ||
|
||
Finally, as with all rules of thumb, there may be exceptions. The important thing is to take back button behavior into consideration when designing the view URL and make sure it makes sense. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I might tweak that to 'take the back button behavior', because when I was reading it for the first time, my brain first interpreted it as it being important to 'take back' something called 'button behavior' and then the sentence completely fell apart. It took a moment to rearrange that mentally into only 'taking' that 'back button behavior' into consideration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are -> area