form/feature/add_metadata #10
| @ -30,21 +30,22 @@ Form Bloc for Dart & Flutter. | ||||
| ## Features | ||||
| 
 | ||||
| - Form | ||||
|     * FormInput: *atom of a form* | ||||
|     * FormInputValidator: | ||||
|         - Store data | ||||
|         - Validate this data | ||||
|     * FormEntry: *shell of this atom* | ||||
|         - Associate a key to an input | ||||
|         - Configure form attribute (exportation, name...) | ||||
|     * FormData: *collection of entries* | ||||
|         - Contain all entries | ||||
|     * FormInputMetadata: | ||||
|         - Store infos and options about an input. | ||||
|     * FormInput: | ||||
|         - Associaite a key, to a validator and metadata. | ||||
|     * FormData: *collection of inputs* | ||||
|         - Contain all inputs | ||||
|         - Basic set operation | ||||
| 
 | ||||
| - FormDataCubit | ||||
|     * Data management behind a form. | ||||
|         - Use entries to pass a FormData object | ||||
|         - You can use several pre configured FormInput for validation | ||||
|         - You can use updateFormData() to change FormData and validators during runtime (intersection, union, difference or replace) | ||||
|         - You can use several pre configured `FormInputValidator` for validation | ||||
|         - You can use updateFormData() to change `FormData` during runtime (intersection, union, difference or replace) | ||||
| 
 | ||||
| - Consistent | ||||
|     * Every class have same naming convention | ||||
| @ -59,4 +60,120 @@ import 'package:wyatt_form_bloc/wyatt_form_bloc.dart'; | ||||
| 
 | ||||
| ## Usage | ||||
| 
 | ||||
| todo | ||||
| Firstly, you have to create your form inputs. | ||||
| 
 | ||||
| ```dart | ||||
| static List<FormInput> getNormalEntries() { | ||||
|     return const [ | ||||
|         FormInput(formFieldName, Name.pure()), | ||||
|         FormInput(formFieldEmail, Email.pure()), | ||||
|         FormInput(formFieldPhone, Phone.pure()), | ||||
|         FormInput(formFieldList, ListOption<String>.pure(defaultValue: ['checkbox3'])), | ||||
|         FormInput(formFieldRadio, TextString.pure()), | ||||
|         FormInput(formFieldPro, Boolean.pure(), metadata: FormInputMetadata<void>(name: 'business')), | ||||
|         FormInput(formFieldHidden, Boolean.pure(), metadata: FormInputMetadata<void>(export: false)), | ||||
|     ]; | ||||
| } | ||||
| 
 | ||||
| static List<FormInput> getBusinessEntries() { | ||||
|     const entries = [ | ||||
|         FormInput(formFieldSiren, Siren.pure()), | ||||
|         FormInput(formFieldIban, Iban.pure()), | ||||
|     ]; | ||||
|     return getNormalEntries() + entries; | ||||
| } | ||||
| 
 | ||||
| static FormData getNormalFormData() { | ||||
|     return FormData(getNormalEntries()); | ||||
| } | ||||
| 
 | ||||
| static FormData getProFormData() { | ||||
|     return FormData(getBusinessEntries()); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| > Let's create functions to enable dynamic changes in runtime. | ||||
| 
 | ||||
| > When creating metadata with `FormInputMetadata<T>`, `T` is the type of `extra` object. | ||||
| 
 | ||||
| Then a `FormData` is a collection of inputs. You can `validate`, `contains`, `intersection`, `difference`, `union`, or `clone` this data. | ||||
| 
 | ||||
| After that, you have to extends `FormDataCubit`. | ||||
| 
 | ||||
| ```dart | ||||
| class CustomFormCubit extends FormDataCubit { | ||||
|   CustomFormCubit({required FormData inputs}) : super(inputs: inputs); | ||||
| 
 | ||||
|   @override | ||||
|   Future<void> submitForm() { | ||||
|     log(state.data.toMap().toString()); | ||||
|     // Handle all your logic here. | ||||
|     emit(...) | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| > The submited form is in `state.data` ! | ||||
| 
 | ||||
| Don't forget to create and provide it ! | ||||
| 
 | ||||
| ```dart | ||||
| FormDataCubit _formCubit = CustomFormCubit(inputs: getNormalFormData()); | ||||
| 
 | ||||
| return BlocProvider( | ||||
|     create: (context) => _formCubit, | ||||
|     child: const WidgetTree(), | ||||
| ); | ||||
| ``` | ||||
| 
 | ||||
| You can now create the first form input ! | ||||
| 
 | ||||
| ```dart | ||||
| class _EmailInput extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return BlocBuilder<FormDataCubit, FormDataState>( | ||||
|       builder: (context, state) { | ||||
|         return TextField( | ||||
|           onChanged: (email) => context | ||||
|               .read<FormDataCubit>() | ||||
|               .dataChanged(formFieldEmail, Email.dirty(email)), | ||||
|           keyboardType: TextInputType.emailAddress, | ||||
|           decoration: InputDecoration( | ||||
|             labelText: 'email', | ||||
|             helperText: '', | ||||
|             errorText: state.data.isNotValid(formFieldEmail) | ||||
|                 ? 'invalid email' | ||||
|                 : null, | ||||
|           ), | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| Then, you can create the trigger | ||||
| 
 | ||||
| ```dart | ||||
| class _SignUpButton extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return BlocBuilder<FormDataCubit, FormDataState>( | ||||
|       buildWhen: (previous, current) => previous.status != current.status, | ||||
|       builder: (context, state) { | ||||
|         return state.status.isSubmissionInProgress | ||||
|             ? const CircularProgressIndicator() | ||||
|             : ElevatedButton( | ||||
|                 onPressed: state.status.isValidated | ||||
|                     ? () => context.read<FormDataCubit>().submitForm() | ||||
|                     : null, | ||||
|                 child: const Text('SIGN UP'), | ||||
|               ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| > With `state.status` you can access the status of the form to check if there is an error or if it's in submission. | ||||
| @ -1,10 +1,30 @@ | ||||
| # This file tracks properties of this Flutter project. | ||||
| # Used by Flutter tool to assess capabilities and perform upgrades etc. | ||||
| # | ||||
| # This file should be version controlled and should not be manually edited. | ||||
| # This file should be version controlled. | ||||
| 
 | ||||
| version: | ||||
|   revision: 5464c5bac742001448fe4fc0597be939379f88ea | ||||
|   revision: 85684f9300908116a78138ea4c6036c35c9a1236 | ||||
|   channel: stable | ||||
| 
 | ||||
| project_type: app | ||||
| 
 | ||||
| # Tracks metadata for the flutter migrate command | ||||
| migration: | ||||
|   platforms: | ||||
|     - platform: root | ||||
|       create_revision: 85684f9300908116a78138ea4c6036c35c9a1236 | ||||
|       base_revision: 85684f9300908116a78138ea4c6036c35c9a1236 | ||||
|     - platform: web | ||||
|       create_revision: 85684f9300908116a78138ea4c6036c35c9a1236 | ||||
|       base_revision: 85684f9300908116a78138ea4c6036c35c9a1236 | ||||
| 
 | ||||
|   # User provided section | ||||
| 
 | ||||
|   # List of Local paths (relative to this file) that should be | ||||
|   # ignored by the migrate tool. | ||||
|   # | ||||
|   # Files that are not part of the templates will be ignored by default. | ||||
|   unmanaged_files: | ||||
|     - 'lib/main.dart' | ||||
|     - 'ios/Runner.xcodeproj/project.pbxproj' | ||||
|  | ||||
| @ -16,31 +16,37 @@ | ||||
| 
 | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_bloc/flutter_bloc.dart'; | ||||
| import 'package:form_bloc_example/app/metadata.dart'; | ||||
| import 'package:form_bloc_example/constants.dart'; | ||||
| import 'package:form_bloc_example/cubit/custom_form_cubit.dart'; | ||||
| import 'package:form_bloc_example/sign_up/sign_up_page.dart'; | ||||
| import 'package:wyatt_form_bloc/wyatt_form_bloc.dart'; | ||||
| 
 | ||||
| const blue = Metadata(category: Category.perso); | ||||
| const red = Metadata(category: Category.business); | ||||
| 
 | ||||
| class App extends StatelessWidget { | ||||
|   const App({Key? key}) : super(key: key); | ||||
| 
 | ||||
|   static List<FormEntry> getNormalEntries() { | ||||
|   static List<FormInput> getNormalEntries() { | ||||
|     return const [ | ||||
|       FormEntry(formFieldName, Name.pure()), | ||||
|       FormEntry(formFieldEmail, Email.pure()), | ||||
|       FormEntry(formFieldPhone, Phone.pure()), | ||||
|       FormEntry( | ||||
|       FormInput(formFieldName, Name.pure(), metadata: FormInputMetadata<Metadata>(extra: blue)), | ||||
|       FormInput(formFieldEmail, Email.pure(), metadata: FormInputMetadata<Metadata>(extra: blue)), | ||||
|       FormInput(formFieldPhone, Phone.pure(), metadata: FormInputMetadata<Metadata>(extra: red)), | ||||
|       FormInput( | ||||
|           formFieldList, ListOption<String>.pure(defaultValue: ['checkbox3'])), | ||||
|       FormEntry(formFieldRadio, TextString.pure()), | ||||
|       FormEntry(formFieldPro, Boolean.pure(), name: 'business'), | ||||
|       FormEntry(formFieldHidden, Boolean.pure(), export: false), | ||||
|       FormInput(formFieldRadio, TextString.pure()), | ||||
|       FormInput(formFieldPro, Boolean.pure(), | ||||
|           metadata: FormInputMetadata<Metadata>(name: 'business')), | ||||
|       FormInput(formFieldHidden, Boolean.pure(), | ||||
|           metadata: FormInputMetadata<Metadata>(export: false, extra: blue)), | ||||
|     ]; | ||||
|   } | ||||
| 
 | ||||
|   static List<FormEntry> getBusinessEntries() { | ||||
|   static List<FormInput> getBusinessEntries() { | ||||
|     const entries = [ | ||||
|       FormEntry(formFieldSiren, Siren.pure()), | ||||
|       FormEntry(formFieldIban, Iban.pure()), | ||||
|       FormInput(formFieldSiren, Siren.pure()), | ||||
|       FormInput(formFieldIban, Iban.pure()), | ||||
|     ]; | ||||
|     return getNormalEntries() + entries; | ||||
|   } | ||||
| @ -55,7 +61,7 @@ class App extends StatelessWidget { | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     FormDataCubit _formCubit = CustomFormCubit(entries: getNormalFormData()); | ||||
|     FormDataCubit _formCubit = CustomFormCubit(inputs: getNormalFormData()); | ||||
| 
 | ||||
|     return BlocProvider( | ||||
|       create: (context) => _formCubit, | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| // ignore_for_file: public_member_api_docs, sort_constructors_first | ||||
| // Copyright (C) 2022 WYATT GROUP | ||||
| // Please see the AUTHORS file for details. | ||||
| // | ||||
| @ -14,9 +15,20 @@ | ||||
| // You should have received a copy of the GNU General Public License | ||||
| // along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| part of 'custom_form_cubit.dart'; | ||||
| enum Category { | ||||
|   perso, | ||||
|   business | ||||
| } | ||||
| 
 | ||||
| @immutable | ||||
| abstract class CustomFormState {} | ||||
| class Metadata { | ||||
|   final Category category; | ||||
| 
 | ||||
| class CustomFormInitial extends CustomFormState {} | ||||
|   const Metadata({ | ||||
|     required this.category, | ||||
|   }); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return category.toString(); | ||||
|   } | ||||
| } | ||||
| @ -16,18 +16,15 @@ | ||||
| 
 | ||||
| import 'dart:developer'; | ||||
| 
 | ||||
| import 'package:meta/meta.dart'; | ||||
| import 'package:wyatt_form_bloc/wyatt_form_bloc.dart'; | ||||
| 
 | ||||
| part 'custom_form_state.dart'; | ||||
| 
 | ||||
| class CustomFormCubit extends FormDataCubit { | ||||
|   CustomFormCubit({required FormData entries}) : super(entries: entries); | ||||
|   CustomFormCubit({required FormData inputs}) : super(inputs: inputs); | ||||
| 
 | ||||
|   @override | ||||
|   Future<void> submitForm() { | ||||
|     log(state.data.toMap().toString()); | ||||
|      | ||||
| 
 | ||||
|     return Future.value(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -19,35 +19,83 @@ import 'dart:developer'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_bloc/flutter_bloc.dart'; | ||||
| import 'package:form_bloc_example/app/app.dart'; | ||||
| import 'package:form_bloc_example/app/metadata.dart'; | ||||
| import 'package:form_bloc_example/constants.dart'; | ||||
| import 'package:wyatt_form_bloc/wyatt_form_bloc.dart'; | ||||
| 
 | ||||
| class CategoryIndicator extends StatelessWidget { | ||||
|   const CategoryIndicator( | ||||
|       {Key? key, required this.field, required this.builder}) | ||||
|       : super(key: key); | ||||
| 
 | ||||
|   final String field; | ||||
|   final Function(BuildContext context, FormDataState state) builder; | ||||
| 
 | ||||
|   Color computeColor(Metadata meta) { | ||||
|     switch (meta.category) { | ||||
|       case Category.perso: | ||||
|         return Colors.blue; | ||||
|       case Category.business: | ||||
|         return Colors.red; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return BlocBuilder<FormDataCubit, FormDataState>( | ||||
|       builder: ((context, state) { | ||||
|         final meta = state.data.metadataOf(field).extra as Metadata; | ||||
|         final color = computeColor(meta); | ||||
| 
 | ||||
|         return Row( | ||||
|           children: [ | ||||
|             Container( | ||||
|               height: 8, | ||||
|               width: 8, | ||||
|               decoration: BoxDecoration( | ||||
|                 color: color, | ||||
|                 borderRadius: BorderRadius.circular(8), | ||||
|               ), | ||||
|             ), | ||||
|             const SizedBox(width: 20,), | ||||
|             SizedBox( | ||||
|               width: MediaQuery.of(context).size.width - 45, | ||||
|               child: builder(context, state), | ||||
|             ) | ||||
|           ], | ||||
|         ); | ||||
|       }), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class _NameInput extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return BlocBuilder<FormDataCubit, FormDataState>( | ||||
|       builder: (context, state) { | ||||
|         return TextField( | ||||
|           onChanged: (name) => context | ||||
|               .read<FormDataCubit>() | ||||
|               .dataChanged(formFieldName, Name.dirty(name)), | ||||
|           keyboardType: TextInputType.name, | ||||
|           decoration: InputDecoration( | ||||
|             labelText: 'name', | ||||
|             helperText: '', | ||||
|             errorText: | ||||
|                 state.data.input(formFieldName).invalid ? 'invalid name' : null, | ||||
|           ), | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|     return CategoryIndicator( | ||||
|         field: formFieldName, | ||||
|         builder: (context, state) { | ||||
|           return TextField( | ||||
|             onChanged: (name) => context | ||||
|                 .read<FormDataCubit>() | ||||
|                 .dataChanged(formFieldName, Name.dirty(name)), | ||||
|             keyboardType: TextInputType.name, | ||||
|             decoration: InputDecoration( | ||||
|               labelText: 'name', | ||||
|               helperText: '', | ||||
|               errorText: | ||||
|                   state.data.isNotValid(formFieldName) ? 'invalid name' : null, | ||||
|             ), | ||||
|           ); | ||||
|         }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class _EmailInput extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return BlocBuilder<FormDataCubit, FormDataState>( | ||||
|     return CategoryIndicator( | ||||
|         field: formFieldEmail, | ||||
|       builder: (context, state) { | ||||
|         return TextField( | ||||
|           onChanged: (email) => context | ||||
| @ -57,9 +105,8 @@ class _EmailInput extends StatelessWidget { | ||||
|           decoration: InputDecoration( | ||||
|             labelText: 'email', | ||||
|             helperText: '', | ||||
|             errorText: state.data.input(formFieldEmail).invalid | ||||
|                 ? 'invalid email' | ||||
|                 : null, | ||||
|             errorText: | ||||
|                 state.data.isNotValid(formFieldEmail) ? 'invalid email' : null, | ||||
|           ), | ||||
|         ); | ||||
|       }, | ||||
| @ -70,7 +117,8 @@ class _EmailInput extends StatelessWidget { | ||||
| class _PhoneInput extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return BlocBuilder<FormDataCubit, FormDataState>( | ||||
|     return CategoryIndicator( | ||||
|         field: formFieldPhone, | ||||
|       builder: (context, state) { | ||||
|         return TextField( | ||||
|           onChanged: (phone) => context | ||||
| @ -80,9 +128,8 @@ class _PhoneInput extends StatelessWidget { | ||||
|           decoration: InputDecoration( | ||||
|             labelText: 'phone', | ||||
|             helperText: '', | ||||
|             errorText: state.data.input(formFieldPhone).invalid | ||||
|                 ? 'invalid phone' | ||||
|                 : null, | ||||
|             errorText: | ||||
|                 state.data.isNotValid(formFieldPhone) ? 'invalid phone' : null, | ||||
|           ), | ||||
|         ); | ||||
|       }, | ||||
| @ -93,7 +140,8 @@ class _PhoneInput extends StatelessWidget { | ||||
| class _SirenInput extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return BlocBuilder<FormDataCubit, FormDataState>( | ||||
|     return CategoryIndicator( | ||||
|         field: formFieldName, | ||||
|       builder: (context, state) { | ||||
|         return TextField( | ||||
|           onChanged: (siren) => context | ||||
| @ -103,9 +151,8 @@ class _SirenInput extends StatelessWidget { | ||||
|           decoration: InputDecoration( | ||||
|             labelText: 'siren', | ||||
|             helperText: '', | ||||
|             errorText: state.data.input(formFieldSiren).invalid | ||||
|                 ? 'invalid SIREN' | ||||
|                 : null, | ||||
|             errorText: | ||||
|                 state.data.isNotValid(formFieldSiren) ? 'invalid SIREN' : null, | ||||
|           ), | ||||
|         ); | ||||
|       }, | ||||
| @ -127,7 +174,7 @@ class _IbanInput extends StatelessWidget { | ||||
|             labelText: 'iban', | ||||
|             helperText: '', | ||||
|             errorText: | ||||
|                 state.data.input(formFieldIban).invalid ? 'invalid IBAN' : null, | ||||
|                 state.data.isNotValid(formFieldIban) ? 'invalid IBAN' : null, | ||||
|           ), | ||||
|         ); | ||||
|       }, | ||||
| @ -141,7 +188,7 @@ class _CheckListInput extends StatelessWidget { | ||||
|     return BlocBuilder<FormDataCubit, FormDataState>( | ||||
|       builder: (context, state) { | ||||
|         final _input = | ||||
|             state.data.input<List<String>>(formFieldList) as ListOption<String>; | ||||
|             state.data.validatorOf<ListOption<String>>(formFieldList); | ||||
|         final _options = _input.value; | ||||
| 
 | ||||
|         return Column( | ||||
| @ -192,8 +239,7 @@ class _RadioListInput extends StatelessWidget { | ||||
|   Widget build(BuildContext context) { | ||||
|     return BlocBuilder<FormDataCubit, FormDataState>( | ||||
|       builder: (context, state) { | ||||
|         final _input = | ||||
|             state.data.input<String>(formFieldRadio) as TextString; | ||||
|         final _input = state.data.validatorOf<TextString>(formFieldRadio); | ||||
| 
 | ||||
|         return Column( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
| @ -249,7 +295,7 @@ class _CheckHiddenInput extends StatelessWidget { | ||||
|       trailing: BlocBuilder<FormDataCubit, FormDataState>( | ||||
|         builder: (context, state) { | ||||
|           return Checkbox( | ||||
|               value: state.data.input<bool>(formFieldHidden).value, | ||||
|               value: state.data.validatorOf<Boolean>(formFieldHidden).value, | ||||
|               onChanged: (v) { | ||||
|                 final value = v!; // state is false, so value can't be null | ||||
| 
 | ||||
| @ -272,7 +318,7 @@ class _CheckIsProInput extends StatelessWidget { | ||||
|       trailing: BlocBuilder<FormDataCubit, FormDataState>( | ||||
|         builder: (context, state) { | ||||
|           return Checkbox( | ||||
|               value: state.data.input<bool>(formFieldPro).value, | ||||
|               value: state.data.validatorOf<Boolean>(formFieldPro).value, | ||||
|               onChanged: (isPro) { | ||||
|                 final value = isPro!; // state is false, so value can't be null | ||||
| 
 | ||||
| @ -370,7 +416,7 @@ class SignUpForm extends StatelessWidget { | ||||
|             const SizedBox(height: 8), | ||||
|             BlocBuilder<FormDataCubit, FormDataState>( | ||||
|               builder: (context, state) { | ||||
|                 if (state.data.input<bool>(formFieldPro).value) { | ||||
|                 if (state.data.validatorOf<Boolean>(formFieldPro).value) { | ||||
|                   return Column(children: [ | ||||
|                     _SirenInput(), | ||||
|                     const SizedBox(height: 8), | ||||
|  | ||||
| @ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | ||||
| version: 1.0.0+1 | ||||
| 
 | ||||
| environment: | ||||
|   sdk: ">=2.16.2 <3.0.0" | ||||
|   sdk: ">=2.17.2 <3.0.0" | ||||
| 
 | ||||
| # Dependencies specify other packages that your package needs in order to work. | ||||
| # To automatically upgrade your package dependencies to the latest versions | ||||
|  | ||||
							
								
								
									
										
											BIN
										
									
								
								packages/wyatt_form_bloc/example/web/favicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								packages/wyatt_form_bloc/example/web/favicon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 917 B | 
							
								
								
									
										
											BIN
										
									
								
								packages/wyatt_form_bloc/example/web/icons/Icon-192.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								packages/wyatt_form_bloc/example/web/icons/Icon-192.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 5.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/wyatt_form_bloc/example/web/icons/Icon-512.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								packages/wyatt_form_bloc/example/web/icons/Icon-512.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 8.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/wyatt_form_bloc/example/web/icons/Icon-maskable-192.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								packages/wyatt_form_bloc/example/web/icons/Icon-maskable-192.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 5.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								packages/wyatt_form_bloc/example/web/icons/Icon-maskable-512.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								packages/wyatt_form_bloc/example/web/icons/Icon-maskable-512.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 20 KiB | 
							
								
								
									
										58
									
								
								packages/wyatt_form_bloc/example/web/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								packages/wyatt_form_bloc/example/web/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
|   <!-- | ||||
|     If you are serving your web app in a path other than the root, change the | ||||
|     href value below to reflect the base path you are serving from. | ||||
| 
 | ||||
|     The path provided below has to start and end with a slash "/" in order for | ||||
|     it to work correctly. | ||||
| 
 | ||||
|     For more details: | ||||
|     * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base | ||||
| 
 | ||||
|     This is a placeholder for base href that will be replaced by the value of | ||||
|     the `--base-href` argument provided to `flutter build`. | ||||
|   --> | ||||
|   <base href="$FLUTTER_BASE_HREF"> | ||||
| 
 | ||||
|   <meta charset="UTF-8"> | ||||
|   <meta content="IE=Edge" http-equiv="X-UA-Compatible"> | ||||
|   <meta name="description" content="A new Flutter project."> | ||||
| 
 | ||||
|   <!-- iOS meta tags & icons --> | ||||
|   <meta name="apple-mobile-web-app-capable" content="yes"> | ||||
|   <meta name="apple-mobile-web-app-status-bar-style" content="black"> | ||||
|   <meta name="apple-mobile-web-app-title" content="example"> | ||||
|   <link rel="apple-touch-icon" href="icons/Icon-192.png"> | ||||
| 
 | ||||
|   <!-- Favicon --> | ||||
|   <link rel="icon" type="image/png" href="favicon.png"/> | ||||
| 
 | ||||
|   <title>example</title> | ||||
|   <link rel="manifest" href="manifest.json"> | ||||
| 
 | ||||
|   <script> | ||||
|     // The value below is injected by flutter build, do not touch. | ||||
|     var serviceWorkerVersion = null; | ||||
|   </script> | ||||
|   <!-- This script adds the flutter initialization JS code --> | ||||
|   <script src="flutter.js" defer></script> | ||||
| </head> | ||||
| <body> | ||||
|   <script> | ||||
|     window.addEventListener('load', function(ev) { | ||||
|       // Download main.dart.js | ||||
|       _flutter.loader.loadEntrypoint({ | ||||
|         serviceWorker: { | ||||
|           serviceWorkerVersion: serviceWorkerVersion, | ||||
|         } | ||||
|       }).then(function(engineInitializer) { | ||||
|         return engineInitializer.initializeEngine(); | ||||
|       }).then(function(appRunner) { | ||||
|         return appRunner.runApp(); | ||||
|       }); | ||||
|     }); | ||||
|   </script> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										35
									
								
								packages/wyatt_form_bloc/example/web/manifest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								packages/wyatt_form_bloc/example/web/manifest.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| { | ||||
|     "name": "example", | ||||
|     "short_name": "example", | ||||
|     "start_url": ".", | ||||
|     "display": "standalone", | ||||
|     "background_color": "#0175C2", | ||||
|     "theme_color": "#0175C2", | ||||
|     "description": "A new Flutter project.", | ||||
|     "orientation": "portrait-primary", | ||||
|     "prefer_related_applications": false, | ||||
|     "icons": [ | ||||
|         { | ||||
|             "src": "icons/Icon-192.png", | ||||
|             "sizes": "192x192", | ||||
|             "type": "image/png" | ||||
|         }, | ||||
|         { | ||||
|             "src": "icons/Icon-512.png", | ||||
|             "sizes": "512x512", | ||||
|             "type": "image/png" | ||||
|         }, | ||||
|         { | ||||
|             "src": "icons/Icon-maskable-192.png", | ||||
|             "sizes": "192x192", | ||||
|             "type": "image/png", | ||||
|             "purpose": "maskable" | ||||
|         }, | ||||
|         { | ||||
|             "src": "icons/Icon-maskable-512.png", | ||||
|             "sizes": "512x512", | ||||
|             "type": "image/png", | ||||
|             "purpose": "maskable" | ||||
|         } | ||||
|     ] | ||||
| } | ||||
| @ -17,6 +17,7 @@ | ||||
| import 'dart:async'; | ||||
| 
 | ||||
| import 'package:bloc/bloc.dart'; | ||||
| import 'package:equatable/equatable.dart'; | ||||
| import 'package:meta/meta.dart'; | ||||
| import 'package:wyatt_form_bloc/src/enums/enums.dart'; | ||||
| import 'package:wyatt_form_bloc/src/form/form.dart'; | ||||
| @ -24,19 +25,19 @@ import 'package:wyatt_form_bloc/src/form/form.dart'; | ||||
| part 'form_data_state.dart'; | ||||
| 
 | ||||
| abstract class FormDataCubit extends Cubit<FormDataState> { | ||||
|   FormDataCubit({required FormData entries}) | ||||
|       : super(FormDataState(data: entries)); | ||||
|   FormDataCubit({required FormData inputs}) | ||||
|       : super(FormDataState(data: inputs)); | ||||
| 
 | ||||
|   /// Change value of a field. | ||||
|   ///  | ||||
|   /// Inputs: | ||||
|   /// - `field`: The key of the field to change. | ||||
|   /// - `dirtyValue`: The new value of the field. | ||||
|   void dataChanged(String field, FormInput dirtyValue) { | ||||
|   /// - `dirtyValue`: The new value of the field. (Wrapped in a dirty validator) | ||||
|   void dataChanged(String field, FormInputValidator dirtyValue) { | ||||
|     final _form = state.data.clone(); | ||||
| 
 | ||||
|     if (_form.contains(field)) { | ||||
|       _form.update(field, dirtyValue); | ||||
|       _form.updateValidator(field, dirtyValue); | ||||
|     } else { | ||||
|       throw Exception('Form field $field not found'); | ||||
|     } | ||||
| @ -44,7 +45,7 @@ abstract class FormDataCubit extends Cubit<FormDataState> { | ||||
|     emit( | ||||
|       state.copyWith( | ||||
|         data: _form, | ||||
|         status: _form.selfValidate(), | ||||
|         status: _form.validate(), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| @ -78,7 +79,7 @@ abstract class FormDataCubit extends Cubit<FormDataState> { | ||||
|     emit( | ||||
|       state.copyWith( | ||||
|         data: _form, | ||||
|         status: _form.selfValidate(), | ||||
|         status: _form.validate(), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @ -17,7 +17,7 @@ | ||||
| part of 'form_data_cubit.dart'; | ||||
| 
 | ||||
| @immutable | ||||
| class FormDataState { | ||||
| class FormDataState extends Equatable { | ||||
|   final FormStatus status; | ||||
|   final FormData data; | ||||
|   final String? errorMessage; | ||||
| @ -41,23 +41,8 @@ class FormDataState { | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     if (identical(this, other)) return true; | ||||
| 
 | ||||
|     return other is FormDataState && | ||||
|         other.status == status && | ||||
|         other.data == data && | ||||
|         other.errorMessage == errorMessage; | ||||
|   } | ||||
| 
 | ||||
|   bool? get stringify => true; | ||||
|    | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     return status.hashCode ^ data.hashCode ^ errorMessage.hashCode; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'FormDataState(status: $status, data: $data, ' | ||||
|         'errorMessage: $errorMessage)'; | ||||
|   } | ||||
|   List<Object?> get props => [status, data, errorMessage]; | ||||
| } | ||||
|  | ||||
| @ -14,6 +14,16 @@ | ||||
| // You should have received a copy of the GNU General Public License | ||||
| // along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| import 'package:wyatt_form_bloc/src/form/form.dart'; | ||||
| 
 | ||||
| const Set<FormStatus> _validatedFormStatuses = <FormStatus>{ | ||||
|   FormStatus.valid, | ||||
|   FormStatus.submissionInProgress, | ||||
|   FormStatus.submissionSuccess, | ||||
|   FormStatus.submissionFailure, | ||||
|   FormStatus.submissionCanceled, | ||||
| }; | ||||
| 
 | ||||
| /// Enum representing the status of a form at any given point in time. | ||||
| enum FormStatus { | ||||
|   /// The form has not been touched. | ||||
| @ -35,19 +45,8 @@ enum FormStatus { | ||||
|   submissionFailure, | ||||
| 
 | ||||
|   /// The form submission has been canceled. | ||||
|   submissionCanceled | ||||
| } | ||||
|   submissionCanceled; | ||||
| 
 | ||||
| const Set<FormStatus> _validatedFormStatuses = <FormStatus>{ | ||||
|   FormStatus.valid, | ||||
|   FormStatus.submissionInProgress, | ||||
|   FormStatus.submissionSuccess, | ||||
|   FormStatus.submissionFailure, | ||||
|   FormStatus.submissionCanceled, | ||||
| }; | ||||
| 
 | ||||
| /// Useful extensions on [FormStatus] | ||||
| extension FormStatusX on FormStatus { | ||||
|   /// Indicates whether the form is untouched. | ||||
|   bool get isPure => this == FormStatus.pure; | ||||
| 
 | ||||
| @ -76,4 +75,13 @@ extension FormStatusX on FormStatus { | ||||
| 
 | ||||
|   /// Indicates whether the form submission has been canceled. | ||||
|   bool get isSubmissionCanceled => this == FormStatus.submissionCanceled; | ||||
| 
 | ||||
|   /// Validate a list of inputs | ||||
|   static FormStatus validate(List<FormInput> inputs) { | ||||
|     return inputs.every((FormInput input) => input.validator.pure) | ||||
|         ? FormStatus.pure | ||||
|         : inputs.any((FormInput input) => input.validator.valid == false) | ||||
|             ? FormStatus.invalid | ||||
|             : FormStatus.valid; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,19 +1,25 @@ | ||||
| // Copyright (C) 2022 WYATT GROUP | ||||
| // Please see the AUTHORS file for details. | ||||
| //  | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU General Public License as published by | ||||
| // the Free Software Foundation, either version 3 of the License, or | ||||
| // any later version. | ||||
| //  | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
| // GNU General Public License for more details. | ||||
| //  | ||||
| // | ||||
| // You should have received a copy of the GNU General Public License | ||||
| // along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| export 'form_data.dart'; | ||||
| export 'form_entry.dart'; | ||||
| export 'form_input.dart'; | ||||
| import 'package:equatable/equatable.dart'; | ||||
| import 'package:meta/meta.dart'; | ||||
| import 'package:wyatt_form_bloc/src/enums/form_input_status.dart'; | ||||
| import 'package:wyatt_form_bloc/src/enums/form_status.dart'; | ||||
| 
 | ||||
| part 'form_input.dart'; | ||||
| part 'form_input_metadata.dart'; | ||||
| part 'form_input_validator.dart'; | ||||
| part 'form_data.dart'; | ||||
|  | ||||
| @ -14,133 +14,164 @@ | ||||
| // You should have received a copy of the GNU General Public License | ||||
| // along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| import 'package:meta/meta.dart'; | ||||
| import 'package:wyatt_form_bloc/src/enums/enums.dart'; | ||||
| import 'package:wyatt_form_bloc/src/form/form.dart'; | ||||
| import 'package:wyatt_form_bloc/src/utils/list_equals.dart'; | ||||
| part of 'form.dart'; | ||||
| 
 | ||||
| @immutable | ||||
| class FormData { | ||||
|   const FormData(this._entries); | ||||
| class FormData extends Equatable { | ||||
|   final List<FormInput> _inputs; | ||||
| 
 | ||||
|   FormData.empty() : this(<FormEntry>[]); | ||||
|   const FormData(this._inputs); | ||||
|   const FormData.empty() : this(const <FormInput>[]); | ||||
| 
 | ||||
|   final List<FormEntry> _entries; | ||||
| 
 | ||||
|   List<FormEntry> get entries => _entries; | ||||
| 
 | ||||
|   List<FormInput<T, ValidationError>> inputs<T>() { | ||||
|     return _entries | ||||
|         .map((FormEntry entry) => entry.input as FormInput<T, ValidationError>) | ||||
|         .toList(); | ||||
|   } | ||||
| 
 | ||||
|   static FormStatus validate(List<FormInput> inputs) { | ||||
|     return inputs.every((FormInput element) => element.pure) | ||||
|         ? FormStatus.pure | ||||
|         : inputs.any((FormInput input) => input.valid == false) | ||||
|             ? FormStatus.invalid | ||||
|             : FormStatus.valid; | ||||
|   } | ||||
| 
 | ||||
|   FormStatus selfValidate() { | ||||
|     return validate(inputs<dynamic>()); | ||||
|   } | ||||
| 
 | ||||
|   FormInput<T, ValidationError> input<T>(String key) { | ||||
|   /// Returns the input for the associated key | ||||
|   FormInput inputByKey(String key) { | ||||
|     if (contains(key)) { | ||||
|       return _entries.firstWhere((FormEntry entry) => entry.key == key).input | ||||
|           as FormInput<T, ValidationError>; | ||||
|       return _inputs.firstWhere((FormInput input) => input.key == key); | ||||
|     } else { | ||||
|       throw Exception('FormInput with key `$key` does not exist in form'); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   bool contains(String key) { | ||||
|     return _entries.any((FormEntry entry) => entry.key == key); | ||||
|   } | ||||
| 
 | ||||
|   FormData intersection(FormData other) { | ||||
|     final List<FormEntry> entries = <FormEntry>[]; | ||||
| 
 | ||||
|     for (final FormEntry entry in _entries) { | ||||
|       if (other.contains(entry.key)) { | ||||
|         entries.add(entry); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return FormData(entries); | ||||
|   } | ||||
| 
 | ||||
|   FormData difference(FormData other) { | ||||
|     final List<FormEntry> entries = <FormEntry>[]; | ||||
| 
 | ||||
|     for (final FormEntry otherEntry in other._entries) { | ||||
|       if (!contains(otherEntry.key)) { | ||||
|         entries.add(otherEntry); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     for (final FormEntry entry in _entries) { | ||||
|       if (!other.contains(entry.key)) { | ||||
|         entries.add(entry); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return FormData(entries); | ||||
|   } | ||||
| 
 | ||||
|   FormData union(FormData other) { | ||||
|     final List<FormEntry> entries = <FormEntry>[]; | ||||
| 
 | ||||
|     for (final FormEntry entry in _entries) { | ||||
|       entries.add(entry); | ||||
|     } | ||||
| 
 | ||||
|     for (final FormEntry otherEntry in other._entries) { | ||||
|       if (!contains(otherEntry.key)) { | ||||
|         entries.add(otherEntry); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return FormData(entries); | ||||
|   } | ||||
| 
 | ||||
|   void update(String key, FormInput input) { | ||||
|   /// Updates a input (perform a replace at index). | ||||
|   void updateInput(String key, FormInput input) { | ||||
|     if (contains(key)) { | ||||
|       final index = _entries.indexOf( | ||||
|         _entries.firstWhere((FormEntry entry) => entry.key == key), | ||||
|       final index = _inputs.indexOf( | ||||
|         inputByKey(key), | ||||
|       ); | ||||
|       _entries[index] = _entries[index].copyWith(input: input); | ||||
|       _inputs[index] = input; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// Updates validator of a given input. (perform copyWith) | ||||
|   void updateValidator(String key, FormInputValidator dirtyValue) { | ||||
|     if (contains(key)) { | ||||
|       final index = _inputs.indexOf( | ||||
|         inputByKey(key), | ||||
|       ); | ||||
|       _inputs[index] = _inputs[index].copyWith(validator: dirtyValue); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// Updates metadata of a given input. (perform copyWith) | ||||
|   void updateMetadata(String key, FormInputMetadata meta) { | ||||
|     if (contains(key)) { | ||||
|       final index = _inputs.indexOf( | ||||
|         inputByKey(key), | ||||
|       ); | ||||
|       _inputs[index] = _inputs[index].copyWith(metadata: meta); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// A [FormInputValidator] represents the value of a single form input field. | ||||
|   /// It contains information about the [FormInputStatus], value, as well | ||||
|   /// as validation status. | ||||
|   T validatorOf<T>(String key) { | ||||
|     return inputByKey(key).validator as T; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a validation error if the [FormInputValidator] is invalid.  | ||||
|   /// Returns null if the [FormInputValidator] is valid. | ||||
|   E? errorOf<V, E>(String key) { | ||||
|     return (inputByKey(key).validator as FormInputValidator<V, E>).error; | ||||
|   } | ||||
| 
 | ||||
|   /// The value of the associated [FormInputValidator]. For example,  | ||||
|   /// if you have a FormInputValidator for FirstName, the value could be 'Joe'. | ||||
|   V valueOf<V, E>(String key) { | ||||
|     return (inputByKey(key).validator as FormInputValidator<V, E>).value; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns `true` if the [FormInputValidator] is not valid. | ||||
|   /// Same as `E? errorOf<V, E>(String key) != null` | ||||
|   bool isNotValid(String key) { | ||||
|     return !inputByKey(key).validator.valid; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns the metadata associated. With `M` the type of extra data. | ||||
|   FormInputMetadata<M> metadataOf<M>(String key) { | ||||
|     return inputByKey(key).metadata as FormInputMetadata<M>; | ||||
|   } | ||||
| 
 | ||||
|   /// Validate self inputs. | ||||
|   FormStatus validate() { | ||||
|     return FormStatus.validate(_inputs); | ||||
|   } | ||||
| 
 | ||||
|   /// Check if this contains an input with the given key. | ||||
|   bool contains(String key) { | ||||
|     return _inputs.any((FormInput input) => input.key == key); | ||||
|   } | ||||
| 
 | ||||
|   /// Makes an intersection set operation and returns newly created [FormData] | ||||
|   FormData intersection(FormData other) { | ||||
|     final List<FormInput> inputs = <FormInput>[]; | ||||
| 
 | ||||
|     for (final FormInput i in _inputs) { | ||||
|       if (other.contains(i.key)) { | ||||
|         inputs.add(i); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return FormData(inputs); | ||||
|   } | ||||
| 
 | ||||
|   /// Makes a difference set operation and returns newly created [FormData] | ||||
|   FormData difference(FormData other) { | ||||
|     final List<FormInput> inputs = <FormInput>[]; | ||||
| 
 | ||||
|     for (final FormInput i in other._inputs) { | ||||
|       if (!contains(i.key)) { | ||||
|         inputs.add(i); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     for (final FormInput i in _inputs) { | ||||
|       if (!other.contains(i.key)) { | ||||
|         inputs.add(i); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return FormData(inputs); | ||||
|   } | ||||
| 
 | ||||
|   /// Makes an union set operation and returns newly created [FormData] | ||||
|   FormData union(FormData other) { | ||||
|     final List<FormInput> inputs = <FormInput>[]; | ||||
| 
 | ||||
|     for (final FormInput i in _inputs) { | ||||
|       inputs.add(i); | ||||
|     } | ||||
| 
 | ||||
|     for (final FormInput i in other._inputs) { | ||||
|       if (!contains(i.key)) { | ||||
|         inputs.add(i); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return FormData(inputs); | ||||
|   } | ||||
| 
 | ||||
|   /// Deeply copy this. | ||||
|   FormData clone() { | ||||
|     return FormData( | ||||
|       _entries.map((FormEntry entry) => entry.clone()).toList(), | ||||
|       _inputs.map((FormInput input) => input.clone()).toList(), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// Export this to [Map] format. | ||||
|   Map<String, dynamic> toMap() { | ||||
|     final map = <String, dynamic>{}; | ||||
|     for (final entry in _entries) { | ||||
|       if (entry.export) { | ||||
|         map[entry.name] = entry.input.value; | ||||
|     for (final input in _inputs) { | ||||
|       if (input.metadata.export) { | ||||
|         map[input.name] = input.validator.value; | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     if (identical(this, other)) return true; | ||||
| 
 | ||||
|     return other is FormData && listEquals(other._entries, _entries); | ||||
|   } | ||||
| 
 | ||||
|   bool? get stringify => true; | ||||
|    | ||||
|   @override | ||||
|   int get hashCode => _entries.hashCode; | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'FormData(entries: $_entries)'; | ||||
|   List<Object?> get props => _inputs; | ||||
| } | ||||
|  | ||||
| @ -1,79 +0,0 @@ | ||||
| // Copyright (C) 2022 WYATT GROUP | ||||
| // Please see the AUTHORS file for details. | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU General Public License as published by | ||||
| // the Free Software Foundation, either version 3 of the License, or | ||||
| // any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
| // GNU General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU General Public License | ||||
| // along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| import 'package:meta/meta.dart'; | ||||
| import 'package:wyatt_form_bloc/src/form/form.dart'; | ||||
| 
 | ||||
| @immutable | ||||
| class FormEntry { | ||||
|   const FormEntry(this.key, this.input, {this.export = true, String? name}) | ||||
|       : _field = name ?? key; | ||||
| 
 | ||||
|   final String key; | ||||
|   final FormInput input; | ||||
|   final bool export; | ||||
|   final String _field; | ||||
| 
 | ||||
|   String get name => _field; | ||||
| 
 | ||||
|   FormEntry copyWith({ | ||||
|     String? key, | ||||
|     FormInput? input, | ||||
|     bool? export, | ||||
|     String? name, | ||||
|   }) { | ||||
|     return FormEntry( | ||||
|       key ?? this.key, | ||||
|       input ?? this.input, | ||||
|       export: export ?? this.export, | ||||
|       name: name, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   FormEntry clone() { | ||||
|     return FormEntry( | ||||
|       key, | ||||
|       input, | ||||
|       export: export, | ||||
|       name: name, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     if (identical(this, other)) return true; | ||||
| 
 | ||||
|     return other is FormEntry && | ||||
|         other.key == key && | ||||
|         other.input == input && | ||||
|         other.export == export && | ||||
|         other.name == name; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     return key.hashCode ^ | ||||
|         input.hashCode ^ | ||||
|         export.hashCode ^ | ||||
|         name.hashCode; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'FormEntry(key: $key, input: $input, ' | ||||
|         'export: $export, name: $name)'; | ||||
|   } | ||||
| } | ||||
| @ -1,3 +1,4 @@ | ||||
| // ignore_for_file: public_member_api_docs, sort_constructors_first | ||||
| // Copyright (C) 2022 WYATT GROUP | ||||
| // Please see the AUTHORS file for details. | ||||
| // | ||||
| @ -14,98 +15,45 @@ | ||||
| // You should have received a copy of the GNU General Public License | ||||
| // along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| import 'package:meta/meta.dart'; | ||||
| import 'package:wyatt_form_bloc/src/enums/enums.dart'; | ||||
| part of 'form.dart'; | ||||
| 
 | ||||
| /// {@template form_input} | ||||
| /// A [FormInput] represents the value of a single form input field. | ||||
| /// It contains information about the [FormInputStatus], [value], as well | ||||
| /// as validation status. | ||||
| /// | ||||
| /// [FormInput] should be extended to define custom [FormInput] instances. | ||||
| /// | ||||
| /// ```dart | ||||
| /// enum FirstNameError { empty } | ||||
| /// class FirstName extends FormInput<String, FirstNameError> { | ||||
| ///   const FirstName.pure({String value = ''}) : super.pure(value); | ||||
| ///   const FirstName.dirty({String value = ''}) : super.dirty(value); | ||||
| /// | ||||
| ///   @override | ||||
| ///   FirstNameError? validator(String value) { | ||||
| ///     return value.isEmpty ? FirstNameError.empty : null; | ||||
| ///   } | ||||
| /// } | ||||
| /// ``` | ||||
| /// {@endtemplate} | ||||
| @immutable | ||||
| abstract class FormInput<T, E> { | ||||
|   const FormInput._(this.value, [this.pure = true]); | ||||
| class FormInput extends Equatable { | ||||
|   final String key; | ||||
|   final FormInputValidator validator; | ||||
|   final FormInputMetadata metadata; | ||||
| 
 | ||||
|   /// Constructor which create a `pure` [FormInput] with a given value. | ||||
|   const FormInput.pure(T value) : this._(value); | ||||
|   String get name => metadata._name ?? key; | ||||
| 
 | ||||
|   /// Constructor which create a `dirty` [FormInput] with a given value. | ||||
|   const FormInput.dirty(T value) : this._(value, false); | ||||
| 
 | ||||
|   /// The value of the given [FormInput]. | ||||
|   /// For example, if you have a `FormInput` for `FirstName`, | ||||
|   /// the value could be 'Joe'. | ||||
|   final T value; | ||||
| 
 | ||||
|   /// If the [FormInput] is pure (has been touched/modified). | ||||
|   /// Typically when the `FormInput` is initially created, | ||||
|   /// it is created using the `FormInput.pure` constructor to | ||||
|   /// signify that the user has not modified it. | ||||
|   /// | ||||
|   /// For subsequent changes (in response to user input), the | ||||
|   /// `FormInput.dirty` constructor should be used to signify that | ||||
|   /// the `FormInput` has been manipulated. | ||||
|   final bool pure; | ||||
| 
 | ||||
|   /// The [FormInputStatus] which can be one of the following: | ||||
|   /// * [FormInputStatus.pure] | ||||
|   ///   - if the input has not been modified. | ||||
|   /// * [FormInputStatus.invalid] | ||||
|   ///   - if the input has been modified and validation failed. | ||||
|   /// * [FormInputStatus.valid] | ||||
|   ///   - if the input has been modified and validation succeeded. | ||||
|   FormInputStatus get status => pure | ||||
|       ? FormInputStatus.pure | ||||
|       : valid | ||||
|           ? FormInputStatus.valid | ||||
|           : FormInputStatus.invalid; | ||||
| 
 | ||||
|   /// Returns a validation error if the [FormInput] is invalid. | ||||
|   /// Returns `null` if the [FormInput] is valid. | ||||
|   E? get error => validator(value); | ||||
| 
 | ||||
|   /// Whether the [FormInput] value is valid according to the | ||||
|   /// overridden `validator`. | ||||
|   /// | ||||
|   /// Returns `true` if `validator` returns `null` for the | ||||
|   /// current [FormInput] value and `false` otherwise. | ||||
|   bool get valid => validator(value) == null; | ||||
| 
 | ||||
|   /// Whether the [FormInput] value is not valid. | ||||
|   /// A value is invalid when the overridden `validator` | ||||
|   /// returns an error (non-null value). | ||||
|   bool get invalid => status == FormInputStatus.invalid; | ||||
| 
 | ||||
|   /// A function that must return a validation error if the provided | ||||
|   /// [value] is invalid and `null` otherwise. | ||||
|   E? validator(T value); | ||||
|   const FormInput( | ||||
|     this.key, | ||||
|     this.validator, { | ||||
|     // ignore: avoid_redundant_argument_values | ||||
|     this.metadata = const FormInputMetadata<void>(export: true), | ||||
|   }); | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => value.hashCode ^ pure.hashCode; | ||||
|   bool? get stringify => true; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     if (other.runtimeType != runtimeType) return false; | ||||
|     return other is FormInput<T, E> && | ||||
|         other.value == value && | ||||
|         other.pure == pure; | ||||
|   List<Object?> get props => [key, validator, metadata]; | ||||
| 
 | ||||
|   FormInput copyWith({ | ||||
|     String? key, | ||||
|     FormInputValidator? validator, | ||||
|     FormInputMetadata? metadata, | ||||
|   }) { | ||||
|     return FormInput( | ||||
|       key ?? this.key, | ||||
|       validator ?? this.validator, | ||||
|       metadata: metadata ?? this.metadata, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'FormInput<$runtimeType>(value: $value, pure: $pure)'; | ||||
|   FormInput clone() { | ||||
|     return copyWith( | ||||
|       key: key, | ||||
|       validator: validator, | ||||
|       metadata: metadata, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,56 @@ | ||||
| // ignore_for_file: public_member_api_docs, sort_constructors_first | ||||
| // Copyright (C) 2022 WYATT GROUP | ||||
| // Please see the AUTHORS file for details. | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU General Public License as published by | ||||
| // the Free Software Foundation, either version 3 of the License, or | ||||
| // any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
| // GNU General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU General Public License | ||||
| // along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| part of 'form.dart'; | ||||
| 
 | ||||
| class FormInputMetadata<T> extends Equatable { | ||||
|   final bool export; | ||||
|   final String? _name; | ||||
|   final T? extra; | ||||
| 
 | ||||
|   const FormInputMetadata({ | ||||
|     this.export = true, | ||||
|     this.extra, | ||||
|     String? name, | ||||
|   }) : _name = name; | ||||
|    | ||||
|   @override | ||||
|   bool? get stringify => true; | ||||
| 
 | ||||
|   @override | ||||
|   List<Object?> get props => [export, _name, extra]; | ||||
| 
 | ||||
|   FormInputMetadata<T> copyWith({ | ||||
|     bool? export, | ||||
|     String? name, | ||||
|     T? extra, | ||||
|   }) { | ||||
|     return FormInputMetadata<T>( | ||||
|       export: export ?? this.export, | ||||
|       name: name ?? _name, | ||||
|       extra: extra ?? this.extra, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   FormInputMetadata<T> clone() { | ||||
|     return copyWith( | ||||
|       export: export, | ||||
|       name: _name, | ||||
|       extra: extra, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										104
									
								
								packages/wyatt_form_bloc/lib/src/form/form_input_validator.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								packages/wyatt_form_bloc/lib/src/form/form_input_validator.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,104 @@ | ||||
| // Copyright (C) 2022 WYATT GROUP | ||||
| // Please see the AUTHORS file for details. | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU General Public License as published by | ||||
| // the Free Software Foundation, either version 3 of the License, or | ||||
| // any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
| // GNU General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU General Public License | ||||
| // along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| part of 'form.dart'; | ||||
| 
 | ||||
| /// {@template form_input} | ||||
| /// A [FormInputValidator] represents the value of a single form input field. | ||||
| /// It contains information about the [FormInputStatus], [value], as well | ||||
| /// as validation status. | ||||
| /// | ||||
| /// [FormInputValidator] should be extended to define custom | ||||
| /// [FormInputValidator] instances. | ||||
| /// | ||||
| /// ```dart | ||||
| /// enum FirstNameError { empty } | ||||
| /// class FirstName extends FormInputValidator<String, FirstNameError> { | ||||
| ///   const FirstName.pure({String value = ''}) : super.pure(value); | ||||
| ///   const FirstName.dirty({String value = ''}) : super.dirty(value); | ||||
| /// | ||||
| ///   @override | ||||
| ///   FirstNameError? validator(String value) { | ||||
| ///     return value.isEmpty ? FirstNameError.empty : null; | ||||
| ///   } | ||||
| /// } | ||||
| /// ``` | ||||
| /// {@endtemplate} | ||||
| @immutable | ||||
| abstract class FormInputValidator<V, E> extends Equatable { | ||||
|   const FormInputValidator._(this.value, [this.pure = true]); | ||||
| 
 | ||||
|   /// Constructor which create a `pure` [FormInputValidator] with a given value. | ||||
|   const FormInputValidator.pure(V value) : this._(value); | ||||
| 
 | ||||
|   /// Constructor which create a `dirty` [FormInputValidator] with a  | ||||
|   /// given value. | ||||
|   const FormInputValidator.dirty(V value) : this._(value, false); | ||||
| 
 | ||||
|   /// The value of the given [FormInputValidator]. | ||||
|   /// For example, if you have a `FormInputValidator` for `FirstName`, | ||||
|   /// the value could be 'Joe'. | ||||
|   final V value; | ||||
| 
 | ||||
|   /// If the [FormInputValidator] is pure (has been touched/modified). | ||||
|   /// Typically when the `FormInputValidator` is initially created, | ||||
|   /// it is created using the `FormInputValidator.pure` constructor to | ||||
|   /// signify that the user has not modified it. | ||||
|   /// | ||||
|   /// For subsequent changes (in response to user input), the | ||||
|   /// `FormInputValidator.dirty` constructor should be used to signify that | ||||
|   /// the `FormInputValidator` has been manipulated. | ||||
|   final bool pure; | ||||
| 
 | ||||
|   /// The [FormInputStatus] which can be one of the following: | ||||
|   /// * [FormInputStatus.pure] | ||||
|   ///   - if the input has not been modified. | ||||
|   /// * [FormInputStatus.invalid] | ||||
|   ///   - if the input has been modified and validation failed. | ||||
|   /// * [FormInputStatus.valid] | ||||
|   ///   - if the input has been modified and validation succeeded. | ||||
|   FormInputStatus get status => pure | ||||
|       ? FormInputStatus.pure | ||||
|       : valid | ||||
|           ? FormInputStatus.valid | ||||
|           : FormInputStatus.invalid; | ||||
| 
 | ||||
|   /// Returns a validation error if the [FormInputValidator] is invalid. | ||||
|   /// Returns `null` if the [FormInputValidator] is valid. | ||||
|   E? get error => validator(value); | ||||
| 
 | ||||
|   /// Whether the [FormInputValidator] value is valid according to the | ||||
|   /// overridden `validator`. | ||||
|   /// | ||||
|   /// Returns `true` if `validator` returns `null` for the | ||||
|   /// current [FormInputValidator] value and `false` otherwise. | ||||
|   bool get valid => validator(value) == null; | ||||
| 
 | ||||
|   /// Whether the [FormInputValidator] value is not valid. | ||||
|   /// A value is invalid when the overridden `validator` | ||||
|   /// returns an error (non-null value). | ||||
|   bool get invalid => status == FormInputStatus.invalid; | ||||
| 
 | ||||
|   /// A function that must return a validation error if the provided | ||||
|   /// [value] is invalid and `null` otherwise. | ||||
|   E? validator(V value); | ||||
| 
 | ||||
|   @override | ||||
|   bool? get stringify => true; | ||||
| 
 | ||||
|   @override | ||||
|   List<Object?> get props => [value, pure]; | ||||
| } | ||||
| @ -1,25 +0,0 @@ | ||||
| // Copyright (C) 2022 WYATT GROUP | ||||
| // Please see the AUTHORS file for details. | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU General Public License as published by | ||||
| // the Free Software Foundation, either version 3 of the License, or | ||||
| // any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
| // GNU General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU General Public License | ||||
| // along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| bool listEquals<T>(List<T>? a, List<T>? b) { | ||||
|   if (a == null) return b == null; | ||||
|   if (b == null || a.length != b.length) return false; | ||||
|   if (identical(a, b)) return true; | ||||
|   for (int index = 0; index < a.length; index += 1) { | ||||
|     if (a[index] != b[index]) return false; | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
| @ -20,7 +20,7 @@ import 'package:wyatt_form_bloc/src/form/form.dart'; | ||||
| /// {@template boolean} | ||||
| /// Form input for a bool input | ||||
| /// {@endtemplate} | ||||
| class Boolean extends FormInput<bool, ValidationError> { | ||||
| class Boolean extends FormInputValidator<bool, ValidationError> { | ||||
|   /// {@macro boolean} | ||||
|   const Boolean.pure({bool? defaultValue = false}) | ||||
|       : super.pure(defaultValue ?? false); | ||||
|  | ||||
| @ -21,7 +21,7 @@ import 'package:wyatt_form_bloc/src/form/form.dart'; | ||||
| /// Form input for a confirmed password input. | ||||
| /// {@endtemplate} | ||||
| class ConfirmedPassword | ||||
|     extends FormInput<String, ValidationError> { | ||||
|     extends FormInputValidator<String, ValidationError> { | ||||
|   /// {@macro confirmed_password} | ||||
|   const ConfirmedPassword.pure({this.password = ''}) : super.pure(''); | ||||
| 
 | ||||
|  | ||||
| @ -20,7 +20,7 @@ import 'package:wyatt_form_bloc/src/form/form.dart'; | ||||
| /// {@template email} | ||||
| /// Form input for an email input. | ||||
| /// {@endtemplate} | ||||
| class Email extends FormInput<String, ValidationError> { | ||||
| class Email extends FormInputValidator<String, ValidationError> { | ||||
|   /// {@macro email} | ||||
|   const Email.pure() : super.pure(''); | ||||
| 
 | ||||
| @ -28,7 +28,7 @@ class Email extends FormInput<String, ValidationError> { | ||||
|   const Email.dirty([String value = '']) : super.dirty(value); | ||||
| 
 | ||||
|   static final RegExp _emailRegExp = RegExp( | ||||
|     r'^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$', | ||||
|     r'[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+', | ||||
|   ); | ||||
| 
 | ||||
|   @override | ||||
|  | ||||
| @ -20,7 +20,7 @@ import 'package:wyatt_form_bloc/src/form/form.dart'; | ||||
| /// {@template iban} | ||||
| /// Form input for an IBAN input. | ||||
| /// {@endtemplate} | ||||
| class Iban extends FormInput<String, ValidationError> { | ||||
| class Iban extends FormInputValidator<String, ValidationError> { | ||||
|   /// {@macro iban} | ||||
|   const Iban.pure() : super.pure(''); | ||||
| 
 | ||||
|  | ||||
| @ -20,7 +20,7 @@ import 'package:wyatt_form_bloc/src/form/form.dart'; | ||||
| /// {@template list_option} | ||||
| /// Form input for a list input | ||||
| /// {@endtemplate} | ||||
| class ListOption<T> extends FormInput<List<T>, ValidationError> { | ||||
| class ListOption<T> extends FormInputValidator<List<T>, ValidationError> { | ||||
|   /// {@macro list_option} | ||||
|   const ListOption.pure({List<T>? defaultValue}) | ||||
|       : super.pure(defaultValue ?? const []); | ||||
|  | ||||
| @ -20,7 +20,7 @@ import 'package:wyatt_form_bloc/src/form/form.dart'; | ||||
| /// {@template name} | ||||
| /// Form input for a name input. | ||||
| /// {@endtemplate} | ||||
| class Name extends FormInput<String, ValidationError> { | ||||
| class Name extends FormInputValidator<String, ValidationError> { | ||||
|   /// {@macro name} | ||||
|   const Name.pure() : super.pure(''); | ||||
| 
 | ||||
|  | ||||
| @ -20,7 +20,7 @@ import 'package:wyatt_form_bloc/src/form/form.dart'; | ||||
| /// {@template password} | ||||
| /// Form input for a password input. | ||||
| /// {@endtemplate} | ||||
| class Password extends FormInput<String, ValidationError> { | ||||
| class Password extends FormInputValidator<String, ValidationError> { | ||||
|   /// {@macro password} | ||||
|   const Password.pure() : super.pure(''); | ||||
| 
 | ||||
|  | ||||
| @ -20,7 +20,7 @@ import 'package:wyatt_form_bloc/src/form/form.dart'; | ||||
| /// {@template phone} | ||||
| /// Form input for a phone input. | ||||
| /// {@endtemplate} | ||||
| class Phone extends FormInput<String, ValidationError> { | ||||
| class Phone extends FormInputValidator<String, ValidationError> { | ||||
|   /// {@macro phone} | ||||
|   const Phone.pure() : super.pure(''); | ||||
| 
 | ||||
|  | ||||
| @ -20,7 +20,7 @@ import 'package:wyatt_form_bloc/src/form/form.dart'; | ||||
| /// {@template siren} | ||||
| /// Form input for a SIREN input. | ||||
| /// {@endtemplate} | ||||
| class Siren extends FormInput<String, ValidationError> { | ||||
| class Siren extends FormInputValidator<String, ValidationError> { | ||||
|   /// {@macro siren} | ||||
|   const Siren.pure() : super.pure(''); | ||||
| 
 | ||||
|  | ||||
| @ -20,7 +20,7 @@ import 'package:wyatt_form_bloc/src/form/form.dart'; | ||||
| /// {@template text_string} | ||||
| /// Form input for a text input | ||||
| /// {@endtemplate} | ||||
| class TextString extends FormInput<String, ValidationError> { | ||||
| class TextString extends FormInputValidator<String, ValidationError> { | ||||
|   /// {@macro text_string} | ||||
|   const TextString.pure() : super.pure(''); | ||||
| 
 | ||||
|  | ||||
| @ -4,14 +4,15 @@ repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/mas | ||||
| version: 0.0.2 | ||||
| 
 | ||||
| environment: | ||||
|   sdk: '>=2.16.2 <3.0.0' | ||||
|   sdk: '>=2.17.2 <3.0.0' | ||||
| 
 | ||||
| dependencies: | ||||
|   bloc: ^8.0.3 | ||||
|   equatable: ^2.0.3 | ||||
|   meta: ^1.7.0 | ||||
| 
 | ||||
| dev_dependencies: | ||||
|   test: ^1.21.0 | ||||
|   test: ^1.21.4 | ||||
| 
 | ||||
|   wyatt_analysis: | ||||
|     git: | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user