From 6cc8cc121102d8c62292d6c08e1a085a27921d3b Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Thu, 1 Dec 2022 15:54:07 +0300 Subject: [PATCH 1/9] add test for initial form --- lib/src/form_builder.dart | 3 +++ test/form_builder_stream_test.dart | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 test/form_builder_stream_test.dart diff --git a/lib/src/form_builder.dart b/lib/src/form_builder.dart index 1bc8163953..e5ee016b91 100644 --- a/lib/src/form_builder.dart +++ b/lib/src/form_builder.dart @@ -128,6 +128,9 @@ class FormBuilderState extends State { FormBuilderFields get fields => _fields; + // TODO add docs + Stream get onChanged => throw UnimplementedError(); + dynamic transformValue(String name, T? v) { final t = _transformers[name]; return t != null ? t.call(v) : v; diff --git a/test/form_builder_stream_test.dart b/test/form_builder_stream_test.dart new file mode 100644 index 0000000000..bdbc6518a6 --- /dev/null +++ b/test/form_builder_stream_test.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'form_builder_tester.dart'; + +void main() { + group('onChanged --', () { + testWidgets('initial', (WidgetTester tester) async { + final key = GlobalKey(); + final form = FormBuilder( + key: key, + child: FormBuilderTextField( + key: const Key('text1'), + name: 'text1', + ), + ); + + await tester.pumpWidget(buildTestableFieldWidget(form)); + final nextChange = await key.currentState!.onChanged.first; + + expect(nextChange, contains('text1')); + expect(nextChange['text1']?.value, isNull); + }); + }); +} From f3a129b4f557a278e4b243ac5cbf2c4947a8b723 Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Mon, 5 Dec 2022 15:00:26 +0300 Subject: [PATCH 2/9] add streams --- lib/src/form_builder.dart | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/src/form_builder.dart b/lib/src/form_builder.dart index e5ee016b91..196d3495f2 100644 --- a/lib/src/form_builder.dart +++ b/lib/src/form_builder.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:developer'; import 'package:flutter/material.dart'; @@ -113,6 +114,7 @@ class FormBuilderState extends State { final _transformers = {}; final _instantValue = {}; final _savedValue = {}; + final _onChangedStreamController = StreamController(); Map get instantValue => Map.unmodifiable(_instantValue.map((key, value) => @@ -129,7 +131,7 @@ class FormBuilderState extends State { FormBuilderFields get fields => _fields; // TODO add docs - Stream get onChanged => throw UnimplementedError(); + Stream get onChanged => _onChangedStreamController.stream; dynamic transformValue(String name, T? v) { final t = _transformers[name]; @@ -152,6 +154,7 @@ class FormBuilderState extends State { setState(() {}); } widget.onChanged?.call(); + _onChangedStreamController.add(fields); } bool get isValid => @@ -165,6 +168,7 @@ class FormBuilderState extends State { if (isSetState) { setState(() {}); } + _onChangedStreamController.add(fields); } void registerField(String name, FormBuilderFieldState field) { @@ -195,6 +199,7 @@ class FormBuilderState extends State { populateForm: false, ); } + _onChangedStreamController.add(fields); } void unregisterField(String name, FormBuilderFieldState field) { @@ -219,6 +224,7 @@ class FormBuilderState extends State { return true; }()); } + _onChangedStreamController.add(fields); } void save() { @@ -273,6 +279,12 @@ class FormBuilderState extends State { }); } + @override + void dispose() { + _onChangedStreamController.close(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Form( From 5f285240f2ba8600c1d90d843e7d04885461647c Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Mon, 5 Dec 2022 15:00:41 +0300 Subject: [PATCH 3/9] add example --- example/lib/main.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index e127b814ca..54b851185c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -74,6 +74,23 @@ class _CompleteFormState extends State { child: Column( children: [ const SizedBox(height: 15), + StreamBuilder( + stream: _formKey.currentState!.onChanged, + builder: + (context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + final data = snapshot.data; + + return Column( + children: data!.entries + .map((e) => Text('${e.key}: ${e.value.value}')) + .toList(), + ); + } + + return const Text('no data'); + }, + ), FormBuilderDateTimePicker( name: 'date', initialEntryMode: DatePickerEntryMode.calendar, From af2e64e3dd1f01d3bd19a3ba5381c1242b07cfbc Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Mon, 5 Dec 2022 15:29:38 +0300 Subject: [PATCH 4/9] fix null state for FormBuilderState --- example/lib/main.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 54b851185c..e3198c5d5b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -61,6 +61,7 @@ class _CompleteFormState extends State { onChanged: () { _formKey.currentState!.save(); debugPrint(_formKey.currentState!.value.toString()); + setState(() {}); }, autovalidateMode: AutovalidateMode.disabled, initialValue: const { @@ -75,7 +76,7 @@ class _CompleteFormState extends State { children: [ const SizedBox(height: 15), StreamBuilder( - stream: _formKey.currentState!.onChanged, + stream: _formKey.currentState?.onChanged, builder: (context, AsyncSnapshot snapshot) { if (snapshot.hasData) { From 01e5640eaf16cb9063fa22a6415ac68603a997ae Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 6 Dec 2022 01:09:00 +0300 Subject: [PATCH 5/9] stylize stream values on example app --- example/lib/main.dart | 120 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 105 insertions(+), 15 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index e3198c5d5b..cf07e5e2d5 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -75,23 +75,23 @@ class _CompleteFormState extends State { child: Column( children: [ const SizedBox(height: 15), - StreamBuilder( - stream: _formKey.currentState?.onChanged, - builder: - (context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - final data = snapshot.data; + // StreamBuilder( + // stream: _formKey.currentState?.onChanged, + // builder: + // (context, AsyncSnapshot snapshot) { + // if (snapshot.hasData) { + // final data = snapshot.data; - return Column( - children: data!.entries - .map((e) => Text('${e.key}: ${e.value.value}')) - .toList(), - ); - } + // return Column( + // children: data!.entries + // .map((e) => Text('${e.key}: ${e.value.value}')) + // .toList(), + // ); + // } - return const Text('no data'); - }, - ), + // return const Text('no data'); + // }, + // ), FormBuilderDateTimePicker( name: 'date', initialEntryMode: DatePickerEntryMode.calendar, @@ -377,6 +377,96 @@ class _CompleteFormState extends State { ], onChanged: _onChanged, ), + // realtime data stream + Padding( + padding: const EdgeInsets.only(top: 8, bottom: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Realtime Data Stream', + style: Theme.of(context).textTheme.headline6, + ), + StreamBuilder( + stream: _formKey.currentState?.onChanged, + builder: (context, + AsyncSnapshot + snapshot) => + !snapshot.hasData + // if there are no data + ? const Center( + child: Text( + 'No data yet! Change some values.', + ), + ) + // if there are data + : Table( + border: TableBorder.all(), + columnWidths: const { + 0: IntrinsicColumnWidth(flex: 1), + 1: IntrinsicColumnWidth(flex: 2), + }, + children: [ + TableRow( + children: [ + Padding( + padding: + const EdgeInsets.all(8.0), + child: Text( + 'Key', + textAlign: TextAlign.right, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + fontWeight: + FontWeight.bold, + ), + ), + ), + Padding( + padding: + const EdgeInsets.all(8.0), + child: Text( + 'Value', + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + fontWeight: + FontWeight.bold, + ), + ), + ), + ], + ), + ...snapshot.data!.entries.map( + (e) => TableRow( + children: [ + Padding( + padding: + const EdgeInsets.all(8), + child: Text( + e.key, + textAlign: TextAlign.right, + ), + ), + Padding( + padding: + const EdgeInsets.all(8), + child: Text( + e.value.value.toString(), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), ], ), ), From 34dc770d9a93bbd860f7ef535959a3847e7eaccb Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 6 Dec 2022 12:44:19 +0300 Subject: [PATCH 6/9] refactor tests to use xunit-style setup --- test/form_builder_stream_test.dart | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/test/form_builder_stream_test.dart b/test/form_builder_stream_test.dart index bdbc6518a6..6a233494d4 100644 --- a/test/form_builder_stream_test.dart +++ b/test/form_builder_stream_test.dart @@ -6,18 +6,22 @@ import 'form_builder_tester.dart'; void main() { group('onChanged --', () { - testWidgets('initial', (WidgetTester tester) async { - final key = GlobalKey(); - final form = FormBuilder( - key: key, - child: FormBuilderTextField( - key: const Key('text1'), - name: 'text1', - ), + late GlobalKey formKey; + late FormBuilderTextField emptyTextField; + late FormBuilder form; + + setUp(() { + formKey = GlobalKey(); + emptyTextField = FormBuilderTextField( + key: const Key('text1'), + name: 'text1', ); + form = FormBuilder(key: formKey, child: emptyTextField); + }); + testWidgets('initial', (WidgetTester tester) async { await tester.pumpWidget(buildTestableFieldWidget(form)); - final nextChange = await key.currentState!.onChanged.first; + final nextChange = await formKey.currentState!.onChanged.first; expect(nextChange, contains('text1')); expect(nextChange['text1']?.value, isNull); From 4ab032202cea8d82aa25f956e8d98d956292e9b4 Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 6 Dec 2022 15:27:24 +0300 Subject: [PATCH 7/9] add on changed test --- test/form_builder_stream_test.dart | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test/form_builder_stream_test.dart b/test/form_builder_stream_test.dart index 6a233494d4..9058d64e99 100644 --- a/test/form_builder_stream_test.dart +++ b/test/form_builder_stream_test.dart @@ -26,5 +26,31 @@ void main() { expect(nextChange, contains('text1')); expect(nextChange['text1']?.value, isNull); }); + + testWidgets('on changed', (WidgetTester tester) async { + await tester.runAsync(() async { + await tester.pumpWidget(buildTestableFieldWidget(form)); + final widget = find.byWidget(emptyTextField); + + expectLater( + formKey.currentState!.onChanged.map( + (fields) => + fields.entries.map((e) => {e.key: e.value.value}).toList(), + ), + emitsInOrder([ + [ + {'text1': null} + ], + [ + {'text1': 'foo'} + ], + [], // caused by `FormBuilderState.unregisterField` + emitsDone, + ]), + ); + + await tester.enterText(widget, 'foo'); + }); + }); }); } From 302d5c1615fa8d104f28934b3def1f1f0989c45b Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 6 Dec 2022 15:29:42 +0300 Subject: [PATCH 8/9] add docstring --- lib/src/form_builder.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/form_builder.dart b/lib/src/form_builder.dart index 196d3495f2..798f58b790 100644 --- a/lib/src/form_builder.dart +++ b/lib/src/form_builder.dart @@ -130,7 +130,7 @@ class FormBuilderState extends State { FormBuilderFields get fields => _fields; - // TODO add docs + /// A stream that informs the subscribers when the form changes. Stream get onChanged => _onChangedStreamController.stream; dynamic transformValue(String name, T? v) { From 4d3921d28eb0a0450011f03d10df3c934b726af3 Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 6 Dec 2022 15:45:33 +0300 Subject: [PATCH 9/9] add docs to readme --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 8ebc164b4a..6fa1aa25f3 100644 --- a/README.md +++ b/README.md @@ -319,6 +319,27 @@ FormBuilderTextField( ), ``` +#### Stream for Real-time Form Changes + +You can subscribe to the stream on [FormBuilderState.onChanged](https://pub.dev/documentation/flutter_form_builder/latest/flutter_form_builder/FormBuilderState/onChanged.html) in order to react to the changes in real-time. You can use this stream in a `StreamBuilder` widget, an example would be: + +```dart +StreamBuilder( + stream: _formKey.currentState?.onChanged, + builder: (context, AsyncSnapshot snapshot) { + if (!snapshot.hasData) { + // if there are data, render a widget + } else { + // if there are no data, render a widget + } + }, +) +``` + +You can see a further example in the example app. + +You can also use this stream with [Bloc library](https://bloclibrary.dev). + ## Support ### Contribute