Compare commits

..
Author SHA1 Message Date
hugo f558685f11 chore(release): publish packages
- form_bloc_example@1.1.0
 - wyatt_form_bloc@0.0.3
 - authentication_bloc_example@1.0.1
 - wyatt_authentication_bloc@0.2.1+1
2022-07-10 22:12:37 +02:00
hugo b21aa04557 fix(auth): update form plugin and fix auth 2022-07-10 22:11:16 +02:00
hugo 01df7a0dff fix: ignore google services 2022-07-10 22:10:40 +02:00
hugo 8909274543 feat(form): add helpers 2022-07-10 22:10:12 +02:00
hugo 89f94cf1ff Merge pull request 'form/feature/add_metadata' (#10) from form/feature/add_metadata into master
Reviewed-on: #10
2022-07-10 19:23:54 +00:00
hugo 65774010aa Merge branch 'master' into form/feature/add_metadata 2022-07-10 19:23:40 +00:00
hugo 42810c4fb3 Merge pull request 'chore(deps): update dependency flutter_lints to v2' (#4) from renovate/flutter_lints-2.x into master
Reviewed-on: #4
2022-07-10 19:23:17 +00:00
hugo 39d19b4c7b Merge branch 'master' into renovate/flutter_lints-2.x 2022-07-10 19:23:12 +00:00
hugo 8a0af954bf doc: remove deprecated FormEntry 2022-07-10 21:15:33 +02:00
hugo 94b0ff01ad doc: update readme with usage 2022-07-10 21:13:52 +02:00
hugo 741f05e1be doc: update example 2022-07-10 21:13:43 +02:00
hugo d0f1d07363 feat: add metadata on inputs 2022-07-10 21:13:13 +02:00
hugo 08ac6d8fda feat: update to latest dart version 2022-07-10 21:12:55 +02:00
Renovate 83b75ee0e5 chore(deps): update dependency flutter_lints to v2 2022-06-01 14:25:58 +00:00
52 changed files with 942 additions and 508 deletions
+1
View File
@@ -196,3 +196,4 @@ $RECYCLE.BIN/
.idea/
*.iml
google-services.json
+38
View File
@@ -3,6 +3,44 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## 2022-07-10
### Changes
---
Packages with breaking changes:
- There are no breaking changes in this release.
Packages with other changes:
- [`form_bloc_example` - `v1.1.0`](#form_bloc_example---v110)
- [`wyatt_form_bloc` - `v0.0.3`](#wyatt_form_bloc---v003)
- [`authentication_bloc_example` - `v1.0.1`](#authentication_bloc_example---v101)
- [`wyatt_authentication_bloc` - `v0.2.1+1`](#wyatt_authentication_bloc---v0211)
Packages with dependency updates only:
> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project.
- `authentication_bloc_example` - `v1.0.1`
- `wyatt_authentication_bloc` - `v0.2.1+1`
---
#### `form_bloc_example` - `v1.1.0`
- **FEAT**: add list option validator.
- **DOCS**: add example app.
#### `wyatt_form_bloc` - `v0.0.3`
- **FEAT**: add helpers.
- **FEAT**: add metadata on inputs.
- **FEAT**: update to latest dart version.
## 2022-06-24
### Changes
@@ -1,3 +1,7 @@
## 0.2.1+1
- Update a dependency to the latest release.
## 0.2.1
- **FEAT**: add google, facebook, apple, twitter login.
@@ -0,0 +1,4 @@
## 1.0.1
- Update a dependency to the latest release.
@@ -31,23 +31,29 @@ class App extends StatelessWidget {
static FormData getNormalFormData() {
return const FormData([
FormEntry(formFieldName, Name.pure()),
FormEntry(formFieldPhone, Phone.pure()),
FormEntry(formFieldPro, Boolean.pure()),
FormEntry(formFieldConfirmedPassword, ConfirmedPassword.pure(),
export: false),
FormInput(formFieldName, Name.pure()),
FormInput(formFieldPhone, Phone.pure()),
FormInput(formFieldPro, Boolean.pure()),
FormInput(
formFieldConfirmedPassword,
ConfirmedPassword.pure(),
metadata: FormInputMetadata(export: false),
),
]);
}
static FormData getProFormData() {
return const FormData([
FormEntry(formFieldName, Name.pure()),
FormEntry(formFieldPhone, Phone.pure()),
FormEntry(formFieldPro, Boolean.pure()),
FormEntry(formFieldSiren, Siren.pure()),
FormEntry(formFieldIban, Iban.pure()),
FormEntry(formFieldConfirmedPassword, ConfirmedPassword.pure(),
export: false),
FormInput(formFieldName, Name.pure()),
FormInput(formFieldPhone, Phone.pure()),
FormInput(formFieldPro, Boolean.pure()),
FormInput(formFieldSiren, Siren.pure()),
FormInput(formFieldIban, Iban.pure()),
FormInput(
formFieldConfirmedPassword,
ConfirmedPassword.pure(),
metadata: FormInputMetadata(export: false),
),
]);
}
@@ -103,42 +109,42 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
AuthenticationRepositoryInterface _authenticationRepository =
AuthenticationRepositoryInterface authenticationRepository =
AuthenticationRepositoryFirebase();
AuthenticationCubit _authenticationCubit = AuthenticationCubit(
authenticationRepository: _authenticationRepository,
AuthenticationCubit authenticationCubit = AuthenticationCubit(
authenticationRepository: authenticationRepository,
onAuthSuccess: onAuthSuccess,
);
SignUpCubit _signUpCubit = SignUpCubit(
authenticationRepository: _authenticationRepository,
authenticationCubit: _authenticationCubit,
SignUpCubit signUpCubit = SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: getNormalFormData(),
onSignUpSuccess: onSignUpSuccess,
);
SignInCubit _signInCubit = SignInCubit(
authenticationRepository: _authenticationRepository,
authenticationCubit: _authenticationCubit,
SignInCubit signInCubit = SignInCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
);
return MultiRepositoryProvider(
providers: [
RepositoryProvider<AuthenticationRepositoryInterface>(
create: (context) => _authenticationRepository,
create: (context) => authenticationRepository,
),
],
child: MultiBlocProvider(
providers: [
BlocProvider<AuthenticationCubit>(
create: (context) => _authenticationCubit..init(),
create: (context) => authenticationCubit..init(),
),
BlocProvider<SignUpCubit>(
create: (context) => _signUpCubit,
create: (context) => signUpCubit,
),
BlocProvider<SignInCubit>(
create: (context) => _signInCubit,
create: (context) => signInCubit,
),
],
child: const AppView(),
@@ -37,7 +37,7 @@ class _NameInput extends StatelessWidget {
labelText: 'name',
helperText: '',
errorText:
state.data.input(formFieldName).invalid ? 'invalid name' : null,
state.data.isNotValid(formFieldName) ? 'invalid name' : null,
),
);
},
@@ -58,7 +58,7 @@ class _PhoneInput extends StatelessWidget {
decoration: InputDecoration(
labelText: 'phone',
helperText: '',
errorText: state.data.input(formFieldPhone).invalid
errorText: state.data.isNotValid(formFieldPhone)
? 'invalid phone'
: null,
),
@@ -81,7 +81,7 @@ class _SirenInput extends StatelessWidget {
decoration: InputDecoration(
labelText: 'siren',
helperText: '',
errorText: state.data.input(formFieldSiren).invalid
errorText: state.data.isNotValid(formFieldSiren)
? 'invalid SIREN'
: null,
),
@@ -105,7 +105,7 @@ class _IbanInput extends StatelessWidget {
labelText: 'iban',
helperText: '',
errorText:
state.data.input(formFieldIban).invalid ? 'invalid IBAN' : null,
state.data.isNotValid(formFieldIban) ? 'invalid IBAN' : null,
),
);
},
@@ -149,8 +149,7 @@ class _PasswordInput extends StatelessWidget {
.read<SignUpCubit>()
.state
.data
.input(formFieldConfirmedPassword)
.value,
.valueOf<String, ValidationError>(formFieldConfirmedPassword),
),
);
},
@@ -185,7 +184,7 @@ class _ConfirmPasswordInput extends StatelessWidget {
decoration: InputDecoration(
labelText: 'confirm password',
helperText: '',
errorText: state.data.input(formFieldConfirmedPassword).invalid
errorText: state.data.isNotValid(formFieldConfirmedPassword)
? 'passwords do not match'
: null,
),
@@ -203,7 +202,7 @@ class _CheckIsProInput extends StatelessWidget {
trailing: BlocBuilder<SignUpCubit, SignUpState>(
builder: (context, state) {
return Checkbox(
value: state.data.input<bool>(formFieldPro).value,
value: state.data.valueOf<bool, ValidationError>(formFieldPro),
onChanged: (isPro) {
final value =
isPro!; // tristate is false, so value can't be null
@@ -297,7 +296,7 @@ class SignUpForm extends StatelessWidget {
const SizedBox(height: 8),
BlocBuilder<SignUpCubit, SignUpState>(
builder: (context, state) {
if (state.data.input<bool>(formFieldPro).value) {
if (state.data.valueOf<bool, ValidationError>(formFieldPro)) {
return Column(children: [
_SirenInput(),
const SizedBox(height: 8),
@@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1
version: 1.0.1
environment:
sdk: ">=2.16.2 <3.0.0"
@@ -38,7 +38,7 @@ dependencies:
wyatt_form_bloc:
git:
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages
ref: wyatt_form_bloc-v0.0.2
ref: wyatt_form_bloc-v0.0.3
path: packages/wyatt_form_bloc
# The following adds the Cupertino Icons font to your application.
@@ -55,7 +55,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^1.0.0
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@@ -33,7 +33,7 @@ class PasswordResetCubit extends Cubit<PasswordResetState> {
emit(
state.copyWith(
email: email,
status: FormData.validate([email]),
status: FormStatus.validate([email]),
),
);
}
@@ -39,7 +39,7 @@ class SignInCubit extends Cubit<SignInState> {
emit(
state.copyWith(
email: email,
status: FormData.validate([email, state.password]),
status: FormStatus.validate([email, state.password]),
),
);
}
@@ -49,7 +49,7 @@ class SignInCubit extends Cubit<SignInState> {
emit(
state.copyWith(
password: password,
status: FormData.validate([state.email, password]),
status: FormStatus.validate([state.email, password]),
),
);
}
@@ -44,16 +44,16 @@ class SignUpCubit extends Cubit<SignUpState> {
void emailChanged(String value) {
final Email email = Email.dirty(value);
final List<FormInput<dynamic, ValidationError>> inputsToValidate = [
final List<FormInputValidator<dynamic, ValidationError>> toValidate = [
email,
state.password,
...state.data.inputs<dynamic>(),
...state.data.validators<dynamic, ValidationError>(),
];
emit(
state.copyWith(
email: email,
status: FormData.validate(inputsToValidate),
status: FormStatus.validate(toValidate),
),
);
}
@@ -61,26 +61,26 @@ class SignUpCubit extends Cubit<SignUpState> {
void passwordChanged(String value) {
final Password password = Password.dirty(value);
final List<FormInput<dynamic, ValidationError>> inputsToValidate = [
final List<FormInputValidator<dynamic, ValidationError>> toValidate = [
state.email,
password,
...state.data.inputs<dynamic>(),
...state.data.validators<dynamic, ValidationError>(),
];
emit(
state.copyWith(
password: password,
status: FormData.validate(inputsToValidate),
status: FormStatus.validate(toValidate),
),
);
}
// Take from wyatt_form_bloc/wyatt_form_bloc.dart
void dataChanged<T>(String field, FormInput dirtyValue) {
void dataChanged<T>(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');
}
@@ -88,8 +88,12 @@ class SignUpCubit extends Cubit<SignUpState> {
emit(
state.copyWith(
data: _form,
status: FormData.validate(
[state.email, state.password, ..._form.inputs<dynamic>()],
status: FormStatus.validate(
[
state.email,
state.password,
...state.data.validators<dynamic, ValidationError>(),
],
),
),
);
@@ -120,8 +124,12 @@ class SignUpCubit extends Cubit<SignUpState> {
emit(
state.copyWith(
data: _form,
status: FormData.validate(
[state.email, state.password, ..._form.inputs<dynamic>()],
status: FormStatus.validate(
[
state.email,
state.password,
...state.data.validators<dynamic, ValidationError>(),
],
),
),
);
@@ -1,7 +1,7 @@
name: wyatt_authentication_bloc
description: Authentication BLoC for Flutter
repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_authentication_bloc
version: 0.2.1
version: 0.2.1+1
environment:
sdk: ">=2.15.1 <3.0.0"
@@ -23,7 +23,7 @@ dependencies:
wyatt_form_bloc:
git:
url: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages
ref: wyatt_form_bloc-v0.0.2
ref: wyatt_form_bloc-v0.0.3
path: packages/wyatt_form_bloc
@@ -68,9 +68,9 @@ void main() {
SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: FormData.empty(),
entries: const FormData.empty(),
).state,
SignUpState(data: FormData.empty()),
const SignUpState(data: FormData.empty()),
);
});
@@ -80,11 +80,11 @@ void main() {
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: FormData.empty(),
entries: const FormData.empty(),
),
act: (SignUpCubit cubit) => cubit.emailChanged(invalidEmailString),
expect: () => <SignUpState>[
SignUpState(
const SignUpState(
email: invalidEmail,
status: FormStatus.invalid,
data: FormData.empty(),
@@ -97,15 +97,15 @@ void main() {
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: FormData.empty(),
entries: const FormData.empty(),
),
seed: () => SignUpState(
seed: () => const SignUpState(
password: validPassword,
data: FormData.empty(),
),
act: (SignUpCubit cubit) => cubit.emailChanged(validEmailString),
expect: () => <SignUpState>[
SignUpState(
const SignUpState(
email: validEmail,
password: validPassword,
status: FormStatus.valid,
@@ -121,12 +121,12 @@ void main() {
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: FormData.empty(),
entries: const FormData.empty(),
),
act: (SignUpCubit cubit) =>
cubit.passwordChanged(invalidPasswordString),
expect: () => <SignUpState>[
SignUpState(
const SignUpState(
password: invalidPassword,
status: FormStatus.invalid,
data: FormData.empty(),
@@ -139,15 +139,15 @@ void main() {
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: FormData.empty(),
entries: const FormData.empty(),
),
seed: () => SignUpState(
seed: () => const SignUpState(
email: validEmail,
data: FormData.empty(),
),
act: (SignUpCubit cubit) => cubit.passwordChanged(validPasswordString),
expect: () => <SignUpState>[
SignUpState(
const SignUpState(
email: validEmail,
password: validPassword,
status: FormStatus.valid,
@@ -163,7 +163,7 @@ void main() {
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: FormData.empty(),
entries: const FormData.empty(),
),
act: (SignUpCubit cubit) => cubit.signUpFormSubmitted(),
expect: () => const <SignUpState>[],
@@ -174,9 +174,9 @@ void main() {
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: FormData.empty(),
entries: const FormData.empty(),
),
seed: () => SignUpState(
seed: () => const SignUpState(
status: FormStatus.valid,
email: validEmail,
password: validPassword,
@@ -199,9 +199,9 @@ void main() {
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: FormData.empty(),
entries: const FormData.empty(),
),
seed: () => SignUpState(
seed: () => const SignUpState(
status: FormStatus.valid,
email: validEmail,
password: validPassword,
@@ -209,13 +209,13 @@ void main() {
),
act: (SignUpCubit cubit) => cubit.signUpFormSubmitted(),
expect: () => <SignUpState>[
SignUpState(
const SignUpState(
status: FormStatus.submissionInProgress,
email: validEmail,
password: validPassword,
data: FormData.empty(),
),
SignUpState(
const SignUpState(
status: FormStatus.submissionSuccess,
email: validEmail,
password: validPassword,
@@ -238,9 +238,9 @@ void main() {
build: () => SignUpCubit(
authenticationRepository: authenticationRepository,
authenticationCubit: authenticationCubit,
entries: FormData.empty(),
entries: const FormData.empty(),
),
seed: () => SignUpState(
seed: () => const SignUpState(
status: FormStatus.valid,
email: validEmail,
password: validPassword,
@@ -248,13 +248,13 @@ void main() {
),
act: (SignUpCubit cubit) => cubit.signUpFormSubmitted(),
expect: () => <SignUpState>[
SignUpState(
const SignUpState(
status: FormStatus.submissionInProgress,
email: validEmail,
password: validPassword,
data: FormData.empty(),
),
SignUpState(
const SignUpState(
status: FormStatus.submissionFailure,
email: validEmail,
password: validPassword,
@@ -26,10 +26,10 @@ void main() {
group('SignUpState', () {
test('supports value comparisons', () {
expect(
SignUpState(
const SignUpState(
data: FormData.empty(),
),
SignUpState(
const SignUpState(
data: FormData.empty(),
),
);
@@ -37,10 +37,10 @@ void main() {
test('returns same object when no properties are passed', () {
expect(
SignUpState(
const SignUpState(
data: FormData.empty(),
).copyWith(),
SignUpState(
const SignUpState(
data: FormData.empty(),
),
);
@@ -48,10 +48,10 @@ void main() {
test('returns object with updated status when status is passed', () {
expect(
SignUpState(
const SignUpState(
data: FormData.empty(),
).copyWith(status: FormStatus.pure),
SignUpState(
const SignUpState(
data: FormData.empty(),
),
);
@@ -59,10 +59,10 @@ void main() {
test('returns object with updated email when email is passed', () {
expect(
SignUpState(
const SignUpState(
data: FormData.empty(),
).copyWith(email: email),
SignUpState(
const SignUpState(
email: email,
data: FormData.empty(),
),
@@ -71,10 +71,10 @@ void main() {
test('returns object with updated password when password is passed', () {
expect(
SignUpState(
const SignUpState(
data: FormData.empty(),
).copyWith(password: password),
SignUpState(
const SignUpState(
password: password,
data: FormData.empty(),
),
@@ -85,12 +85,12 @@ void main() {
'returns object with updated data'
' when data is passed', () {
expect(
SignUpState(
const SignUpState(
data: FormData.empty(),
).copyWith(
data: const FormData(
[
FormEntry(
FormInput(
'field',
Name.pure(),
),
@@ -100,7 +100,7 @@ void main() {
const SignUpState(
data: FormData(
[
FormEntry(
FormInput(
'field',
Name.pure(),
),
@@ -49,7 +49,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^1.0.0
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
+6
View File
@@ -1,3 +1,9 @@
## 0.0.3
- **FEAT**: add helpers.
- **FEAT**: add metadata on inputs.
- **FEAT**: update to latest dart version.
## 0.0.2
- **FEAT**: add list option validator.
+126 -9
View File
@@ -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.
+22 -2
View File
@@ -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'
@@ -0,0 +1,5 @@
## 1.1.0
- **FEAT**: add list option validator.
- **DOCS**: add example app.
@@ -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,13 +16,10 @@
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() {
@@ -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),
@@ -15,10 +15,10 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1
version: 1.1.0
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
@@ -47,7 +47,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^1.0.0
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -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>
@@ -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,9 +17,14 @@
part of 'form_data_cubit.dart';
@immutable
class FormDataState {
class FormDataState extends Equatable {
/// Global status of a form.
final FormStatus status;
/// FormData with all inputs, and associated metadata.
final FormData data;
/// Optional error message.
final String? errorMessage;
const FormDataState({
@@ -41,23 +46,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,23 @@ extension FormStatusX on FormStatus {
/// Indicates whether the form submission has been canceled.
bool get isSubmissionCanceled => this == FormStatus.submissionCanceled;
/// Validate a list of inputs by processing them in `validate` as validators.
static FormStatus validateInputs(List<FormInput> inputs) {
return validate(
inputs
.map<FormInputValidator>((FormInput input) => input.validator)
.toList(),
);
}
/// Validate a list of validators.
static FormStatus validate(List<FormInputValidator> validators) {
return validators.every((FormInputValidator validator) => validator.pure)
? FormStatus.pure
: validators
.any((FormInputValidator validator) => validator.valid == false)
? FormStatus.invalid
: FormStatus.valid;
}
}
@@ -14,6 +14,12 @@
// 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,185 @@
// 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;
/// Returns all inputs as a list
List<FormInput> inputs() => _inputs;
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 inputOf(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(
inputOf(key),
);
_entries[index] = _entries[index].copyWith(input: input);
_inputs[index] = input;
}
}
/// Returns all associated validators as a list
List<FormInputValidator<V, E>> validators<V, E>() {
return _inputs
.map<FormInputValidator<V, E>>(
(FormInput input) => input.validator as FormInputValidator<V, E>,
)
.toList();
}
/// 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 inputOf(key).validator as T;
}
/// Updates validator of a given input. (perform copyWith)
void updateValidator(String key, FormInputValidator dirtyValue) {
if (contains(key)) {
final index = _inputs.indexOf(
inputOf(key),
);
_inputs[index] = _inputs[index].copyWith(validator: dirtyValue);
}
}
/// Returns a validation error if the [FormInputValidator] is invalid.
/// Returns null if the [FormInputValidator] is valid.
E? errorOf<V, E>(String key) {
return (inputOf(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 (inputOf(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 !inputOf(key).validator.valid;
}
/// Returns all associated metadata as a list
List<FormInputMetadata<M>> metadata<M>() {
return _inputs
.map<FormInputMetadata<M>>(
(FormInput input) => input.metadata as FormInputMetadata<M>,
)
.toList();
}
/// Returns the metadata associated. With `M` the type of extra data.
FormInputMetadata<M> metadataOf<M>(String key) {
return inputOf(key).metadata as FormInputMetadata<M>;
}
/// Updates metadata of a given input. (perform copyWith)
void updateMetadata(String key, FormInputMetadata meta) {
if (contains(key)) {
final index = _inputs.indexOf(
inputOf(key),
);
_inputs[index] = _inputs[index].copyWith(metadata: meta);
}
}
/// Validate self inputs.
FormStatus validate() {
return FormStatus.validateInputs(_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,
);
}
}
@@ -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 -3
View File
@@ -1,17 +1,18 @@
name: wyatt_form_bloc
description: Manage forms in Dart & Flutter with Bloc
repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_form_bloc
version: 0.0.2
version: 0.0.3
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:
@@ -48,7 +48,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^1.0.0
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec