flutter_form_builder icon indicating copy to clipboard operation
flutter_form_builder copied to clipboard

autovalidateMode breaks the invalidate method

Open adimshev opened this issue 1 year ago • 0 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues

Package/Plugin version

9.1.0

Platforms

  • [X] Android
  • [X] iOS
  • [ ] Linux
  • [ ] MacOS
  • [X] Web
  • [ ] Windows

Flutter doctor

Flutter doctor
[!] Flutter (Channel stable, 3.10.5, on macOS 13.4.1 22F82 darwin-arm64, locale
    en-RU)
    • Flutter version 3.10.5 on channel stable at
      /Users/dmitry/fvm/versions/3.10.5
    ! Warning: `dart` on your path resolves to
      /opt/homebrew/Cellar/dart/2.18.6/libexec/bin/dart, which is not inside
      your current Flutter SDK checkout at /Users/dmitry/fvm/versions/3.10.5.
      Consider adding /Users/dmitry/fvm/versions/3.10.5/bin to the front of your
      path.
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 796c8ef792 (9 weeks ago), 2023-06-13 15:51:02 -0700
    • Engine revision 45f6e00911
    • Dart version 3.0.5
    • DevTools version 2.23.1
    • If those were intentional, you can disregard the above warnings; however
      it is recommended to use "git" directly to perform update checks and
      upgrades.

[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.1)
    • Android SDK at /Users/dmitry/Library/Android/sdk
    • Platform android-33, build-tools 33.0.1
    • Java binary at: /Applications/Android
      Studio.app/Contents/jbr/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build
      11.0.15+0-b2043.56-8887301)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 14.3)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 14E222b
    • CocoaPods version 1.12.1

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2022.1)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build
      11.0.15+0-b2043.56-8887301)

[✓] VS Code (version 1.81.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.70.0

[✓] Connected device (2 available)
    • macOS (desktop) • macos  • darwin-arm64   • macOS 13.4.1 22F82
      darwin-arm64
    • Chrome (web)    • chrome • web-javascript • Google Chrome 115.0.5790.170

[✓] Network resources
    • All expected network resources are available.

Minimal code example

Code sample
typedef FormErrors = Map<String, String>;

mixin FormHelperMixin<T extends StatefulWidget> on State<T> {
  final formKey = GlobalKey<FormBuilderState>();

  AutovalidateMode autovalidateMode = AutovalidateMode.disabled;

  FormBuilderState? get _form => formKey.currentState;

  FormErrors? _formErrors;
  Map<String, void Function(dynamic)> formHelpers = {};

  Future<FormErrors?> onSubmitForm(Map<String, dynamic> values);

  Future<void> submitForm() async {
    if (autovalidateMode != AutovalidateMode.always) {
      setState(() {
        autovalidateMode = AutovalidateMode.always;
      });
    }

    if (_form?.validate(autoScrollWhenFocusOnInvalid: true) ?? false) {
      final formErrors = await onSubmitForm(_form!.instantValue);

      _handleFormErrors(formErrors);
    }
  }

  void _handleFormErrors(FormErrors? formErrors) {
    if (!mounted || _form == null) {
      return;
    }

    setState(() {
      _formErrors = formErrors;
      formHelpers = {};

      if (formErrors != null) {
        for (final entry in formErrors.entries) {
          final name = entry.key;
          final error = entry.value;
          final field = _form?.fields[name];

          if (field != null && error.isNotEmpty) {
            field.invalidate(error, shouldFocus: false);

            formHelpers[name] = (_) {
              if (_formErrors?[name] != null) {
                _formErrors?.remove(name);
                _form?.fields[name]?.validate();
              }
            };
          }
        }
      }
    });
  }
}

typedef OnSubmit = Future<FormErrors?> Function({
  required String email,
  required String password,
});

enum LoginFields { email, password }

class LoginForm extends StatefulWidget {
  final bool isLoading;
  final OnSubmit? onSubmit;

  const LoginForm({
    super.key,
    this.isLoading = false,
    this.onSubmit,
  });

  @override
  State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> with FormHelperMixin {
  final _email = LoginFields.email.name;
  final _password = LoginFields.password.name;

  @override
  Future<FormErrors?> onSubmitForm(Map<String, dynamic> values) async {
    return widget.onSubmit?.call(
      email: values[_email]?.trim() ?? '',
      password: values[_password]?.trim() ?? '',
    );
  }

  @override
  Widget build(BuildContext context) {
    return FormBuilder(
      key: formKey,
      autovalidateMode: autovalidateMode,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          FormBuilderField<String>(
            name: _email,
            onChanged: formHelpers[_email],
            builder: (field) {
              return OutlinedTextField(
                label: 'E-mail',
                value: field.value,
                errorText: field.errorText,
                onChanged: field.didChange,
              );
            },
          ),
          const SizedBox(height: 20),
          FormBuilderField<String>(
            name: _password,
            onChanged: formHelpers[_password],
            builder: (field) {
              return OutlinedTextField(
                label: 'Password',
                isPassword: true,
                value: field.value,
                errorText: field.errorText,
                onChanged: field.didChange,
              );
            },
          ),
          const SizedBox(height: 28),
          PrimaryButton.title(
            'Enter',
            isLoading: widget.isLoading,
            onPressed: submitForm,
          ),
        ],
      ),
    );
  }
}


Current Behavior

The problem is that after changing autovalidateMode to always or onUserInteraction, the field's invalidate method doesn't work, and previous custom errors disappear.

Expected Behavior

The most common behavior in my opinion.

The form is not automatically validated, the user enters the data, clicks the submit button, the validation becomes autovalidateMode.always, if the form is valid, a request is sent to the backend, if some fields are not correct, the backend returns Map<String, String> with errors and I write them to the fields with formKey.currentState.fields[fireldName].invalidate(error). Now when the user changes the value of the field, the error from the backend should disappear and validation should only take place by local form validators.

Steps To Reproduce

look at code and videos

Aditional information

https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/assets/33316685/1c376642-f1b8-4998-98da-fd02ad4c2513

https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/assets/33316685/60c75006-1a5a-4482-a0c1-c7a809cea447

adimshev avatar Aug 13 '23 11:08 adimshev