diff --git a/example/.metadata b/example/.metadata index f5dde3d5a2..958d4cc466 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled. version: - revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + revision: d9111f64021372856901a1fd5bfbc386cade3318 channel: stable project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 - base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 - - platform: linux - create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 - base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + create_revision: d9111f64021372856901a1fd5bfbc386cade3318 + base_revision: d9111f64021372856901a1fd5bfbc386cade3318 + - platform: macos + create_revision: d9111f64021372856901a1fd5bfbc386cade3318 + base_revision: d9111f64021372856901a1fd5bfbc386cade3318 # User provided section diff --git a/example/lib/home_page.dart b/example/lib/home_page.dart index b09dee70ba..43318780d0 100644 --- a/example/lib/home_page.dart +++ b/example/lib/home_page.dart @@ -1,3 +1,5 @@ +import 'package:example/sources/nested_form.dart'; + import 'sources/custom_fields.dart'; import 'package:example/sources/signup_form.dart'; import 'package:flutter/cupertino.dart'; @@ -15,6 +17,24 @@ class HomePage extends StatelessWidget { appBar: AppBar(title: const Text('Flutter Form Builder')), body: ListView( children: [ + ListTile( + title: const Text('Nested Form'), + trailing: const Icon(CupertinoIcons.right_chevron), + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) { + return const CodePage( + title: 'Nested Form', + sourceFilePath: 'lib/sources/nested_form.dart', + child: NestedForm(), + ); + }, + ), + ); + }, + ), + const Divider(), ListTile( title: const Text('Complete Form'), trailing: const Icon(CupertinoIcons.right_chevron), diff --git a/example/lib/main.dart b/example/lib/main.dart index e127b814ca..4563bf8ced 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,6 +4,8 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:intl/intl.dart'; +import 'home_page.dart'; + void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { @@ -11,16 +13,17 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( + return const MaterialApp( title: 'Flutter FormBuilder Demo', debugShowCheckedModeBanner: false, - localizationsDelegates: const [ + localizationsDelegates: [ FormBuilderLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ], - supportedLocales: FormBuilderLocalizations.delegate.supportedLocales, - home: const CompleteForm(), + // supportedLocales: FormBuilderLocalizations.delegate.supportedLocales, + home: HomePage(), + // home: const CompleteForm(), ); } } diff --git a/example/lib/sources/nested_form.dart b/example/lib/sources/nested_form.dart new file mode 100644 index 0000000000..b8bcc2c53a --- /dev/null +++ b/example/lib/sources/nested_form.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; + +class NestedForm extends StatefulWidget { + const NestedForm({ + Key? key, + }) : super(key: key); + + @override + State createState() => _NestedFormState(); +} + +class _NestedFormState extends State { + GlobalKey formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + FormBuilder( + key: formKey, + initialValue: const { + 'foo': 'foo value0', + 'inner': { + 'bar': 'bar value1', + 'baz': 'baz value2', + 'inner2': { + 'test': 'test value', + }, + } + }, + child: Column( + children: [ + FormBuilderTextField(name: 'foo'), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: NestedFormBuilder( + name: 'inner', + child: Column( + children: [ + FormBuilderTextField(name: 'bar'), + FormBuilderTextField(name: 'baz'), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: NestedFormBuilder( + name: 'inner2', + child: FormBuilderTextField(name: 'test'), + ), + ) + ], + )), + ) + ], + )), + ElevatedButton( + onPressed: () { + final st = formKey.currentState!; + st.saveAndValidate(); + final value = st.value; + debugPrint("debug form value $value"); + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(value.toString()))); + }, + child: const Text("Show Value")) + ], + ); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 8cf19da6b2..7903b5dc73 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,49 +5,49 @@ packages: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.9.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.16.0" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" flutter: @@ -66,7 +66,7 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" flutter_localizations: @@ -83,49 +83,49 @@ packages: dependency: "direct main" description: name: form_builder_validators - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "8.1.1" + version: "8.4.0" intl: dependency: "direct main" description: name: intl - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.0" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.0" + version: "2.0.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.5" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.2" sky_engine: @@ -137,49 +137,49 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.9.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.12" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" sdks: diff --git a/lib/flutter_form_builder.dart b/lib/flutter_form_builder.dart index e3bf533301..5e5efd0025 100644 --- a/lib/flutter_form_builder.dart +++ b/lib/flutter_form_builder.dart @@ -1,6 +1,7 @@ library flutter_form_builder; export 'src/form_builder.dart'; +export 'src/nested_form_builder.dart'; export 'src/form_builder_field.dart'; export 'src/form_builder_field_option.dart'; export 'src/fields/form_builder_checkbox.dart'; diff --git a/lib/src/nested_form_builder.dart b/lib/src/nested_form_builder.dart new file mode 100644 index 0000000000..ce571b5d11 --- /dev/null +++ b/lib/src/nested_form_builder.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; + +class NestedFormBuilder extends StatelessWidget { + /// Parent Form Key + final GlobalKey? parentFormKey; + + /// Nested Form Key + final GlobalKey formKey; + + /// Called just before field value is saved. Used to massage data just before + /// committing the value. + /// See [FormBuilderField.valueTransformer] + final ValueTransformer?>? valueTransformer; + + /// Used to reference the field within the form, or to reference form data + /// after the form is submitted. + final String name; + + /// Called when one of the form fields changes. + /// + /// In addition to this callback being invoked, all the form fields themselves + /// will rebuild. + final VoidCallback? onChanged; + + /// Enables the form to veto attempts by the user to dismiss the [ModalRoute] + /// that contains the form. + /// + /// If the callback returns a Future that resolves to false, the form's route + /// will not be popped. + /// + /// See also: + /// + /// * [WillPopScope], another widget that provides a way to intercept the + /// back button. + final WillPopCallback? onWillPop; + + /// The widget below this widget in the tree. + /// + /// This is the root of the widget hierarchy that contains this form. + /// + /// {@macro flutter.widgets.child} + final Widget child; + + /// Used to enable/disable form fields auto validation and update their error + /// text. + /// + /// {@macro flutter.widgets.form.autovalidateMode} + final AutovalidateMode? autovalidateMode; + + /// An optional Map of field initialValues. Keys correspond to the field's + /// name and value to the initialValue of the field. + /// + /// The initialValues set here will be ignored if the field has a local + /// initialValue set. + final Map? initialValue; + + /// Whether the form should ignore submitting values from fields where + /// `enabled` is `false`. + /// This behavior is common in HTML forms where _readonly_ values are not + /// submitted when the form is submitted. + /// + /// When `true`, the final form value will not contain disabled fields. + /// Default is `false`. + final bool skipDisabled; + + /// Whether the form is able to receive user input. + /// + /// Defaults to true. + /// + /// When `false` all the form fields will be disabled - won't accept input - + /// and their enabled state will be ignored. + final bool enabled; + + /// Whether the form should auto focus on the first field that fails validation. + final bool autoFocusOnValidationFailure; + + /// Whether to clear the internal value of a field when it is unregistered. + /// + /// Defaults to `false`. + /// + /// When set to `true`, the form builder will not keep the internal values + /// from disposed [FormBuilderField]s. This is useful for dynamic forms where + /// fields are registered and unregistered due to state change. + /// + /// This setting will have no effect when registering a field with the same + /// name as the unregistered one. + final bool clearValueOnUnregister; + + NestedFormBuilder({ + Key? key, + required this.name, + required this.child, + this.onChanged, + this.onWillPop, + this.autovalidateMode, + this.initialValue, + this.skipDisabled = false, + this.enabled = true, + this.autoFocusOnValidationFailure = false, + this.clearValueOnUnregister = false, + GlobalKey? formKey, + this.parentFormKey, + this.valueTransformer, + }) : formKey = formKey ?? GlobalKey(), + super(key: key); + + @override + Widget build(BuildContext context) => FormBuilderField>( + name: name, + initialValue: + parentFormKey?.currentState?.initialValue[name] ?? initialValue, + valueTransformer: valueTransformer ?? (_) => _, + onReset: () => formKey.currentState?.reset(), + builder: (field) => FormBuilder( + key: formKey, + initialValue: field.value ?? {}, + onChanged: () { + final st = formKey.currentState; + if (st == null) return; + st.save(); + field.didChange(valueTransformer?.call(st.value) ?? st.value); + }, + autovalidateMode: autovalidateMode, + onWillPop: onWillPop, + skipDisabled: skipDisabled, + enabled: enabled, + autoFocusOnValidationFailure: autoFocusOnValidationFailure, + clearValueOnUnregister: clearValueOnUnregister, + child: child, + ), + ); +} diff --git a/pubspec.lock b/pubspec.lock index 6ba1a68aaf..c5bfc2c87e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,42 +5,42 @@ packages: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.9.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" collection: dependency: "direct main" description: name: collection - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.16.0" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" flutter: @@ -52,7 +52,7 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" flutter_test: @@ -64,42 +64,42 @@ packages: dependency: "direct main" description: name: intl - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.17.0" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.0" + version: "2.0.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.5" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.8.2" sky_engine: @@ -111,49 +111,49 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.9.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.12" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" sdks: