From: Erik Lumme Date: Wed, 13 Sep 2017 07:45:39 +0000 (+0300) Subject: Migrate EnableAndDisableButtonsToIndicateState X-Git-Tag: 8.2.0.alpha2~64^2~26 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=4402f17af84e22c6487273e62ef515c6a42b9268;p=vaadin-framework.git Migrate EnableAndDisableButtonsToIndicateState --- diff --git a/documentation/articles/EnableAndDisableButtonsToIndicateState.asciidoc b/documentation/articles/EnableAndDisableButtonsToIndicateState.asciidoc new file mode 100644 index 0000000000..713fdc3a9e --- /dev/null +++ b/documentation/articles/EnableAndDisableButtonsToIndicateState.asciidoc @@ -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); +} +.... diff --git a/documentation/articles/contents.asciidoc b/documentation/articles/contents.asciidoc index c3d3eebd90..90eb33e442 100644 --- a/documentation/articles/contents.asciidoc +++ b/documentation/articles/contents.asciidoc @@ -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 index 0000000000..dceb70d098 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 index 0000000000..cb0920eead 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 index 0000000000..f78a27d549 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 index 0000000000..8c60bf638c Binary files /dev/null and b/documentation/articles/img/potus2.png differ