form_builder_validators icon indicating copy to clipboard operation
form_builder_validators copied to clipboard

Implement the API modifications

Open ArturAssisComp opened this issue 1 year ago • 12 comments

Connection with issue(s)

Close #128

Connected to #119 #116

Solution description

Screenshots or Videos

To Do

  • [x] Read contributing guide
  • [ ] Check the original issue to confirm it is fully satisfied
  • [ ] Add solution description to help guide reviewers
  • [ ] Add unit test to verify new or fixed behaviour
  • [ ] If apply, add documentation to code properties and package readme

ArturAssisComp avatar Aug 07 '24 01:08 ArturAssisComp

Codecov Report

Attention: Patch coverage is 97.79559% with 11 lines in your changes missing coverage. Please review.

Project coverage is 98.74%. Comparing base (e8da6fc) to head (1834d8e). Report is 96 commits behind head on refactor_2024_11.

Files with missing lines Patch % Lines
lib/src/validators/string_validators.dart 90.14% 7 Missing :warning:
lib/src/validators/collection_validators.dart 96.87% 2 Missing :warning:
lib/src/validators/datetime_validators.dart 95.83% 1 Missing :warning:
lib/src/validators/path_validators.dart 96.00% 1 Missing :warning:
Additional details and impacted files
@@                  Coverage Diff                  @@
##           refactor_2024_11     #129       +/-   ##
=====================================================
+ Coverage             86.44%   98.74%   +12.29%     
=====================================================
  Files                   109      111        +2     
  Lines                  1483     1510       +27     
=====================================================
+ Hits                   1282     1491      +209     
+ Misses                  201       19      -182     
Flag Coverage Δ
unittests 98.74% <97.79%> (+12.29%) :arrow_up:

Flags with carried forward coverage won't be shown. Click here to find out more.

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

codecov[bot] avatar Aug 13 '24 02:08 codecov[bot]

@ArturAssisComp before implement all validators, I think that is better create a documentation on readme to be discuss about. I don't sure about the "duplicate" all validators to add a Elementary behaviour

deandreamatias avatar Aug 20 '24 07:08 deandreamatias

@deandreamatias ok, I will do that. I implemented only the bool validators. I am using the version already implemented to make use of the tests, but today I will implement a prototype of the FormBuilderValidator using the new API to discuss the idea. Thanks for the feedback.

ArturAssisComp avatar Aug 20 '24 08:08 ArturAssisComp

I added a section at the end of the readme for the discussion. But it can also be discussed here. I made a video documentation about what I did too.

https://youtu.be/zgzHehmsFd0?si=AE-zE4ESETPFP9xU

ArturAssisComp avatar Aug 21 '24 04:08 ArturAssisComp

I taken a look on this propouse. My thougths:

    1. Is really hard to catch all cases and logic on the API propouse. Make sense the abstract concept, but I think that the implementation can be more simple for developer experience. Maybe can use FormBuilderValidators.compose to add required and optional validators. Like FormBuilderValidators.compose(required: [validators], optional: [another validators])
  1. The name ValidatorBuilder is really necessary? On Flutter, Builder word usually is used to widgets that have a builder property.
  2. The types validators like IsNumericElementaryValidator, I think like a nice thing to have, is an improvement to reuse code.
  3. Thinks like
 ValidatorBuilder.numeric(
          errorText: 'La edad debe ser numérica.',
          and: <BaseElementaryValidator<num, dynamic>>[
            ValidatorBuilder.max(70),
          ])

could be simplify. A option is use ValidatorBuilder.numeric().and(ValidatorBuilder.max(70)) or even improve andc reate something like ValidatorBuilder.numeric() && ValidatorBuilder.max(70)

deandreamatias avatar Aug 23 '24 04:08 deandreamatias

    1. Is really hard to catch all cases and logic on the API propouse. Make sense the abstract concept, but I think that the implementation can be more simple for developer experience. Maybe can use FormBuilderValidators.compose to add required and optional validators. Like FormBuilderValidators.compose(required: [validators], optional: [another validators])

I will investigate that with a prototype.

  1. The name ValidatorBuilder is really necessary? On Flutter, Builder word usually is used to widgets that have a builder property.

