reactive_forms icon indicating copy to clipboard operation
reactive_forms copied to clipboard

Validation only on Submission

Open LordOfTheNeverThere opened this issue 4 years ago • 8 comments

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 avatar Aug 21 '21 11:08 LordOfTheNeverThere

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

LordOfTheNeverThere avatar Aug 21 '21 16:08 LordOfTheNeverThere

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.

ebelevics avatar Oct 15 '21 13:10 ebelevics

You can use ReactiveFormField.showErrors.

kuhnroyal avatar Oct 15 '21 14:10 kuhnroyal

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

ebelevics avatar Oct 15 '21 15:10 ebelevics

Well you have to track your custom state yourself.

bool mySaveButtonWasClicked = false;

...

showErrors: (control) => mySaveButtonWasClicked

kuhnroyal avatar Oct 15 '21 16:10 kuhnroyal

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().

ebelevics avatar Oct 15 '21 16:10 ebelevics

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 = "[email protected]";
  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!

enzo-santos avatar Oct 28 '21 21:10 enzo-santos

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

DumbDev168 avatar Aug 06 '22 12:08 DumbDev168