123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188 |
- ---
- 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);
- }
- ....
|