aboutsummaryrefslogtreecommitdiffstats
path: root/documentation/articles/EnableAndDisableButtonsToIndicateState.asciidoc
blob: e6ccb856219bc2345dfbb7202dedba537a759915 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
---
title: Enable And Disable Buttons To Indicate State
order: 59
layout: page
---

[[enable-and-disable-buttons-to-indicate-state]]
= Enable and disable buttons to indicate state

Most user interfaces have actions that can only be performed if certain
conditions are met. In other cases, the actions can be performed at any
time in principle, but don’t really make any sense to in certain
situations. And quite often, there are actions that really need to be
performed, e.g. to prevent data loss.

A good example of this is a typical CRUD form for entering items into a
database, with buttons for saving, reverting (i.e. discarding changes)
and deleting items:

image:img/potus1.png[POTUS Database CRUD example]

The above image illustrates a typical UI for adding, modifying and
deleting data: A table listing the available items above, and a form for
editing the selected item below. The same form is also used to enter new
items. The _Add new_ button prepares the form for entering a new item.
Clicking a table row selects the corresponding item for editing.

[[disabling-actions-to-prevent-errors]]
Disabling actions to prevent errors
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Naturally, the Save action in the UI depicted above can only be
performed if an existing item has been selected, or if the _“Add new”_
button has been clicked to create a new item. Assuming there are
required fields (which there nearly always are), the _Save_ action can
only be successfully performed when all these have been properly filled
in. Let’s call these two requirements the *_technical criteria_* for
performing the _Save_ action.

Similarly, the _Delete_ action can only be performed if an existing,
previously saved item is selected. Attempting to delete a nonexistent
(yet to be saved) item would result in an error. Thus, selection of an
existing item is a technical criterion for the _Delete_ action.

So how do we handle these criteria in our code? An unfortunately common
solution is to display a pop-up error message explaining the situation.
The problem with this approach is that the user’s time is wasted
invoking an unperformable action and in being forced to dismiss an
annoying pop-up window (usually by clicking “OK” or something to that
effect). Also, users tend to ignore popups and just click them away
without reading the message, so they might not even be aware that the
action wasn’t performed.

A clearly superior approach is to simply *disable actions until their
criteria are fulfilled*. By disabling actions that cannot be currently
performed, the user gets a clear visual indication of this situation, is
spared the trouble of attempting in vain to perform the action, and the
nuisance of an error message.

image:img/potus2.png[Save and Revert actions disabled when they cannot be
successfully
performed.]

[[disablingenabling-actions-to-indicate-state]]
Disabling/enabling actions to indicate state
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The action criteria discussed so far are only the purely _technical_
criteria for performing the _Save_ and _Delete_ actions. They are simply
there to prevent an exception from being thrown or a database constraint
being violated. Looking beyond the _technical_ requirements, neither the
_Save_ action or the _Revert_ action actually _do_ anything unless there
are *unsaved changes* in the form, so it doesn’t really make sense do
perform them at that time, even though they wouldn't result in an error.
We could call the existence of unsaved changes the *_logical criteria_*
for the _Save_ and _Revert_ actions.

On the other hand, if there _are_ unsaved changes, then either _Save_ or
_Revert_ should be performed to either save the changes or revert the
fields to their original values, and you definitely want your users to
be aware of this state.

It might seem unlikely that a user would be unaware of the state of the
form he or she is currently filling in, but out in The Real World, your
users will be constantly distracted by co-workers, incoming emails,
internet porn, coffee breaks and shiny things. They probably have “a
hunch” about whether they already clicked _Save_ or not, but even then
they might have some doubts about whether that action was _successfully
performed_. In the end, any uncertainty about whether their precious
data is safely stored is a tiny source of unnecessary stress for your
users.

The following graphic illustrates a UI that does not, in any way,
indicate the current state of the form:

image:img/disabled-before.png[UI without form state indication]

Thus, both of these states (unsaved changes or not) should be indicated
to the user somehow. The solution, again, is *disabling and enabling*
the corresponding actions: The _Save/Cancel_ buttons are *disabled*
until any change is made in the form. As soon as changes are detected,
and the new values have been validated, the _Save/Cancel_ buttons are
*enabled*. When either one is clicked, both are *disabled* again to
indicate that the action was successfully performed.

With this approach we add even more information about the current state
of the application to the buttons themselves. Not only are we indicating
when actions *_technically can_* be performed, but we also indicate when
they *_logically make sense_* to perform, and, in cases like the
_Save/Cancel_ actions in the example above, we also notify the user
about actions that *_probably should_* be performed to prevent data
loss. This is a great deal of information being *_subtly_* and
*_non-intrusively_* conveyed to the user, without resorting to annoying
popups, simply by enabling and disabling buttons.

image:img/disabled-after.png[UI with form state indication]

[[how-to-do-this-in-a-vaadin-application]]
How to do this in a Vaadin application
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

To implement the above functionality, we need to be able to trigger the
button-toggling code for changes in the following states:

* Item selection
* Field validation
* Unsaved changes

The first one, whether or not an item has been selected and loaded into
the form is quite trivial of course. You can check for that in the same
code that handles item selection.

The second one is really easy if you’ve bound the fields with a
*FieldGroup*, since in that case you can use the *isValid()* method on
the *FieldGroup* to check if all fields are valid or not. Empty required
fields cause this to return false, as do any validators you’ve
explicitly added.

The third one is a bit trickier, since a change listener has to be added
to each field separately, and the type of listener you need to add
depends on the type of field. For most field components, a
*ValueChangeListener* is fine, since it triggers a notification when the
field’s value changes, such as when a different item is selected in a
*ComboBox*. However, for the various text field components (*TextField,
TextArea and PasswordField*) you’ll be better off with a
*TextChangeListener*, since you’ll want to trigger the button-toggling
code as soon as any change is made to the field’s text content, and a
*ValueChangeListener* won’t do that.

Luckily, adding the change listeners can be done in a fairly simple loop
over the components in a layout, or the fields bound through a
*FieldGroup*. The appropriate type of listener can be chosen based on
whether the component implements the *FieldEvents.TextChangeNotifier*
interface:

[source,java]
....
TextChangeListener textListener = new TextChangeListener() {
  @Override
  public void textChange(TextChangeEvent event) {
    formHasChanged();
  }
};

ValueChangeListener valueListener = new ValueChangeListener() {
  @Override
  public void valueChange(ValueChangeEvent event) {
    formHasChanged();
  }
};

for (Field f : fieldGroup.getFields()) {
  if (f instanceof TextChangeNotifier) {
    ((TextChangeNotifier) f).addTextChangeListener(textListener);
  } else {
    f.addValueChangeListener(valueListener);
  }
}
....

[source,java]
....
public void formHasChanged() {
  btnRevert.setEnabled(true);
  boolean allFieldsValid = fieldGroup.isValid();
  btnSave.setEnabled(allFieldsValid);
}
....