That is right. I forgot about the builder semantics that is specific to Flutter. I will think about another name.

  1. Thinks like
 ValidatorBuilder.numeric(
          errorText: 'La edad debe ser numérica.',
          and: <BaseElementaryValidator<num, dynamic>>[
            ValidatorBuilder.max(70),
          ])

could be simplify. A option is use ValidatorBuilder.numeric().and(ValidatorBuilder.max(70)) or even improve andc reate something like ValidatorBuilder.numeric() && ValidatorBuilder.max(70)

I think that would be a good idea. I will investigate with prototypes too.

ArturAssisComp avatar Aug 23 '24 15:08 ArturAssisComp

Tomorrow I will give a better comment on the new API prototype. But it is already implemented for the examples. Fell free to take a look at the new_api_prototype.dart if you want. Otherwise, wait for my comments. I also did a draft benchmark on a password checker comparing the new api with the old one.

image

ArturAssisComp avatar Aug 30 '24 05:08 ArturAssisComp

Conventions: I will call the API that is being used as "original api", the previous proposal as "initial prototype" and this prototype as "new prototype". About the new prototype:

  • The idea was to simply decouple the required, optional and type validators from the other validators. Those validators are considered intermediate validators, thus they receive as an argument the next validator. There are 'and' and 'or' compositions that make it easier to create complex logical combination of multiple validators.
  • It maintains the functionality of the original and the initial prototype
// All the use cases of the example/lib/main.dart were reproduced using the new prototype. They are in example/lib/app_refactoring_main.dart
// Some examples are
// Original api
FormBuilderValidators.compose(<FormFieldValidator<String>>[
    /// Makes this field required
    FormBuilderValidators.required(),

     /// Ensures the value entered is numeric - with custom error message
     FormBuilderValidators.numeric(
         errorText: 'La edad debe ser numérica.',
     ),

     /// Sets a maximum value of 70
     FormBuilderValidators.max(70),

     /// Include your own custom `FormFieldValidator` function, if you want
     /// Ensures positive values only. We could also have used `FormBuilderValidators.min( 0)` instead
     (String? val) {
          if (val != null) {
              final int? number = int.tryParse(val);
              if (number == null) return null;
              if (number < 0) return 'We cannot have a negative age';
          }
          return null;
     }
]);

// New prototype
/// Makes this field required (req is an alias for isRequired)
req(
    /// Ensures the value entered is numeric - with custom error message
    isNum(
         and([
             /// Sets a maximum value of 70
             max(70),
             /// Include your own custom `FormFieldValidator` function, if you want
             (num value) {
                 if (value < 0) return 'We cannot have a negative age';
                 return null;
             },
         ]),
         isNumMessage: 'La edad debe ser numérica.',
    ),
);
  • It removes redundancy of computations for composition of validators. Most of them recalculate if it is null/empty and check the type. As a direct result, it improves the response time of composed validators.
  • It maintains the simplicity of the original api.
  • It is completely compatible with the original api
/// The previous example but using the compose method from the original api
FormBuilderValidators.compose(<FormFieldValidator<String>>[
    req(isNum(max(70), isNumMessage: 'La edad debe ser numérica.')),
    /// Include your own custom `FormFieldValidator` function, if you want
    /// Ensures positive values only. We could also have used `FormBuilderValidators.min( 0)` instead
    (String? val) {
        if (val != null) {
            final int? number = int.tryParse(val);
            if (number == null) return null;
            if (number < 0) return 'We cannot have a negative age';
        }
        return null;
    }
])
  • It maintains the easiness for creating custom validators like it was for the original api
  • It makes it easier for composing validators using and and or logical operators.
// Composition with and
// Original api
FormBuilderValidators.compose(<FormFieldValidator<String>>[
    FormBuilderValidators.required(),
    FormBuilderValidators.numeric(),
    FormBuilderValidators.min(0),
    FormBuilderValidators.max(120),
]),
// New prototype
isRequired(isNum(and([min(0), max(120)])));

// Composition with or and and: The error message may be improved in the future with composition functions like: prefix, suffix, etc. There is already a composition validator called message that replaces the inner message with another one.
//(optional) Choose a value that is either a num in the set: (-10,5] U {7, 8} U (100, +inf) or an even integer
isOptional(or([
    isNum(or([
        and([gt(-10), ltE(5)]),
        equal(7),
        equal(8),
        gt(100),
    ])),
    isInt(
        (int value) =>value % 2 == 0 ? null : 'The input must be even.',
    ),
]));

