Skip to content

Conversation

@hasanmhallak
Copy link
Contributor

Introducing the forceErrorText property to both FormField and TextFormField. With this addition, we gain the capability to trigger an error state and provide an error message without invoking the validator method.

While the idea of making the Validator method work asynchronously may be appealing, it could introduce significant complexity to our current form field implementation. Additionally, this approach might not be suitable for all developers, as discussed by @justinmc in this comment.

This PR try to address this issue by adding forceErrorText property allowing us to force the error to the FormField or TextFormField at our own base making it possible to preform some async operations without the need for any hacks while keep the ability to check for errors if we call formKey.currentState!.validate().

Here is an example:

Code Example
import 'package:flutter/material.dart';

void main() {
  runApp(
    const MaterialApp(home: MyHomePage()),
  );
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({
    super.key,
  });

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final key = GlobalKey<FormState>();
  String? forcedErrorText;

  Future<void> handleValidation() async {
    // simulate some async work..
    await Future.delayed(const Duration(seconds: 3));

    setState(() {
      forcedErrorText = 'this username is not available.';
    });

    // wait for build to run and then check.
    //
    // this is not required though, as the error would already be showing.
    WidgetsBinding.instance.addPostFrameCallback((_) {
      print(key.currentState!.validate());
    });
  }

  @override
  Widget build(BuildContext context) {
    print('build');
    return Scaffold(
      floatingActionButton: FloatingActionButton(onPressed: handleValidation),
      body: Form(
        key: key,
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 30),
                child: TextFormField(
                  forceErrorText: forcedErrorText,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Related to #9688 & #56414.

Happy to hear your thoughts on this.

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

@github-actions github-actions bot added a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. labels Aug 19, 2023
Fix unresolved doc reference errors
@justinmc
Copy link
Contributor

justinmc commented Aug 21, 2023

I appreciate you opening a PR to start a discussion on this. I agree that this kind of validation logic could be easier in Flutter, but I'm skeptical about adding this new parameter unless we're sure it's going to be worth it.

How is this better than the approach that I mentioned in the comment that you linked above? I've update the dartpad here so that it runs on modern Flutter versions: https://dartpad.dev/?id=b8c96286ae3d35c47c202123197ff6a0

@hasanmhallak
Copy link
Contributor Author

Hey @justinmc, Thank you for taking the time to replay and update the example.

I think this is a better approach for two reasons actually. The first one is we don't have to set autovalidateMode to always. The second is we keep the validator function more isolated or use any pre-made library for validation that integrated well the Validator signature, and not go through the hassle of saving the old value and check it to make the api call, and then use setState with autovalidateMode set true to make the validation runs on rebuild.

Your approach is nice, but that's only because we're using in it in a statefull widget and relying on setState, and assuming the error would only happen form isUserNameAvailble endpoint. I remember when you first commented this approach, and I've been using it ever since, but it will become very hard when the error could be an predictable and using any state management solution, especially the point where we store the previous value to compare it against the new one and forcing a rebuild with setState.

With the approach that I'm suggesting, we can simply let our service (remote service, repo, controller etc..) handle the possible error that we would have, wrap the text field with say a blocBuilder to force the error state to a specific field when it happens.

This is important because we are not limited to just checking for a username, sometimes the api can still return a validation error even if the user pass the validation on mobile side. For example this is something happened to me recently, a long form that the user must submit it with his credit card number, but for some reasons there is a problem with that particular card. so it would be nice to be able to force the error to that particular field with an error message coming from the backend.

Please note that this kind of approach is not new to flutter. The InputDecoration class has a property called errorText to force the error, but the problem is, it only forces it on the field decoration and the FormField, so calling validate() will still return true even if the decoration is showing an error.

here is a snippet:

...
 TextFormField(
        decoration: InputDecoration(
             errorText: 'Forcing error to decoration',
         ),
...

@justinmc
Copy link
Contributor

Would you be able to post a full code example of the credit card case you mentioned, and/or the case of using bloc?

I'm actually on board with you that being able to simply provide error text to display unconditionally is a more simple and declarative pattern than what we have now. However, I want to make sure we don't bloat the API into having multiple ways of doing things. If what we really need is a bigger refactor or a breaking change then maybe we should take the time to plan that out instead.

I'm interested to look at those other code examples if you don't mind writing them, though!

@hasanmhallak
Copy link
Contributor Author

I'm not sure if I can provide the full code of the credit card case, but I can write some example to demonstrate more the cases that I have mention. Will be working on them and update the thread here.

@hasanmhallak
Copy link
Contributor Author

hey @justinmc, I've just created a small example, I tried not to use any library so you can able to check it on dart-pad (idk if dart-pad support using external libraries).

here's a link for the example on dart pad.

Now here, you may find it a bit confusing. But Riverpod/Bloc has a very nice way of filtering the state changes. But I couldn't simulate it without spending too much time.

 ValueListenableBuilder(
            valueListenable: service,
            builder: (context, value, child) {

              // this is the normal field without forcing any error.
             // it is here just to clean up the code a bit.
              final normalChild = _Field(
              controller: creditCardController,
              label: 'Credit Card',
              validator: Validators.creditCard,
            );

            return value.maybeWhen(
              orElse: () => normalChild,
              error: (type, message) {
                return switch (type) {
                 // filtring the error state related to cred card field to get
                // the error message if any.
                  ErrorType.creditCard => _Field(
                      controller: creditCardController,
                      label: 'Credit Card',
                      validator: Validators.creditCard,
                      // here I used the `errorText` property to simulate adding my suggestion to `FormField`
                      forceErrorText: message,
                    ),
                  _ => normalChild,
                      };
                    },
                  );
                },
              )

you can see in submit method that I saved a lot of boilerplate relating to caching the previous validated data
in order to run the validator again, and didn't be forced to use auto validation mode.

Future<void> submit() async {
    if (!_formKey.currentState!.validate()) return;

    // the approach I suggested won't let me register again when there is an error,
    // as the error would be forced to the [FormField] making the `validate` function return `false`.
    await service.register(
      username: usernameController.text,
      password: passwordController.text,
      creditCard: creditCardController.text,
    );
  }

The basic idea is to register a listener that will only be triggered if there is an error in an exact text field, so it will be rebuilt again with a forced error text.

this way we keep the logic for backed errors handling encapsulated in a (service, controller ..etc) and the ui will only be responsible for listening to any state change.

I think this way will give the developers more freedom on deciding how they would handle errors rather than sticking with one approach that can look like a work around.

Here is the link to the full flutter project on github link

@HansMuller HansMuller requested a review from justinmc August 25, 2023 21:51
@hasanmhallak
Copy link
Contributor Author

hey @justinmc had you got the time to review this?

@Renzo-Olivares Renzo-Olivares self-requested a review November 30, 2023 22:54
@flutter-dashboard
Copy link

This pull request executed golden file tests, but it has not been updated in a while (20+ days). Test results from Gold expire after as many days, so this pull request will need to be updated with a fresh commit in order to get results from Gold.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hasanmhallak Apologies for the long silence on this PR. I've been hoping we could get around to refactoring this instead of incrementally making fixes like this, but I think a refactor is still a ways off. Are you still interested in moving forward with this PR?

@justinmc
Copy link
Contributor

justinmc commented Mar 4, 2024

Somewhat related: #54104

@justinmc justinmc self-requested a review March 7, 2024 23:04
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 20, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 20, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 20, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 21, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 21, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 21, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 22, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 22, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 23, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 23, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 24, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 24, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 25, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 25, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Jun 25, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Aug 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: text input Entering text in a text field or keyboard related problems autosubmit Merge PR when tree becomes green via auto submit App d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels. will affect goldens Changes to golden files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants