Repository: flutter-form-builder-ecosystem/flutter_form_builder

Issue:
FormBuilderTextField sets field state to '' even though transformer sets return to null
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Package/Plugin version

9.1.1

### Platforms

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

### Flutter doctor

<details>
  <summary>Flutter doctor</summary>

<!-- Paste the output below inside ``` and ``` -->
```
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 3.16.3, on Microsoft Windows [Version 10.0.22621.3007], locale en-US)
[√] Windows Version (Installed version of Windows is version 10 or higher)
[√] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
[√] Chrome - develop for the web
[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.5.3)
[√] Android Studio (version 2021.2)
[√] VS Code (version 1.85.1)
[√] Connected device (3 available)
[√] Network resources

• No issues found!
```

</details>


### Minimal code example

<details>
  <summary>Code sample</summary>

<!-- Paste the output below inside ``` and ``` -->

```dart
FormBuilderTextField(
                  name: 'writeSize',
                  decoration: const InputDecoration(
                    labelText: 'Write Size (bytes)',
                  ),
                  initialValue: writeSize.toString(),
                  validator: FormBuilderValidators.compose(
                    [
                      FormBuilderValidators.integer(),
                      FormBuilderValidators.min(1),
                      FormBuilderValidators.max(128),
                    ],
                  ),
                  valueTransformer: (value) => int.tryParse(value!),
                ),
```

</details>


### Current Behavior

Trying to use FormBuilderTextField to create a field that accepts either an integer or nothing.  The int.tryParse call inside valueTransformer returns null if value is an empty string, however the field state gets set to an empty string rather than null.  This forces me to transform the transform so that I can get to the needed null.

While experimenting I took the int.tryParse out of the equation by transforming to null no matter what, e.g. valueTransformer: (value) => null.  The field state is still changed to ''.

### Expected Behavior

If I am intentionally transforming the value to null, I expect the field state to be null and not an empty string.  Am I missing a necessary property?

### Steps To Reproduce

Incorporate the above field widget sample into a form and try it out.

### Aditional information

I know I can work around this by transforming all of the empty integer fields to null on submit, but this should be unnecessary.  Please let me know if this can be fixed or if I'm missing something.


Discussion: I've dug into this further and it appears that if the field started out as null and was left null, it works as expected on save - I get a null value and not an empty string.  However, if the field is "touched", e.g. I empty a field that has a value or I enter some data and then empty it, the valueTransformer runs and sets the value to null but the currentState.values entry for the field gets set to an empty string at some point between valueTransformer and saveAndValidate.
same issue for me, and it makes my serialiser fails: even if the transformer correctly returns null, it still gets saved as '', from a quick check I think it take the null as a return value for "do not transform" and it applies the current value, I think this is wrong in terms of logic as it doesn't work with the nullable fields
Im having the same issue, also i pointed it out on the discussions:

https://github.com/flutter-form-builder-ecosystem/flutter_form_builder/discussions/1286
I have looked through the code and the issue is indeed with how the valueTransformer is implemented. As i am not too familiar with the library and there seem to be some dead code in regards to the valueTransformer I am reluctant to make a pull request but could do so if noone else has the time to look through this. The problem is in every instance where the transformer is used:

_transformers[key]?.call(value) ?? value))

This works fine as long as the transformer doesn't transform the value to null. If the transformer however transform the value to null the evaluation will use the original value instead

To fix this changing the above code to:

_transformers[key] == null ? value : _transformers[key]!(value)

Here the transformed value will be used even if it transforms the value to null.

Fix patch:
diff --git a/lib/src/form_builder.dart b/lib/src/form_builder.dart
index 28333c5b3..0734e4514 100644
--- a/lib/src/form_builder.dart
+++ b/lib/src/form_builder.dart
@@ -170,14 +170,24 @@ class FormBuilderState extends State<FormBuilder> {
   /// Get all fields of form.
   FormBuilderFields get fields => _fields;
 
-  Map<String, dynamic> get instantValue =>
-      Map<String, dynamic>.unmodifiable(_instantValue.map((key, value) =>
-          MapEntry(key, _transformers[key]?.call(value) ?? value)));
+  Map<String, dynamic> get instantValue => Map<String, dynamic>.unmodifiable(
+        _instantValue.map(
+          (key, value) => MapEntry(
+            key,
+            _transformers[key] == null ? value : _transformers[key]!(value),
+          ),
+        ),
+      );
 
   /// Returns the saved value only
-  Map<String, dynamic> get value =>
-      Map<String, dynamic>.unmodifiable(_savedValue.map((key, value) =>
-          MapEntry(key, _transformers[key]?.call(value) ?? value)));
+  Map<String, dynamic> get value => Map<String, dynamic>.unmodifiable(
+        _savedValue.map(
+          (key, value) => MapEntry(
+            key,
+            _transformers[key] == null ? value : _transformers[key]!(value),
+          ),
+        ),
+      );
 
   dynamic transformValue<T>(String name, T? v) {
     final t = _transformers[name];
diff --git a/lib/src/form_builder_field.dart b/lib/src/form_builder_field.dart
index e73d6f365..043afffa7 100644
--- a/lib/src/form_builder_field.dart
+++ b/lib/src/form_builder_field.dart
@@ -88,7 +88,8 @@ class FormBuilderFieldState<F extends FormBuilderField<T>, T>
       (_formBuilderState?.initialValue ??
           const <String, dynamic>{})[widget.name] as T?;
 
-  dynamic get transformedValue => widget.valueTransformer?.call(value) ?? value;
+  dynamic get transformedValue =>
+      widget.valueTransformer == null ? value : widget.valueTransformer!(value);
 
   @override
   String? get errorText => super.errorText ?? _customErrorText;


Test patch:
diff --git a/test/src/form_builder_test.dart b/test/src/form_builder_test.dart
index 53c0f9052..db9e59e40 100644
--- a/test/src/form_builder_test.dart
+++ b/test/src/form_builder_test.dart
@@ -39,10 +39,14 @@ void main() {
           ),
         );
 
+        // Write an input resulting in a null value after transformation
+        formFieldDidChange(testWidgetName, 'a');
+        expect(formInstantValue(testWidgetName), isNull);
+
         // Write an input and test the transformer
         formFieldDidChange(testWidgetName, '1');
-        expect(int, formInstantValue(testWidgetName).runtimeType);
-        expect(1, formInstantValue(testWidgetName));
+        expect(formInstantValue(testWidgetName).runtimeType, int);
+        expect(formInstantValue(testWidgetName), 1);
 
         // Remove the dynamic field from the widget tree
         final _DynamicFormFieldsState dynamicFieldState =
@@ -54,8 +58,8 @@ void main() {
 
         // With the field unregistered, the form does not have its transformer
         // but it still has its value, now recovered as type String
-        expect(String, formInstantValue(testWidgetName).runtimeType);
-        expect('1', formInstantValue(testWidgetName));
+        expect(formInstantValue(testWidgetName).runtimeType, String);
+        expect(formInstantValue(testWidgetName), '1');
 
         // Show and recreate the field's state
         dynamicFieldState.show = true;
@@ -63,8 +67,8 @@ void main() {
 
         // The transformer is registered again and with the internal value that
         // was kept, it's expected an int of value 1
-        expect(int, formInstantValue(testWidgetName).runtimeType);
-        expect(1, formInstantValue(testWidgetName));
+        expect(formInstantValue(testWidgetName).runtimeType, int);
+        expect(formInstantValue(testWidgetName), 1);
       },
     );
 
@@ -102,8 +106,8 @@ void main() {
 
         // With the field unregistered, the form does not have its transformer,
         // and since the value was cleared, neither its value
-        expect(Null, formInstantValue(testWidgetName).runtimeType);
-        expect(null, formInstantValue(testWidgetName));
+        expect(formInstantValue(testWidgetName).runtimeType, Null);
+        expect(formInstantValue(testWidgetName), isNull);
 
         // Show and recreate the field's state
         dynamicFieldState.show = true;
@@ -112,8 +116,8 @@ void main() {
         // A new input is needed to get another value
         formFieldDidChange(testWidgetName, '2');
         await tester.pump();
-        expect(int, formInstantValue(testWidgetName).runtimeType);
-        expect(2, formInstantValue(testWidgetName));
+        expect(formInstantValue(testWidgetName).runtimeType, int);
+        expect(formInstantValue(testWidgetName), 2);
       },
     );
   });