ArturAssisComp avatar Aug 31 '24 17:08 ArturAssisComp

I also explored possibilities using operator overloading which would allow things like: req() * isNum() * (gt(10) * lt(15) + gtE(20)), using boolean logic notation as example. But it would become very complex to handle some edge cases and the error message composition, and the compiler would not help too much during the composition. It would be necessary to rely on some runtime check or to maintain the generality of each validator, maintaining the redundancy.

ArturAssisComp avatar Aug 31 '24 19:08 ArturAssisComp

I want to know if the parameter regex in some of the validators is really necessary. There is an example below of a validator that checks if the input has a minimum number of lowercase chars. It already has the default regex: RegExp('[a-z]') which I think is enough. I am not seeing any use case for that parameter.

The parameter is also in other validators. The only one that I can see a use case is the regex validator. If there is a use case, please, clarify for me.

/// {@template has_lowercase_chars_template}
/// [HasLowercaseCharsValidator] extends [TranslatedValidator] to validate if a string
/// contains a specified minimum number of lowercase characters.
///
/// ## Parameters:
///
/// - [atLeast] The minimum number of lowercase characters required.
/// - [regex] The regular expression used to identify lowercase characters.
/// - [errorText] The error message returned if the validation fails.
///
/// {@macro lower_case_template}
/// {@endtemplate}
class HasLowercaseCharsValidator extends TranslatedValidator<String> {
  /// Constructor for the lowercase characters validator.
  HasLowercaseCharsValidator({
    this.atLeast = 1,

    /// {@macro lower_case_template}
    RegExp? regex,

    /// {@macro base_validator_error_text}
    super.errorText,

    /// {@macro base_validator_null_check}
    super.checkNullOrEmpty,
  }){...}

ArturAssisComp avatar Sep 26 '24 11:09 ArturAssisComp

I want to know if the parameter regex in some of the validators is really necessary. There is an example below of a validator that checks if the input has a minimum number of lowercase chars. It already has the default regex: RegExp('[a-z]') which I think is enough. I am not seeing any use case for that parameter.

The parameter is also in other validators. The only one that I can see a use case is the regex validator. If there is a use case, please, clarify for me.

/// {@template has_lowercase_chars_template}
/// [HasLowercaseCharsValidator] extends [TranslatedValidator] to validate if a string
/// contains a specified minimum number of lowercase characters.
///
/// ## Parameters:
///
/// - [atLeast] The minimum number of lowercase characters required.
/// - [regex] The regular expression used to identify lowercase characters.
/// - [errorText] The error message returned if the validation fails.
///
/// {@macro lower_case_template}
/// {@endtemplate}
class HasLowercaseCharsValidator extends TranslatedValidator<String> {
  /// Constructor for the lowercase characters validator.
  HasLowercaseCharsValidator({
    this.atLeast = 1,

    /// {@macro lower_case_template}
    RegExp? regex,

    /// {@macro base_validator_error_text}
    super.errorText,

    /// {@macro base_validator_null_check}
    super.checkNullOrEmpty,
  }){...}

Is necesary in some cases. In some languages, the regex need to be increased. For example in spanish, that need to add ñ on regex (example).

deandreamatias avatar Sep 26 '24 20:09 deandreamatias

There are some considerations about collection validators below:

  1. I moved the containsElement validator from the 'collections' group to a new group that I called 'generic validators', which are validators that handle generic type T input, but are not necessarily collections. The problem is that containsElement does not handle necessarily a collection input from the user. The "collection" part is only the reference list of elements that will be used internally by the validator to check if the user input (which is not necessarily a collection) is in that list. What do you think?
  2. The same with the uniqueValidator. It does not check something about a user input collection necessarily. What do you think?
  3. Other than moving uniqueValidator from collections category to generic type category, I cannot find an use case for this validator. Maybe, a validator like: allowed/notAllowed, which checks if the user input is inside a list of allowed/not allowed elements would be better. What do you think?
  4. I think the range validator can be split into 2 validators: betweenLength and between (that already exists). The former will check if the length of the input collection is between a min and a max. The latter will do a numeric comparison. In my opinion, merging both in the same validator make it more confusing and I do not see a clear use case for that. What do you think?

ArturAssisComp avatar Oct 07 '24 15:10 ArturAssisComp

Static vs dynamic values in validators

I will use the isEqual validator to show some issues that I found, but it is valid for other validators like: isGreaterThan, isNotEqual, etc.

This validator has the following signatures: Current API: static FormFieldValidator<T> equal<T>(Object value, {String? errorText, bool checkNullOrEmpty = true,}) New API: Validator<T> isEqual<T extends Object?>(T? value, { T Function()? dynValue, String Function(String)? equalMsg,}) Both has a parameter value that will be used to compare with the user input.

The issue starts when we want to use a dynamic value as value. I created an example (example/lib/new_api_examples/static_vs_dynamic_value_example_main.dart) to illustrate that situation. I also recorded a video showing the situation:

https://github.com/user-attachments/assets/89f8d04a-82bc-4727-ba87-2e3497917689

Basically, what happens is that inside the FormBuilder from flutter_form_builder, the update for the error message is delayed by 1 frame. I don't know exactly what happens under the hood, but, as you can see in the example, when I increment the value used by the isEqual/equal validator, it updates properly when the text field is outside the FormBuilder, but not when it is inside it. The provisory solution that I found for that was to add a value parameter as a callback. Below is the implementation:

Validator<T> isEqual<T extends Object?>(
  T? value, {
  T Function()? dynValue, // This is the dynamic value
  String Function(String)? equalMsg,
}) {
  assert(
      (value != null && dynValue == null) ||
          (value == null && dynValue != null),
      'Exactly one of "value" or "dynValue" must be provided');
  return (T input) {
    final T? currentValue = value ?? dynValue?.call();
    final String valueString = currentValue!.toString();
    return currentValue == input
        ? null
        : equalMsg?.call(valueString) ??
            FormBuilderLocalizations.current.equalErrorText(valueString);
  };
}

In the example, I called isEqual (static) the one that uses the value parameter (does not work inside FormBuilder) and I called isEqual (dynamic) the one that uses the dynValue parameter (works inside the FormBuilder). Their code is illustrated below:

// The state variable 'referenceValue' is used by the validator isEqual/equal
class _HomePageState extends State<_HomePage> {
  int referenceValue = 0;
  //(...)

  // The static example (does not work well inside FormBuilder)
  FormBuilderTextField(
    name: 'isEqual (static)',
    autovalidateMode: AutovalidateMode.always,
    decoration:
        const InputDecoration(labelText: 'isEqual (static)'),
    validator: isRequired(isInt(isEqual(referenceValue))),
  ),
  //The dynamic example (works well inside FormBuilder)
  FormBuilderTextField(
    name: 'isEqual (dynamic)',
    autovalidateMode: AutovalidateMode.always,
    decoration:
        const InputDecoration(labelText: 'isEqual (dynamic)'),
    validator: isRequired(
        isInt(isEqual(null, dynValue: () => referenceValue))),
  ),
  // The current API (does not work well inside FormBuilder)
  FormBuilderTextField(
    name: 'isEqual (old API)',
    autovalidateMode: AutovalidateMode.always,
    decoration:
        const InputDecoration(labelText: 'isEqual (old API)'),
    validator: FormBuilderValidators.equal(
        referenceValue.toString()),
  ),

Questions

  • What do you think about this situation? Is this a bug or is there any solution to solve this issue?
  • Will it be necessary to have the dynamic parameter for all the validators that depend on external dynamic values?

Complete code for the example

The complete code for the example is below:

import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:form_builder_validators/form_builder_validators.dart';

import '../localization/intl/app_localizations.dart';

final GlobalKey<FormBuilderState> _formKey = GlobalKey<FormBuilderState>();

void main() {
  runApp(const _App());
}

class _App extends StatelessWidget {
  const _App();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Static vs Dynamic - Example',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const _HomePage(),
      supportedLocales: AppLocalizations.supportedLocales,
      localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
        ...GlobalMaterialLocalizations.delegates,
        // Placed in front of `FormBuilderLocalizations.delegate`
        ...AppLocalizations.localizationsDelegates,
        FormBuilderLocalizations.delegate,
      ],
    );
  }
}

