Skip to content
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<FormBuilderFields> 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
Expand Down
108 changes: 108 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class _CompleteFormState extends State<CompleteForm> {
onChanged: () {
_formKey.currentState!.save();
debugPrint(_formKey.currentState!.value.toString());
setState(() {});
},
autovalidateMode: AutovalidateMode.disabled,
initialValue: const {
Expand All @@ -74,6 +75,23 @@ class _CompleteFormState extends State<CompleteForm> {
child: Column(
children: <Widget>[
const SizedBox(height: 15),
// StreamBuilder(
// stream: _formKey.currentState?.onChanged,
// builder:
// (context, AsyncSnapshot<FormBuilderFields> 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,
Expand Down Expand Up @@ -359,6 +377,96 @@ class _CompleteFormState extends State<CompleteForm> {
],
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<FormBuilderFields>
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(),
),
),
],
),
),
],
),
),
],
),
),
],
),
),
Expand Down
15 changes: 15 additions & 0 deletions lib/src/form_builder.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:developer';

import 'package:flutter/material.dart';
Expand Down Expand Up @@ -113,6 +114,7 @@ class FormBuilderState extends State<FormBuilder> {
final _transformers = <String, Function>{};
final _instantValue = <String, dynamic>{};
final _savedValue = <String, dynamic>{};
final _onChangedStreamController = StreamController<FormBuilderFields>();

Map<String, dynamic> get instantValue =>
Map<String, dynamic>.unmodifiable(_instantValue.map((key, value) =>
Expand All @@ -128,6 +130,9 @@ class FormBuilderState extends State<FormBuilder> {

FormBuilderFields get fields => _fields;

/// A stream that informs the subscribers when the form changes.
Stream<FormBuilderFields> get onChanged => _onChangedStreamController.stream;

dynamic transformValue<T>(String name, T? v) {
final t = _transformers[name];
return t != null ? t.call(v) : v;
Expand All @@ -149,6 +154,7 @@ class FormBuilderState extends State<FormBuilder> {
setState(() {});
}
widget.onChanged?.call();
_onChangedStreamController.add(fields);
}

bool get isValid =>
Expand All @@ -162,6 +168,7 @@ class FormBuilderState extends State<FormBuilder> {
if (isSetState) {
setState(() {});
}
_onChangedStreamController.add(fields);
}

void registerField(String name, FormBuilderFieldState field) {
Expand Down Expand Up @@ -192,6 +199,7 @@ class FormBuilderState extends State<FormBuilder> {
populateForm: false,
);
}
_onChangedStreamController.add(fields);
}

void unregisterField(String name, FormBuilderFieldState field) {
Expand All @@ -216,6 +224,7 @@ class FormBuilderState extends State<FormBuilder> {
return true;
}());
}
_onChangedStreamController.add(fields);
}

void save() {
Expand Down Expand Up @@ -270,6 +279,12 @@ class FormBuilderState extends State<FormBuilder> {
});
}

@override
void dispose() {
_onChangedStreamController.close();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Form(
Expand Down
56 changes: 56 additions & 0 deletions test/form_builder_stream_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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 --', () {
late GlobalKey<FormBuilderState> formKey;
late FormBuilderTextField emptyTextField;
late FormBuilder form;

setUp(() {
formKey = GlobalKey<FormBuilderState>();
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 formKey.currentState!.onChanged.first;

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');
});
});
});
}