joanpablo / reactive_forms

This is a model-driven approach to handling form inputs and validations, heavily inspired in Angular's Reactive Forms
MIT License
469 stars 88 forks source link

Validation only on Submission #205

Open LordOfTheNeverThere opened 3 years ago

LordOfTheNeverThere commented 3 years ago

Hi there! I couldn't see any examples of form validation that occurs only after we have tried to submit a form, such is the case when we wish to check if the user is in our DB and if the inputted password is his.

Is there any way to mark the form touched only after submission?

Example:

final authForm= FormGroup({
    "username" : FormControl<String>( validators: [Validators.required],),
    "password" : FormControl<String>(validators: [Validators.required],),
  }, asyncValidators:[_isCorrectPassword("username", "password")]
  );
ReactiveForm(
          formGroup: this.authForm,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Row(
                crossAxisAlignment: CrossAxisAlignment.center,
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Image.asset("Assets/images/HOLI_titleNBG.png", height: 50,),
                  SizedBox(width: 30,),
                  CustomText(text:"Dashboard", size: 50 , color: Colors.white,)
                ],
              ),
              ReactiveTextField(
                formControlName: 'username',
                decoration: InputDecoration(hintText: "Insert your Username", labelText: "Username"),
                validationMessages: (control)=> {
                  ValidationMessage.required : "The Username must not be empty",
                  'Not_Found' : 'That Username does not exist',

                },
              ),

              ReactiveTextField(
                formControlName: 'password',
                obscureText: true,
                decoration: InputDecoration(
                  labelText: 'Password',
                  hintText: 'Insert your Password!',),
                  validationMessages: (control) => {
                    ValidationMessage.required : "The password must not be empty",
                    'incorrect' : 'Your Username and password combination are incorrect'

                  }
                ),

              ReactiveFormConsumer(
                builder: (context, authForm, child) {
                  return TextButton(child: CustomText(text: "Sign In!",), onPressed: authForm.valid ? () {} : null,);
                }),
            ],
          ),)
// Async Custom Group Validator
 AsyncValidatorFunction _isCorrectPassword(String controlName, String passwordControlName){
  return (AbstractControl<dynamic> control) async  {
    final form = control as FormGroup;

    final username = form.control(controlName);
    final password = form.control(passwordControlName);
    var incorrect = false;

    for (var User in users) {
      await Future.delayed(Duration(seconds: 1)); //Simulate server request

      if (await Future.delayed(Duration(seconds: 1), () => User.username != username.value || User.password != password.value)){
        //Simulate server request, so there is time for the information to be gathered before advancing to a new code line

        password.setErrors({'incorrect' : true});
        incorrect=true;
      }else if(!incorrect){

        password.removeError('incorrect');
        return Future.value(null);
      }
  }
  };
}

P.S: I apologize for any mistakes, I am still a rookie at Flutter.

Thanks! :) Nice work!

LordOfTheNeverThere commented 3 years ago

I manage to do it like this: (Can't say if it's the best way)

ReactiveForm(
            formGroup: this.authForm,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Row(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Image.asset(
                      "Assets/images/HOLI_titleNBG.png",
                      height: 50,
                    ),
                    SizedBox(
                      width: 30,
                    ),
                    CustomText(
                      text: "Dashboard",
                      size: 50,
                      color: Colors.white,
                    )
                  ],
                ),
                ReactiveTextField(
                  formControlName: 'username',
                  decoration: InputDecoration(
                      hintText: "Insert your Username", labelText: "Username"),
                  validationMessages: (control) => {
                    ValidationMessage.required:
                        "The Username must not be empty",
                  },
                ),
                ReactiveTextField(
                    formControlName: 'password',
                    obscureText: true,
                    decoration: InputDecoration(
                      labelText: 'Password',
                      hintText: 'Insert your Password!',
                    ),
                    validationMessages: (control) => {
                          ValidationMessage.required:
                              "The password must not be empty",
                          'incorrect':
                              'Your Username and password combination are incorrect'
                        }),
                ReactiveFormConsumer(builder: (context, authForm, child) {
                  return TextButton(
                    child: CustomText(
                      text: "Sign In!",
                    ),
                    onPressed: () {
                      if (authForm.valid) {
                        // If the Form is valid (It will be valid if the _isCorrectPassword returns null)
                        Get.to(Layout());
                      } else {
                        // Otherwise it will release the error "incorrect" which will notify the user that their password or username are incorrect
                        authForm.control('password').setErrors({'incorrect': true});
                        authForm.control('password').markAsTouched(); // Makes the error visible to the user
                      }
                    },
                  );
                }),
              ],
            ),
          )
// Async Custom Group Validator
AsyncValidatorFunction _isCorrectPassword(
    String controlName, String passwordControlName) {
  return (AbstractControl<dynamic> control) async {
    final form = control as FormGroup;

    final username = form.control(controlName);
    final password = form.control(passwordControlName);
    var incorrect = false;

    for (var User in users) {
      await Future.delayed(
          Duration(milliseconds: 500)); //Simulate server request

      if (await Future.delayed( Duration (milliseconds: 500), () => User.username != username.value || User.password != password.value)) {
        // If one of these informations prompted by the user is wrong it means the user cannot log in
        //Simulate server request, so there is time for the information to be gathered before advancing to a new code line

        form.markAllAsTouched();
        incorrect = true;
      } else if (!incorrect) { //If the password and username combination is right it means the user prompted the right information

        return Future.value(null);
      }
    }
  };
}

Nothing Further to add

ebelevics commented 3 years ago

In my opinion this an issue why I'm not using this package. I wanted that error text show only when I'm checking is form valid, not by touching. And it seems there is no easy way how to bypass it.

kuhnroyal commented 3 years ago

You can use ReactiveFormField.showErrors.

ebelevics commented 3 years ago

I still can't even with showErrors manage to do something similar to:

     if (_formKey.currentState!.validate()) {
           FocusScope.of(context).unfocus();
           _formKey.currentState!.save();

where on validate it checks or marks form fields as invalid if that's the case

The validation seems are checked every time a change value in form field

kuhnroyal commented 3 years ago

Well you have to track your custom state yourself.

bool mySaveButtonWasClicked = false;

...

showErrors: (control) => mySaveButtonWasClicked
ebelevics commented 3 years ago

Yeah that was only solution I could also came up with. Was hoping that form has parameter inside a class, that checks if form was validated manually with some class function validate().

enzo-santos commented 3 years ago

I had the same problem and managed to solve it this way:

  1. Create a field inside your widget's state to keep track if you can execute the async validator or not:
bool _canRequest = false;
  1. Declare your send button this way:
MaterialButton(
  child: Text("Press me"),
  onPressed: () async {
    _canRequest = true;
    await Future.forEach<AsyncValidatorFunction>(
      form.asyncValidators, (validator) => validator(form));
    _canRequest = false;
  },
)
  1. Declare an async validator method inside your widget's state:
Future<Map<String, dynamic>?> _validator(AbstractControl control) async {
  if (!_canRequest) return null;

  final FormGroup form = control as FormGroup;

  // Simulate an asynchronous action
  final String email = "abc@def.com";
  final String password = (["correct", "incorrect"]..shuffle()).first;
  await Future.delayed(Duration(seconds: 5));

  if (password == "correct") return null;

  form.control("password").setErrors({"password": "Invalid password"});
}

No more changes needed!

DumbDev168 commented 2 years ago

Check this https://github.com/joanpablo/reactive_forms/issues/275#issuecomment-1207205182