class _HomePage extends StatefulWidget {
  const _HomePage();

  @override
  State<_HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<_HomePage> {
  int referenceValue = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Static vs Dynamic - Example')),
      body: Padding(
        padding: const EdgeInsets.all(8),
        child: Column(
          children: <Widget>[
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                IconButton(
                    onPressed: () => setState(() {
                          referenceValue--;
                        }),
                    icon: const Icon(Icons.remove)),
                Text(referenceValue.toString()),
                IconButton(
                    onPressed: () => setState(() {
                          referenceValue++;
                        }),
                    icon: const Icon(Icons.add)),
              ],
            ),
            const SizedBox(height: 8),
            Expanded(
              child: SingleChildScrollView(
                child: Column(
                  children: [
                    const Text('With flutter TextFormField:'),
                    TextFormField(
                      decoration:
                          const InputDecoration(labelText: 'isEqual (static)'),
                      autovalidateMode: AutovalidateMode.always,
                      validator: isRequired(isInt(isEqual(referenceValue))),
                    ),
                    const SizedBox(height: 5),
                    TextFormField(
                      decoration:
                          const InputDecoration(labelText: 'isEqual (dynamic)'),
                      autovalidateMode: AutovalidateMode.always,
                      validator: isRequired(
                          isInt(isEqual(null, dynValue: () => referenceValue))),
                    ),
                    const SizedBox(height: 5),
                    TextFormField(
                      decoration:
                          const InputDecoration(labelText: 'isEqual (old API)'),
                      autovalidateMode: AutovalidateMode.always,
                      validator: FormBuilderValidators.equal(
                          referenceValue.toString()),
                    ),
                    const SizedBox(height: 8),
                    const Text(
                        'With flutter_form_builder outside a FormBuilder:'),
                    FormBuilderTextField(
                      name: 'isEqual (static)',
                      autovalidateMode: AutovalidateMode.always,
                      decoration:
                          const InputDecoration(labelText: 'isEqual (static)'),
                      validator: isRequired(isInt(isEqual(referenceValue))),
                    ),
                    const SizedBox(height: 5),
                    FormBuilderTextField(
                      name: 'isEqual (dynamic)',
                      autovalidateMode: AutovalidateMode.always,
                      decoration:
                          const InputDecoration(labelText: 'isEqual (dynamic)'),
                      validator: isRequired(
                          isInt(isEqual(null, dynValue: () => referenceValue))),
                    ),
                    const SizedBox(height: 5),
                    FormBuilderTextField(
                      name: 'isEqual (old API)',
                      autovalidateMode: AutovalidateMode.always,
                      decoration:
                          const InputDecoration(labelText: 'isEqual (old API)'),
                      validator: FormBuilderValidators.equal(
                          referenceValue.toString()),
                    ),
                    const SizedBox(height: 8),
                    const Text(
                        'With flutter_form_builder inside a FormBuilder:'),
                    FormBuilder(
                      key: _formKey,
                      autovalidateMode: AutovalidateMode.always,
                      child: Column(
                        children: <Widget>[
                          FormBuilderTextField(
                            name: 'isEqual (static)',
                            decoration: const InputDecoration(
                                labelText: 'isEqual (static)'),
                            validator:
                                isRequired(isInt(isEqual(referenceValue))),
                          ),
                          const SizedBox(height: 5),
                          FormBuilderTextField(
                            name: 'isEqual (dynamic)',
                            decoration: const InputDecoration(
                                labelText: 'isEqual (dynamic)'),
                            validator: isRequired(isInt(
                                isEqual(null, dynValue: () => referenceValue))),
                          ),
                          const SizedBox(height: 5),
                          FormBuilderTextField(
                            name: 'isEqual (old API)',
                            decoration: const InputDecoration(
                                labelText: 'isEqual (old API)'),
                            validator: FormBuilderValidators.equal(
                                referenceValue.toString()),
                          ),
                        ],
                      ),
                    )
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

ArturAssisComp avatar Oct 10 '24 17:10 ArturAssisComp

There are some considerations about collection validators below:

1. I moved the `containsElement` validator from the 'collections' group to a new group that I called 'generic validators', which are validators that handle generic type T input, but are not necessarily collections. The problem is that `containsElement` does not handle necessarily a collection input from the user. The "collection" part is only the reference list of elements that will be used internally by the validator to check if the user input (which is not necessarily a collection) is in that list. What do you think?

2. The same with the `uniqueValidator`. It does not check something about a user input collection necessarily. What do you think?

3. Other than moving uniqueValidator from collections category to generic type category, I cannot find an use case for this validator. Maybe, a validator like: allowed/notAllowed, which checks if the user input is inside a list of allowed/not allowed elements would be better. What do you think?

4. I think the range validator can be split into 2 validators: betweenLength and between (that already exists). The former will check if the length of the input collection is between a min and a max. The latter will do a numeric comparison. In my opinion, merging both in the same validator make it more confusing and I do not see a clear use case for that. What do you think?
  1. I think that this is a minor change and don't make a lot difference on DX.
  2. The same
  3. Maybe remove this validator
  4. OK. Make sense create and add to collection group

deandreamatias avatar Oct 26 '24 09:10 deandreamatias

Static vs dynamic values in validators

I will use the isEqual validator to show some issues that I found, but it is valid for other validators like: isGreaterThan, isNotEqual, etc.

This validator has the following signatures: Current API: static FormFieldValidator<T> equal<T>(Object value, {String? errorText, bool checkNullOrEmpty = true,}) New API: Validator<T> isEqual<T extends Object?>(T? value, { T Function()? dynValue, String Function(String)? equalMsg,}) Both has a parameter value that will be used to compare with the user input.

The issue starts when we want to use a dynamic value as value. I created an example (example/lib/new_api_examples/static_vs_dynamic_value_example_main.dart) to illustrate that situation. I also recorded a video showing the situation: demo.mp4

Basically, what happens is that inside the FormBuilder from flutter_form_builder, the update for the error message is delayed by 1 frame. I don't know exactly what happens under the hood, but, as you can see in the example, when I increment the value used by the isEqual/equal validator, it updates properly when the text field is outside the FormBuilder, but not when it is inside it. The provisory solution that I found for that was to add a value parameter as a callback. Below is the implementation:

Validator<T> isEqual<T extends Object?>(
  T? value, {
  T Function()? dynValue, // This is the dynamic value
  String Function(String)? equalMsg,
}) {
  assert(
      (value != null && dynValue == null) ||
          (value == null && dynValue != null),
      'Exactly one of "value" or "dynValue" must be provided');
  return (T input) {
    final T? currentValue = value ?? dynValue?.call();
    final String valueString = currentValue!.toString();
    return currentValue == input
        ? null
        : equalMsg?.call(valueString) ??
            FormBuilderLocalizations.current.equalErrorText(valueString);
  };
}

In the example, I called isEqual (static) the one that uses the value parameter (does not work inside FormBuilder) and I called isEqual (dynamic) the one that uses the dynValue parameter (works inside the FormBuilder). Their code is illustrated below:

// The state variable 'referenceValue' is used by the validator isEqual/equal
class _HomePageState extends State<_HomePage> {
  int referenceValue = 0;
  //(...)

  // The static example (does not work well inside FormBuilder)
  FormBuilderTextField(
    name: 'isEqual (static)',
    autovalidateMode: AutovalidateMode.always,
    decoration:
        const InputDecoration(labelText: 'isEqual (static)'),
    validator: isRequired(isInt(isEqual(referenceValue))),
  ),
  //The dynamic example (works well inside FormBuilder)
  FormBuilderTextField(
    name: 'isEqual (dynamic)',
    autovalidateMode: AutovalidateMode.always,
    decoration:
        const InputDecoration(labelText: 'isEqual (dynamic)'),
    validator: isRequired(
        isInt(isEqual(null, dynValue: () => referenceValue))),
  ),
  // The current API (does not work well inside FormBuilder)
  FormBuilderTextField(
    name: 'isEqual (old API)',
    autovalidateMode: AutovalidateMode.always,
    decoration:
        const InputDecoration(labelText: 'isEqual (old API)'),
    validator: FormBuilderValidators.equal(
        referenceValue.toString()),
  ),

Questions

* What do you think about this situation? Is this a bug or is there any solution to solve this issue?

* Will it be necessary to have the dynamic parameter for all the validators that depend on external dynamic values?

Complete code for the example

The complete code for the example is below:

import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:form_builder_validators/form_builder_validators.dart';

import '../localization/intl/app_localizations.dart';

final GlobalKey<FormBuilderState> _formKey = GlobalKey<FormBuilderState>();

void main() {
  runApp(const _App());
}

class _App extends StatelessWidget {
  const _App();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Static vs Dynamic - Example',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const _HomePage(),
      supportedLocales: AppLocalizations.supportedLocales,
      localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
        ...GlobalMaterialLocalizations.delegates,
        // Placed in front of `FormBuilderLocalizations.delegate`
        ...AppLocalizations.localizationsDelegates,
        FormBuilderLocalizations.delegate,
      ],
    );
  }
}

class _HomePage extends StatefulWidget {
  const _HomePage();

  @override
  State<_HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<_HomePage> {
  int referenceValue = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Static vs Dynamic - Example')),
      body: Padding(
        padding: const EdgeInsets.all(8),
        child: Column(
          children: <Widget>[
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                IconButton(
                    onPressed: () => setState(() {
                          referenceValue--;
                        }),
                    icon: const Icon(Icons.remove)),
                Text(referenceValue.toString()),
                IconButton(
                    onPressed: () => setState(() {
                          referenceValue++;
                        }),
                    icon: const Icon(Icons.add)),
              ],
            ),
            const SizedBox(height: 8),
            Expanded(
              child: SingleChildScrollView(
                child: Column(
                  children: [
                    const Text('With flutter TextFormField:'),
                    TextFormField(
                      decoration:
                          const InputDecoration(labelText: 'isEqual (static)'),
                      autovalidateMode: AutovalidateMode.always,
                      validator: isRequired(isInt(isEqual(referenceValue))),
                    ),
                    const SizedBox(height: 5),
                    TextFormField(
                      decoration:
                          const InputDecoration(labelText: 'isEqual (dynamic)'),
                      autovalidateMode: AutovalidateMode.always,
                      validator: isRequired(
                          isInt(isEqual(null, dynValue: () => referenceValue))),
                    ),
                    const SizedBox(height: 5),
                    TextFormField(
                      decoration:
                          const InputDecoration(labelText: 'isEqual (old API)'),
                      autovalidateMode: AutovalidateMode.always,
                      validator: FormBuilderValidators.equal(
                          referenceValue.toString()),
                    ),
                    const SizedBox(height: 8),
                    const Text(
                        'With flutter_form_builder outside a FormBuilder:'),
                    FormBuilderTextField(
                      name: 'isEqual (static)',
                      autovalidateMode: AutovalidateMode.always,
                      decoration:
                          const InputDecoration(labelText: 'isEqual (static)'),
                      validator: isRequired(isInt(isEqual(referenceValue))),
                    ),
                    const SizedBox(height: 5),
                    FormBuilderTextField(
                      name: 'isEqual (dynamic)',
                      autovalidateMode: AutovalidateMode.always,
                      decoration:
                          const InputDecoration(labelText: 'isEqual (dynamic)'),
                      validator: isRequired(
                          isInt(isEqual(null, dynValue: () => referenceValue))),
                    ),
                    const SizedBox(height: 5),
                    FormBuilderTextField(
                      name: 'isEqual (old API)',
                      autovalidateMode: AutovalidateMode.always,
                      decoration:
                          const InputDecoration(labelText: 'isEqual (old API)'),
                      validator: FormBuilderValidators.equal(
                          referenceValue.toString()),
                    ),
                    const SizedBox(height: 8),
                    const Text(
                        'With flutter_form_builder inside a FormBuilder:'),
                    FormBuilder(
                      key: _formKey,
                      autovalidateMode: AutovalidateMode.always,
                      child: Column(
                        children: <Widget>[
                          FormBuilderTextField(
                            name: 'isEqual (static)',
                            decoration: const InputDecoration(
                                labelText: 'isEqual (static)'),
                            validator:
                                isRequired(isInt(isEqual(referenceValue))),
                          ),
                          const SizedBox(height: 5),
                          FormBuilderTextField(
                            name: 'isEqual (dynamic)',
                            decoration: const InputDecoration(
                                labelText: 'isEqual (dynamic)'),
                            validator: isRequired(isInt(
                                isEqual(null, dynValue: () => referenceValue))),
                          ),
                          const SizedBox(height: 5),
                          FormBuilderTextField(
                            name: 'isEqual (old API)',
                            decoration: const InputDecoration(
                                labelText: 'isEqual (old API)'),
                            validator: FormBuilderValidators.equal(
                                referenceValue.toString()),
                          ),
                        ],
                      ),
                    )
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

I think that has a lot probability that the issue is inside FormBuilder. I will search some issues there to link here

deandreamatias avatar Oct 26 '24 09:10 deandreamatias

So, I think that this is related with value on autoValidateMode: AutovalidateMode.always. Maybe can try remove the AutovalidateMode.always and validate with _formKey.currentState?.saveAndValidate(). This should work if the problem is the autoValidateMode

I would continue with refactor and use the new approach isEqual (static), as it does not work within FormBuilder is the current behaviour so doesn't change anything

deandreamatias avatar Oct 26 '24 09:10 deandreamatias

Ok, I will continue implementing the other validators that suffer from the same problem in the FormBuilder without using the dynamic value parameter. Given that it is probably a bug in FormBuilder. I will try to test the autoValidateMode approach too.

So, I think that this is related with value on autoValidateMode: AutovalidateMode.always. Maybe can try remove the AutovalidateMode.always and validate with _formKey.currentState?.saveAndValidate(). This should work if the problem is the autoValidateMode

I would continue with refactor and use the new approach isEqual (static), as it does not work within FormBuilder is the current behaviour so doesn't change anything

By the way, I cannot answer directly on your answers like we were doing in the previous threads. I don't know why.

ArturAssisComp avatar Oct 30 '24 01:10 ArturAssisComp

I think that the changes that are being made reached a huge size. Can we push them to a branch to finish this PR and then open other PRs to continue the refactoring? This would make it easier for the review. I already implemented the main validators. OBS.: I should have asked that before, but I underrated the size of the changes.

ArturAssisComp avatar Nov 27 '24 01:11 ArturAssisComp

@ArturAssisComp changed to a branch refactor_2024_11 when MR will ready for review, remove the draft state

deandreamatias avatar Nov 27 '24 16:11 deandreamatias

I started to expose the new api functions. I noticed that 2 ways are being practiced by the current api:

  1. exposing them by using the aggregator class FormBuilderValidators. This avoids the identifier collision but adds the verbosity. Example: FormBuilderValidators.hasNumericChars
  2. exposing directly the name of each validator class. This also does not avoid the verbosity because we need to use the suffix: "Validator.validate" Example: HasNumericCharsValidator.validate

I think of some possible options: OP1. keep exposing the aggregator (I changed its name to "Validators" only to avoid collision with the current api, because some validators have the same name but do not have the same signature) and also expose the validators individually. Given that some of them may generate collisiion (and or or, for instance), the user would have the possibility to add the namespace himself. Example: Import 'package...' as val or import 'package' as validators or import 'package' as v. OP2. Expose only the aggregator. OP3. (I do not find this options interesting) Expose only the functions

What do you think, @deandreamatias ?

ArturAssisComp avatar Dec 13 '24 01:12 ArturAssisComp

@ArturAssisComp I think OP2 is better. Thinking in the future, there will be only one single API so we will avoid the collision

deandreamatias avatar Dec 18 '24 14:12 deandreamatias

Ok, I will keep with op2.

ArturAssisComp avatar Dec 19 '24 21:12 ArturAssisComp

Flutter Team add a validateGranually on version 3.22 https://github.com/flutter/flutter/pull/135578

I don't know if this can help in some way on this new API

deandreamatias avatar Jan 05 '25 10:01 deandreamatias

I added some examples using this new feature (validateGranularly).

ArturAssisComp avatar Jan 07 '25 00:01 ArturAssisComp

There are some things remaining:

  • Translate the new localized messages
  • Add more examples
  • Add some validators (email, uuid, etc) and their tests I will do them if the current changes are approved.

ArturAssisComp avatar Jan 07 '25 00:01 ArturAssisComp