Skip to content
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

RFE: show the cause of a validation error on TextModeLayout (on iOS) #2458

Closed
jsfan3 opened this issue Jun 20, 2018 · 5 comments
Closed

RFE: show the cause of a validation error on TextModeLayout (on iOS) #2458

jsfan3 opened this issue Jun 20, 2018 · 5 comments
Milestone

Comments

@jsfan3
Copy link
Contributor

jsfan3 commented Jun 20, 2018

Related question: https://stackoverflow.com/questions/50886775/codename-one-textmodelayout-on-ios-and-validation-error-feedback

I copy the question for reference:

The TextModeLayout, on iOS, produces only a red cross on the right of a text field to indicate an invalid input (if we use a Validator).

In general, this is fine. However, I added a LengthConstraint(8) to a password field and I'm worried if users understand that there is a requirement for lengtness.

Is there a standard way in the iOS design to provide the user the cause of a validation error, or the only way is to use the Material Design also on iOS (that means set to true the theme const textComponentOnTopBool)?

Code:

   TextModeLayout tl = new TextModeLayout(2, 1);
    Container credentials = new Container(tl);
    credentials.setUIID("MyContainer01");
    TextComponent email = new TextComponent().label("FieldEmail");
    TextComponent password = new TextComponent().label("FieldPassword");
    Validator val = new Validator();
    val.addConstraint(email, RegexConstraint.validEmail());
    val.addConstraint(password, new LengthConstraint(8));
    val.addSubmitButtons(loginButton);
    email.getField().setConstraint(TextField.EMAILADDR);
    password.getField().setConstraint(TextField.PASSWORD);
    InputComponent.group(email, password);
    credentials.add(email);
    credentials.add(password);
@codenameone
Copy link
Collaborator

Do you have a suggestion for how this should look?

@codenameone codenameone self-assigned this Jun 21, 2018
@codenameone codenameone added this to the Version 5.0 milestone Jun 21, 2018
@jsfan3
Copy link
Contributor Author

jsfan3 commented Jul 8, 2018

Yes, now I have a suggestion for how it should look.

I implemented this RFE with a preliminary code, as you can see in this video:
https://jmp.sh/sFYEp5g
or
https://drive.google.com/file/d/1mEVL6VgVk_EOe65qE0a_gdunXAX1T3wu/view

Basically, the error message is shown for two second in place of the label on the left of the InputComponent (or on right of the InputComponent for RTL languages). This solution never breaks the layout, because the error message is trimmed to fit the available space.

If you would like to test (and to comment and/or to improve) my code, replace in your code Validator val = new Validator(); with Validator val = new ExtendedValidator(); and, of course, import my ExtendedValidator class. That's all.

Code:

import com.codename1.ui.Component;
import com.codename1.ui.Display;
import com.codename1.ui.InputComponent;
import com.codename1.ui.Label;
import com.codename1.ui.PickerComponent;
import com.codename1.ui.TextComponent;
import com.codename1.ui.events.FocusListener;
import com.codename1.ui.util.UITimer;
import com.codename1.ui.validation.Constraint;
import com.codename1.ui.validation.GroupConstraint;
import com.codename1.ui.validation.Validator;
import [mypackage].components.utilities.StringUtils;

/**
 * Extended version of the Validator that shows the invalid input error messages
 * also when the InputComponents are not onTopMode (like on iPhone).
 *
 * @author Francesco Galgani
 */
public class ExtendedValidator extends Validator {

    /**
     * Extended version of addConstraint that shows validation errors messages
     * even when the TextModeLayout is not onTopMode. Basically, the error
     * message is shown for two second in place of the label on the left of the
     * InputComponent (or on right of the InputComponent for RTL languages).
     * This solution never breaks the layout, because the error message is
     * trimmed to fit the available space. The error message UIID is
     * "ErrorLabel".
     *
     * @param cmp TextComponent or PickerComponent
     * @param c one or more Constraints
     * @return
     */
    @Override
    public Validator addConstraint(Component cmp, Constraint... c) {
        if (!(cmp instanceof InputComponent)) {
            throw new IllegalArgumentException("addConstraint needs an InputComponent as first argument");
        }
        InputComponent inputComponent = (InputComponent) cmp;
        Constraint constraint = null;
        if (c.length == 1) {
            constraint = c[0];
        } else if (c.length > 1) {
            constraint = new GroupConstraint(c);
        }
        if (constraint == null) {
            throw new IllegalArgumentException("addConstraint needs at least a Constraint, but the Constraint array in empty");
        }
        super.addConstraint(inputComponent, constraint);

        // Show validation error on iPhone
        if (!inputComponent.isOnTopMode()) {
            Label labelForComponent = null;
            if (inputComponent instanceof TextComponent) {
                labelForComponent = ((TextComponent) inputComponent).getField().getLabelForComponent();
            } else if (inputComponent instanceof PickerComponent) {
                labelForComponent = ((PickerComponent) inputComponent).getPicker().getLabelForComponent();
            }

            if (labelForComponent != null) {
                Label myLabel = labelForComponent;
                String originalText = myLabel.getText();
                Constraint myConstraint = constraint;

                Runnable showError = () -> {
                    boolean isValid = false;
                    if (inputComponent instanceof TextComponent) {
                        isValid = myConstraint.isValid(((TextComponent) inputComponent).getField().getText());
                    } else if (inputComponent instanceof PickerComponent) {
                        isValid = myConstraint.isValid(((PickerComponent) inputComponent).getPicker().getValue());
                    }

                    String errorMessage = trimLongString(StringUtils.localize(myConstraint.getDefaultFailMessage()), "ErrorLabel", myLabel.getWidth());

                    if (errorMessage != null && !errorMessage.isEmpty() && !isValid) {
                        // show the error in place of the label for component
                        myLabel.setUIID("ErrorLabel");
                        myLabel.setText(errorMessage);
                        UITimer.timer(2000, false, Display.getInstance().getCurrent(), () -> {
                            myLabel.setUIID("Label");
                            myLabel.setText(originalText);
                        });
                    } else {
                        // show the label for component without the error
                        myLabel.setUIID("Label");
                        myLabel.setText(originalText);
                    }
                };

                FocusListener myFocusListener = new FocusListener() {

                    @Override
                    public void focusLost(Component cmp) {
                        showError.run();
                    }

                    @Override
                    public void focusGained(Component cmp) {
                        // no code here
                    }
                };

                if (inputComponent instanceof TextComponent) {
                    ((TextComponent) inputComponent).getField().addFocusListener(myFocusListener);
                } else if (inputComponent instanceof PickerComponent) {
                    ((PickerComponent) inputComponent).getPicker().addFocusListener(myFocusListener);
                }

            }
        }

        return this;
    }

    /**
     * Long error messages are trimmed to fit the available space in the layout
     *
     * @param errorMessage the string to be trimmed
     * @param uiid the uiid of the errorMessage
     * @param width the maximum width
     * @return the new String trimmed to fit the available width
     */
    private String trimLongString(String errorMessage, String uiid, int width) {
        Label errorLabel = new Label(errorMessage, uiid);
        while (errorLabel.getPreferredW() > width && errorMessage.length() > 1) {
            errorMessage = errorMessage.substring(0, errorMessage.length() - 1);
            errorLabel.setText(errorMessage);
        }
        return errorMessage;
    }

}

In the previous code, my StringUtils.localize(String string) can be replaced with UIManager.getInstance().localize(string, string). Its actual implementation is a bit more complex because I use localized strings with inner parameters like %0, %1, %2, etc., however it's a detail that doesn't matter for this RFE.

@codenameone
Copy link
Collaborator

Great job! Do you want to incorporate this feature into the validator with a PR?

@jsfan3
Copy link
Contributor Author

jsfan3 commented Jul 8, 2018

Thank you, I'm glad that you like my solution. Yes, I'll do a PR to incorporate this feature. Do I have to add a method to enable and to disable this feature? Should it be enable or disabled by default?

@codenameone
Copy link
Collaborator

That would be a good practice, I think it should be on by default as right now this information isn't visible anywhere.

I would also change setUIID("Label"); to store the UIID in a client property before changing it then restore it to the original UIID.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant