diff --git a/lib/src/form_builder_field.dart b/lib/src/form_builder_field.dart index 11824b110..0e896f1c4 100644 --- a/lib/src/form_builder_field.dart +++ b/lib/src/form_builder_field.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; @@ -185,7 +186,7 @@ class FormBuilderFieldState, T> void _informFormForFieldChange() { if (_formBuilderState != null) { - _dirty = true; + _dirty = !_hasSameValue(value, initialValue); if (enabled || readOnly) { _formBuilderState!.setInternalFieldValue(widget.name, value); return; @@ -194,6 +195,19 @@ class FormBuilderFieldState, T> } } + bool _hasSameValue(T? currentValue, T? initialValue) { + if (currentValue is List && initialValue is List) { + return listEquals(currentValue, initialValue); + } + if (currentValue is Set && initialValue is Set) { + return setEquals(currentValue, initialValue); + } + if (currentValue is Map && initialValue is Map) { + return mapEquals(currentValue, initialValue); + } + return currentValue == initialValue; + } + void _touchedHandler() { if (effectiveFocusNode.hasFocus && _touched == false) { setState(() => _touched = true); @@ -224,8 +238,8 @@ class FormBuilderFieldState, T> /// Also reset custom error text if exists, and set [isDirty] to `false`. void reset() { super.reset(); - didChange(initialValue); _dirty = false; + didChange(initialValue); if (_customErrorText != null) { setState(() => _customErrorText = null); } diff --git a/test/src/fields/form_builder_checkbox_group_test.dart b/test/src/fields/form_builder_checkbox_group_test.dart index 2a2d455db..64b370385 100644 --- a/test/src/fields/form_builder_checkbox_group_test.dart +++ b/test/src/fields/form_builder_checkbox_group_test.dart @@ -166,6 +166,36 @@ void main() { expect(checkbox2.value, false); expect(checkbox3.value, true); }); + testWidgets( + 'Should not dirty when didChange receives a list matching initial contents', + (WidgetTester tester) async { + const fieldName = 'cbg1'; + final testWidget = FormBuilderCheckboxGroup( + name: fieldName, + options: const [ + FormBuilderFieldOption(key: ValueKey('1'), value: 1), + FormBuilderFieldOption(key: ValueKey('2'), value: 2), + FormBuilderFieldOption(key: ValueKey('3'), value: 3), + ], + ); + await tester.pumpWidget( + buildTestableFieldWidget( + testWidget, + initialValue: const { + fieldName: [1, 3], + }, + ), + ); + + final state = formKey.currentState?.fields[fieldName]; + expect(state?.isDirty, false); + + formFieldDidChange(fieldName, [1, 3]); + await tester.pumpAndSettle(); + + expect(state?.isDirty, false); + }, + ); testWidgets('When press tab, field will be focused', ( WidgetTester tester, ) async { diff --git a/test/src/form_builder_field_test.dart b/test/src/form_builder_field_test.dart index ef9a31b0a..211e44481 100644 --- a/test/src/form_builder_field_test.dart +++ b/test/src/form_builder_field_test.dart @@ -419,6 +419,27 @@ void main() { expect(textFieldKey.currentState?.isDirty, true); }, ); + testWidgets('Should not dirty when value returns to initial by user', ( + tester, + ) async { + const textFieldName = 'text'; + final textFieldKey = GlobalKey(); + final testWidget = FormBuilderTextField( + name: textFieldName, + key: textFieldKey, + initialValue: 'hi', + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + final widgetFinder = find.byWidget(testWidget); + await tester.enterText(widgetFinder, 'test'); + await tester.pumpAndSettle(); + expect(textFieldKey.currentState?.isDirty, true); + + await tester.enterText(widgetFinder, 'hi'); + await tester.pumpAndSettle(); + expect(textFieldKey.currentState?.isDirty, false); + }); testWidgets('Should not dirty when reset field value', (tester) async { const textFieldName = 'text'; final textFieldKey = GlobalKey(); @@ -481,6 +502,45 @@ void main() { }); }); group('reset -', () { + testWidgets( + 'Should avoid reset recursion when reset is called after dirty state', + (tester) async { + const textFieldName = 'text'; + final textFieldKey = GlobalKey(); + var onChangedCalls = 0; + var onChangedCallsBeforeReset = 0; + bool? isDirtyInResetOnChanged; + final testWidget = FormBuilderTextField( + name: textFieldName, + key: textFieldKey, + initialValue: 'hi', + onChanged: (value) { + onChangedCalls++; + final state = textFieldKey.currentState; + if (value == state?.initialValue) { + isDirtyInResetOnChanged = state?.isDirty; + } + }, + ); + await tester.pumpWidget(buildTestableFieldWidget(testWidget)); + + final widgetFinder = find.byWidget(testWidget); + await tester.enterText(widgetFinder, 'test'); + await tester.pumpAndSettle(); + + expect(textFieldKey.currentState?.isDirty, true); + onChangedCallsBeforeReset = onChangedCalls; + + textFieldKey.currentState?.reset(); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + expect(textFieldKey.currentState?.value, equals('hi')); + expect(textFieldKey.currentState?.isDirty, false); + expect(onChangedCalls, equals(onChangedCallsBeforeReset + 1)); + expect(isDirtyInResetOnChanged, isFalse); + }, + ); testWidgets('Should reset to null when call reset', (tester) async { const textFieldName = 'text'; final textFieldKey = GlobalKey(); diff --git a/test/src/form_builder_test.dart b/test/src/form_builder_test.dart index 4eb79e0e5..2883452c6 100644 --- a/test/src/form_builder_test.dart +++ b/test/src/form_builder_test.dart @@ -444,6 +444,27 @@ void main() { expect(formKey.currentState?.isDirty, true); }); + testWidgets('Should not dirty when value returns to initial by user', ( + tester, + ) async { + const textFieldName = 'text'; + final testWidget = FormBuilderTextField(name: textFieldName); + await tester.pumpWidget( + buildTestableFieldWidget( + testWidget, + initialValue: {textFieldName: 'hi'}, + ), + ); + + final widgetFinder = find.byWidget(testWidget); + await tester.enterText(widgetFinder, 'test'); + await tester.pumpAndSettle(); + expect(formKey.currentState?.isDirty, true); + + await tester.enterText(widgetFinder, 'hi'); + await tester.pumpAndSettle(); + expect(formKey.currentState?.isDirty, false); + }); testWidgets('Should dirty when update field with initial value by method', ( tester, ) async {