]> source.dussan.org Git - vaadin-framework.git/commitdiff
Migrate EnableAndDisableButtonsToIndicateState
authorErik Lumme <erik@vaadin.com>
Wed, 13 Sep 2017 07:45:39 +0000 (10:45 +0300)
committerErik Lumme <erik@vaadin.com>
Wed, 13 Sep 2017 07:45:39 +0000 (10:45 +0300)
documentation/articles/EnableAndDisableButtonsToIndicateState.asciidoc [new file with mode: 0644]
documentation/articles/contents.asciidoc
documentation/articles/img/disabled-after.png [new file with mode: 0644]
documentation/articles/img/disabled-before.png [new file with mode: 0644]
documentation/articles/img/potus1.png [new file with mode: 0644]
documentation/articles/img/potus2.png [new file with mode: 0644]

diff --git a/documentation/articles/EnableAndDisableButtonsToIndicateState.asciidoc b/documentation/articles/EnableAndDisableButtonsToIndicateState.asciidoc
new file mode 100644 (file)
index 0000000..713fdc3
--- /dev/null
@@ -0,0 +1,183 @@
+[[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
+succesfully
+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);
+}
+....
index c3d3eebd900d0b7857e5edbd78d6cc06f4515238..90eb33e4425e44da7351b086543a80813dc16f17 100644 (file)
@@ -65,5 +65,6 @@ are great, too.
 - link:ReadOnlyVsDisabledFields.asciidoc[Read-only vs Disabled fields]
 - link:ValoThemeGettingStarted.asciidoc[Valo theme - Getting started]
 - link:UseTooltipsToClarifyFunctions.asciidoc[Use tooltips to clarify functions]
+- link:EnableAndDisableButtonsToIndicateState.asciidoc[Enable and disable buttons to indicate state]
 - link:CreatingAUIExtension.asciidoc[Creating a UI extension]
 - link:CreatingAThemeUsingSass.asciidoc[Creating a theme using Sass]
diff --git a/documentation/articles/img/disabled-after.png b/documentation/articles/img/disabled-after.png
new file mode 100644 (file)
index 0000000..dceb70d
Binary files /dev/null and b/documentation/articles/img/disabled-after.png differ
diff --git a/documentation/articles/img/disabled-before.png b/documentation/articles/img/disabled-before.png
new file mode 100644 (file)
index 0000000..cb0920e
Binary files /dev/null and b/documentation/articles/img/disabled-before.png differ
diff --git a/documentation/articles/img/potus1.png b/documentation/articles/img/potus1.png
new file mode 100644 (file)
index 0000000..f78a27d
Binary files /dev/null and b/documentation/articles/img/potus1.png differ
diff --git a/documentation/articles/img/potus2.png b/documentation/articles/img/potus2.png
new file mode 100644 (file)
index 0000000..8c60bf6
Binary files /dev/null and b/documentation/articles/img/potus2.png differ