feat(from): add form validator class with strategy design pattern #12

Merged
hugo merged 1 commits from form/feature/form_validator_class into master 2022-07-15 16:24:33 +00:00
29 changed files with 454 additions and 256 deletions
Showing only changes of commit 607b986848 - Show all commits

View File

@ -1 +1,4 @@
include: package:wyatt_analysis/analysis_options.yaml include: package:wyatt_analysis/analysis_options.yaml
analyzer:
exclude: "!example/**"

View File

@ -61,10 +61,10 @@ class App extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
FormDataCubit _formCubit = CustomFormCubit(inputs: getNormalFormData()); FormDataCubit formCubit = CustomFormCubit(inputs: getNormalFormData());
return BlocProvider( return BlocProvider(
create: (context) => _formCubit, create: (context) => formCubit,
child: const WidgetTree(), child: const WidgetTree(),
); );
} }

View File

@ -187,9 +187,9 @@ class _CheckListInput extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<FormDataCubit, FormDataState>( return BlocBuilder<FormDataCubit, FormDataState>(
builder: (context, state) { builder: (context, state) {
final _input = final input =
state.data.validatorOf<ListOption<String>>(formFieldList); state.data.validatorOf<ListOption<String>>(formFieldList);
final _options = _input.value; final options = input.value;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -197,33 +197,33 @@ class _CheckListInput extends StatelessWidget {
ListTile( ListTile(
title: const Text('Checkbox1'), title: const Text('Checkbox1'),
trailing: Checkbox( trailing: Checkbox(
value: _options.contains('checkbox1'), value: options.contains('checkbox1'),
onChanged: (_) { onChanged: (_) {
context.read<FormDataCubit>().dataChanged( context.read<FormDataCubit>().dataChanged(
formFieldList, formFieldList,
_input.select('checkbox1'), input.select('checkbox1'),
); );
}), }),
), ),
ListTile( ListTile(
title: const Text('Checkbox2'), title: const Text('Checkbox2'),
trailing: Checkbox( trailing: Checkbox(
value: _options.contains('checkbox2'), value: options.contains('checkbox2'),
onChanged: (_) { onChanged: (_) {
context.read<FormDataCubit>().dataChanged( context.read<FormDataCubit>().dataChanged(
formFieldList, formFieldList,
_input.select('checkbox2'), input.select('checkbox2'),
); );
}), }),
), ),
ListTile( ListTile(
title: const Text('Checkbox3 (default)'), title: const Text('Checkbox3 (default)'),
trailing: Checkbox( trailing: Checkbox(
value: _options.contains('checkbox3'), value: options.contains('checkbox3'),
onChanged: (_) { onChanged: (_) {
context.read<FormDataCubit>().dataChanged( context.read<FormDataCubit>().dataChanged(
formFieldList, formFieldList,
_input.select('checkbox3'), input.select('checkbox3'),
); );
}), }),
), ),
@ -239,7 +239,7 @@ class _RadioListInput extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<FormDataCubit, FormDataState>( return BlocBuilder<FormDataCubit, FormDataState>(
builder: (context, state) { builder: (context, state) {
final _input = state.data.validatorOf<TextString>(formFieldRadio); final input = state.data.validatorOf<TextString>(formFieldRadio);
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -248,7 +248,7 @@ class _RadioListInput extends StatelessWidget {
title: const Text('Radio1'), title: const Text('Radio1'),
trailing: Radio<bool>( trailing: Radio<bool>(
groupValue: true, groupValue: true,
value: _input.value == 'radio1', value: input.value == 'radio1',
onChanged: (_) { onChanged: (_) {
context.read<FormDataCubit>().dataChanged( context.read<FormDataCubit>().dataChanged(
formFieldRadio, formFieldRadio,
@ -260,7 +260,7 @@ class _RadioListInput extends StatelessWidget {
title: const Text('Radio2'), title: const Text('Radio2'),
trailing: Radio<bool>( trailing: Radio<bool>(
groupValue: true, groupValue: true,
value: _input.value == 'radio2', value: input.value == 'radio2',
onChanged: (_) { onChanged: (_) {
context.read<FormDataCubit>().dataChanged( context.read<FormDataCubit>().dataChanged(
formFieldRadio, formFieldRadio,
@ -272,7 +272,7 @@ class _RadioListInput extends StatelessWidget {
title: const Text('Radio3'), title: const Text('Radio3'),
trailing: Radio<bool>( trailing: Radio<bool>(
groupValue: true, groupValue: true,
value: _input.value == 'radio3', value: input.value == 'radio3',
onChanged: (_) { onChanged: (_) {
context.read<FormDataCubit>().dataChanged( context.read<FormDataCubit>().dataChanged(
formFieldRadio, formFieldRadio,
@ -378,6 +378,20 @@ class _DebugButton extends StatelessWidget {
} }
} }
class _ResetButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<FormDataCubit, FormDataState>(
builder: (context, state) {
return ElevatedButton(
onPressed: () => context.read<FormDataCubit>().resetForm(),
child: const Text('RESET'),
);
},
);
}
}
class SignUpForm extends StatelessWidget { class SignUpForm extends StatelessWidget {
const SignUpForm({Key? key}) : super(key: key); const SignUpForm({Key? key}) : super(key: key);
@ -431,6 +445,8 @@ class SignUpForm extends StatelessWidget {
_SignUpButton(), _SignUpButton(),
const SizedBox(height: 8), const SizedBox(height: 8),
_DebugButton(), _DebugButton(),
const SizedBox(height: 8),
_ResetButton(),
], ],
), ),
), ),

View File

@ -18,38 +18,54 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.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/enums/enums.dart';
import 'package:wyatt_form_bloc/src/form/form.dart'; import 'package:wyatt_form_bloc/src/form/form.dart';
import 'package:wyatt_form_bloc/src/validators/form/every_input_validator.dart';
part 'form_data_state.dart'; part 'form_data_state.dart';
abstract class FormDataCubit extends Cubit<FormDataState> { abstract class FormDataCubit extends Cubit<FormDataState> {
FormDataCubit({required FormData inputs}) FormValidator validationStrategy;
: super(FormDataState(data: inputs)); FormData formCopy;
FormDataCubit({
required FormData inputs,
FormValidator validator = const EveryInputValidator(),
}) : formCopy = inputs.clone(),
validationStrategy = validator,
super(FormDataState(data: inputs));
/// Change value of a field. /// Change value of a field.
/// ///
/// Inputs: /// Inputs:
/// - `field`: The key of the field to change. /// - `field`: The key of the field to change.
/// - `dirtyValue`: The new value of the field. (Wrapped in a dirty validator) /// - `dirtyValue`: The new value of the field. (Wrapped in a dirty validator)
void dataChanged(String field, FormInputValidator dirtyValue) { void dataChanged<V>(
final _form = state.data.clone(); String field,
FormInputValidator<V, ValidationError> dirtyValue,
) {
final form = state.data.clone();
if (_form.contains(field)) { if (form.contains(field)) {
_form.updateValidator(field, dirtyValue); form.updateValidator(field, dirtyValue);
} else { } else {
throw Exception('Form field $field not found'); throw Exception('Form field $field not found');
} }
emit( emit(
state.copyWith( state.copyWith(
data: _form, data: form,
status: _form.validate(), status: validationStrategy.validate(form),
), ),
); );
} }
/// Just validate the form manually. (Useful if you just update the strategy)
void validate() {
final form = state.data;
emit(state.copyWith(status: validationStrategy.validate(form)));
}
/// Update entries list. /// Update entries list.
/// ///
/// Inputs: /// Inputs:
@ -59,31 +75,36 @@ abstract class FormDataCubit extends Cubit<FormDataState> {
FormData data, { FormData data, {
SetOperation operation = SetOperation.replace, SetOperation operation = SetOperation.replace,
}) { }) {
FormData _form = data; FormData form = data;
switch (operation) { switch (operation) {
case SetOperation.replace: case SetOperation.replace:
_form = data; form = data;
break; break;
case SetOperation.difference: case SetOperation.difference:
_form = state.data.difference(data); form = state.data.difference(data);
break; break;
case SetOperation.intersection: case SetOperation.intersection:
_form = state.data.intersection(data); form = state.data.intersection(data);
break; break;
case SetOperation.union: case SetOperation.union:
_form = state.data.union(data); form = state.data.union(data);
break; break;
} }
emit( emit(
state.copyWith( state.copyWith(
data: _form, data: form,
status: _form.validate(), status: validationStrategy.validate(form),
), ),
); );
} }
/// Reset all form inputs
void resetForm() {
emit(FormDataState(data: formCopy));
}
/// Submit the form. /// Submit the form.
Future<void> submitForm(); Future<void> submitForm();
} }

View File

@ -16,7 +16,6 @@
part of 'form_data_cubit.dart'; part of 'form_data_cubit.dart';
@immutable
class FormDataState extends Equatable { class FormDataState extends Equatable {
/// Global status of a form. /// Global status of a form.
final FormStatus status; final FormStatus status;
@ -37,13 +36,11 @@ class FormDataState extends Equatable {
FormStatus? status, FormStatus? status,
FormData? data, FormData? data,
String? errorMessage, String? errorMessage,
}) { }) => FormDataState(
return FormDataState(
status: status ?? this.status, status: status ?? this.status,
data: data ?? this.data, data: data ?? this.data,
errorMessage: errorMessage ?? this.errorMessage, errorMessage: errorMessage ?? this.errorMessage,
); );
}
@override @override
bool? get stringify => true; bool? get stringify => true;

View File

@ -14,8 +14,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // 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>{ const Set<FormStatus> _validatedFormStatuses = <FormStatus>{
FormStatus.valid, FormStatus.valid,
FormStatus.submissionInProgress, FormStatus.submissionInProgress,
@ -51,6 +49,8 @@ enum FormStatus {
bool get isPure => this == FormStatus.pure; bool get isPure => this == FormStatus.pure;
/// Indicates whether the form is completely validated. /// Indicates whether the form is completely validated.
/// This means the [FormStatus] is strictly:
/// * `FormStatus.valid`
bool get isValid => this == FormStatus.valid; bool get isValid => this == FormStatus.valid;
/// Indicates whether the form has been validated successfully. /// Indicates whether the form has been validated successfully.
@ -75,23 +75,4 @@ enum FormStatus {
/// Indicates whether the form submission has been canceled. /// Indicates whether the form submission has been canceled.
bool get isSubmissionCanceled => this == FormStatus.submissionCanceled; 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;
}
} }

View File

@ -14,7 +14,14 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
enum ValidationError { // enum ValidationError {
/// Generic invalid error. // /// Generic invalid error.
invalid // invalid
// }
abstract class ValidationError {}
enum ValidationStandardError implements ValidationError {
invalid,
empty
} }

View File

@ -14,12 +14,15 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:convert';
import 'package:equatable/equatable.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_input_status.dart';
import 'package:wyatt_form_bloc/src/enums/form_status.dart'; import 'package:wyatt_form_bloc/src/enums/form_status.dart';
import 'package:wyatt_form_bloc/src/enums/validation_error.dart';
part 'form_data.dart';
part 'form_input.dart'; part 'form_input.dart';
part 'form_input_metadata.dart'; part 'form_input_metadata.dart';
part 'form_input_validator.dart'; part 'form_input_validator.dart';
part 'form_data.dart'; part 'form_validator.dart';

View File

@ -16,7 +16,6 @@
part of 'form.dart'; part of 'form.dart';
@immutable
class FormData extends Equatable { class FormData extends Equatable {
final List<FormInput> _inputs; final List<FormInput> _inputs;
@ -29,7 +28,7 @@ class FormData extends Equatable {
/// Returns the input for the associated key /// Returns the input for the associated key
FormInput inputOf(String key) { FormInput inputOf(String key) {
if (contains(key)) { if (contains(key)) {
return _inputs.firstWhere((FormInput input) => input.key == key); return _inputs.firstWhere((input) => input.key == key);
} else { } else {
throw Exception('FormInput with key `$key` does not exist in form'); throw Exception('FormInput with key `$key` does not exist in form');
} }
@ -46,23 +45,23 @@ class FormData extends Equatable {
} }
/// Returns all associated validators as a list /// Returns all associated validators as a list
List<FormInputValidator<V, E>> validators<V, E>() { List<FormInputValidator<V, E>> validators<V, E extends ValidationError>() =>
return _inputs _inputs
.map<FormInputValidator<V, E>>( .map<FormInputValidator<V, E>>(
(FormInput input) => input.validator as FormInputValidator<V, E>, (input) => input.validator as FormInputValidator<V, E>,
) )
.toList(); .toList();
}
/// A [FormInputValidator] represents the value of a single form input field. /// A [FormInputValidator] represents the value of a single form input field.
/// It contains information about the [FormInputStatus], value, as well /// It contains information about the [FormInputStatus], value, as well
/// as validation status. /// as validation status.
T validatorOf<T>(String key) { T validatorOf<T>(String key) => inputOf(key).validator as T;
return inputOf(key).validator as T;
}
/// Updates validator of a given input. (perform copyWith) /// Updates validator of a given input. (perform copyWith)
void updateValidator(String key, FormInputValidator dirtyValue) { void updateValidator<V>(
String key,
FormInputValidator<V, ValidationError> dirtyValue,
) {
if (contains(key)) { if (contains(key)) {
final index = _inputs.indexOf( final index = _inputs.indexOf(
inputOf(key), inputOf(key),
@ -73,38 +72,31 @@ class FormData extends Equatable {
/// Returns a validation error if the [FormInputValidator] is invalid. /// Returns a validation error if the [FormInputValidator] is invalid.
/// Returns null if the [FormInputValidator] is valid. /// Returns null if the [FormInputValidator] is valid.
E? errorOf<V, E>(String key) { E? errorOf<E extends ValidationError>(String key) =>
return (inputOf(key).validator as FormInputValidator<V, E>).error; (inputOf(key).validator as FormInputValidator<dynamic, E>).error;
}
/// The value of the associated [FormInputValidator]. For example, /// The value of the associated [FormInputValidator]. For example,
/// if you have a FormInputValidator for FirstName, the value could be 'Joe'. /// if you have a FormInputValidator for FirstName, the value could be 'Joe'.
V valueOf<V, E>(String key) { V valueOf<V>(String key) =>
return (inputOf(key).validator as FormInputValidator<V, E>).value; (inputOf(key).validator as FormInputValidator<V, dynamic>).value;
}
/// Returns `true` if the [FormInputValidator] is not valid. /// Returns `true` if the [FormInputValidator] is not valid.
/// Same as `E? errorOf<V, E>(String key) != null` /// Same as `E? errorOf<V, E>(String key) != null`
bool isNotValid(String key) { bool isNotValid(String key) => !inputOf(key).validator.valid;
return !inputOf(key).validator.valid;
}
/// Returns all associated metadata as a list /// Returns all associated metadata as a list
List<FormInputMetadata<M>> metadata<M>() { List<FormInputMetadata<M>> metadata<M>() => _inputs
return _inputs
.map<FormInputMetadata<M>>( .map<FormInputMetadata<M>>(
(FormInput input) => input.metadata as FormInputMetadata<M>, (input) => input.metadata as FormInputMetadata<M>,
) )
.toList(); .toList();
}
/// Returns the metadata associated. With `M` the type of extra data. /// Returns the metadata associated. With `M` the type of extra data.
FormInputMetadata<M> metadataOf<M>(String key) { FormInputMetadata<M> metadataOf<M>(String key) =>
return inputOf(key).metadata as FormInputMetadata<M>; inputOf(key).metadata as FormInputMetadata<M>;
}
/// Updates metadata of a given input. (perform copyWith) /// Updates metadata of a given input. (perform copyWith)
void updateMetadata(String key, FormInputMetadata meta) { void updateMetadata<M>(String key, FormInputMetadata<M> meta) {
if (contains(key)) { if (contains(key)) {
final index = _inputs.indexOf( final index = _inputs.indexOf(
inputOf(key), inputOf(key),
@ -113,15 +105,8 @@ class FormData extends Equatable {
} }
} }
/// Validate self inputs.
FormStatus validate() {
return FormStatus.validateInputs(_inputs);
}
/// Check if this contains an input with the given key. /// Check if this contains an input with the given key.
bool contains(String key) { bool contains(String key) => _inputs.any((input) => input.key == key);
return _inputs.any((FormInput input) => input.key == key);
}
/// Makes an intersection set operation and returns newly created [FormData] /// Makes an intersection set operation and returns newly created [FormData]
FormData intersection(FormData other) { FormData intersection(FormData other) {
@ -173,11 +158,9 @@ class FormData extends Equatable {
} }
/// Deeply copy this. /// Deeply copy this.
FormData clone() { FormData clone() => FormData(
return FormData( _inputs.map((input) => input.clone()).toList(),
_inputs.map((FormInput input) => input.clone()).toList(),
); );
}
/// Export this to [Map] format. /// Export this to [Map] format.
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
@ -190,6 +173,9 @@ class FormData extends Equatable {
return map; return map;
} }
/// Export this to [String] format.
String toJson() => jsonEncode(toMap());
@override @override
bool? get stringify => true; bool? get stringify => true;

View File

@ -17,10 +17,11 @@
part of 'form.dart'; part of 'form.dart';
class FormInput extends Equatable { class FormInput<Validator extends FormInputValidator<dynamic, ValidationError>>
extends Equatable {
final String key; final String key;
final FormInputValidator validator; final Validator validator;
final FormInputMetadata metadata; final FormInputMetadata<dynamic> metadata;
String get name => metadata._name ?? key; String get name => metadata._name ?? key;
@ -31,29 +32,25 @@ class FormInput extends Equatable {
this.metadata = const FormInputMetadata<void>(export: true), this.metadata = const FormInputMetadata<void>(export: true),
}); });
FormInput copyWith({
String? key,
Validator? validator,
FormInputMetadata? metadata,
}) =>
FormInput(
key ?? this.key,
validator ?? this.validator,
metadata: metadata ?? this.metadata,
);
FormInput clone() => copyWith(
key: key,
validator: validator,
metadata: metadata,
);
@override @override
bool? get stringify => true; bool? get stringify => true;
@override @override
List<Object?> get props => [key, validator, metadata]; 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,
);
}
FormInput clone() {
return copyWith(
key: key,
validator: validator,
metadata: metadata,
);
}
} }

View File

@ -17,7 +17,7 @@
part of 'form.dart'; part of 'form.dart';
class FormInputMetadata<T> extends Equatable { class FormInputMetadata<T extends Object?> extends Equatable {
final bool export; final bool export;
final String? _name; final String? _name;
final T? extra; final T? extra;
@ -28,29 +28,26 @@ class FormInputMetadata<T> extends Equatable {
String? name, String? name,
}) : _name = name; }) : _name = name;
FormInputMetadata<T> copyWith({
bool? export,
String? name,
T? extra,
}) =>
FormInputMetadata<T>(
export: export ?? this.export,
name: name ?? _name,
extra: extra ?? this.extra,
);
FormInputMetadata<T> clone() => copyWith(
export: export,
name: _name,
extra: extra,
);
@override @override
bool? get stringify => true; bool? get stringify => true;
@override @override
List<Object?> get props => [export, _name, extra]; 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,
);
}
} }

View File

@ -16,7 +16,7 @@
part of 'form.dart'; part of 'form.dart';
/// {@template form_input} /// {@template form_input_validator}
/// A [FormInputValidator] represents the value of a single form input field. /// A [FormInputValidator] represents the value of a single form input field.
/// It contains information about the [FormInputStatus], [value], as well /// It contains information about the [FormInputStatus], [value], as well
/// as validation status. /// as validation status.
@ -25,20 +25,22 @@ part of 'form.dart';
/// [FormInputValidator] instances. /// [FormInputValidator] instances.
/// ///
/// ```dart /// ```dart
/// enum FirstNameError { empty } /// class Name extends FormInputValidator<String, ValidationStandardError> {
/// class FirstName extends FormInputValidator<String, FirstNameError> { /// const Name.pure({String value = ''}) : super.pure(value);
/// const FirstName.pure({String value = ''}) : super.pure(value); /// const Name.dirty({String value = ''}) : super.dirty(value);
/// const FirstName.dirty({String value = ''}) : super.dirty(value);
/// ///
/// @override /// @override
/// FirstNameError? validator(String value) { /// ValidationStandardError? validator(String? value) {
/// return value.isEmpty ? FirstNameError.empty : null; /// if (value?.isEmpty ?? false) {
/// return ValidationStandardError.empty
/// }
/// return value == null ? ValidationStandardError.invalid : null;
/// } /// }
/// } /// }
/// ``` /// ```
/// {@endtemplate} /// {@endtemplate}
@immutable abstract class FormInputValidator<V, E extends ValidationError>
abstract class FormInputValidator<V, E> extends Equatable { extends Equatable {
const FormInputValidator._(this.value, [this.pure = true]); const FormInputValidator._(this.value, [this.pure = true]);
/// Constructor which create a `pure` [FormInputValidator] with a given value. /// Constructor which create a `pure` [FormInputValidator] with a given value.

View File

@ -0,0 +1,31 @@
// 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_validator}
/// A [FormValidator] represents the global validaton state of a Form.
/// {@endtemplate}
abstract class FormValidator {
/// {@macro form_validator}
const FormValidator();
bool isPure(FormData form) => form
.validators<dynamic, ValidationError>()
.every((validator) => validator.pure);
FormStatus validate(FormData form);
}

View File

@ -0,0 +1,40 @@
// 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:wyatt_form_bloc/wyatt_form_bloc.dart';
/// {@template every}
/// Check and validate every input of a form
/// {@endtemplate}
class EveryInputValidator extends FormValidator {
/// {@macro every}
const EveryInputValidator() : super();
@override
FormStatus validate(FormData form) {
if (isPure(form)) {
return FormStatus.pure;
}
if (form
.validators<dynamic, ValidationError>()
.any((validator) => validator.valid == false)) {
return FormStatus.invalid;
}
return FormStatus.valid;
}
}

View File

@ -0,0 +1,41 @@
// 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:wyatt_form_bloc/src/enums/validation_error.dart';
import 'package:wyatt_form_bloc/src/form/form.dart';
/// {@template regex_validator}
/// Abstract regex validator for form input.
/// {@endtemplate}
abstract class RegexValidator<E extends ValidationError>
extends FormInputValidator<String, E> {
const RegexValidator.pure() : super.pure('');
const RegexValidator.dirty([super.value = '']) : super.dirty();
RegExp get regex;
/// If the value is **not** null, but empty.
E get onEmpty;
/// If value does not conform to regex.
E get onError;
@override
E? validator(String? value) {
if (value?.isEmpty ?? false) {
return onEmpty;
}
return regex.hasMatch(value ?? '') ? null : onError;
}
}

View File

@ -0,0 +1,38 @@
// 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:wyatt_form_bloc/src/enums/validation_error.dart';
import 'package:wyatt_form_bloc/src/form/form.dart';
abstract class TextValidator<E extends ValidationError>
extends FormInputValidator<String, E> {
const TextValidator.pure() : super.pure('');
const TextValidator.dirty([super.value = '']) : super.dirty();
/// If the value is **not** null, but empty.
E get onEmpty;
/// If value is null.
E get onNull;
@override
E? validator(String? value) {
if (value?.isEmpty ?? false) {
return onEmpty;
}
return value != null ? null : onNull;
}
}

View File

@ -20,7 +20,7 @@ import 'package:wyatt_form_bloc/src/form/form.dart';
/// {@template boolean} /// {@template boolean}
/// Form input for a bool input /// Form input for a bool input
/// {@endtemplate} /// {@endtemplate}
class Boolean extends FormInputValidator<bool, ValidationError> { class Boolean extends FormInputValidator<bool, ValidationStandardError> {
/// {@macro boolean} /// {@macro boolean}
const Boolean.pure({bool? defaultValue = false}) const Boolean.pure({bool? defaultValue = false})
: super.pure(defaultValue ?? false); : super.pure(defaultValue ?? false);
@ -29,7 +29,6 @@ class Boolean extends FormInputValidator<bool, ValidationError> {
const Boolean.dirty({bool value = false}) : super.dirty(value); const Boolean.dirty({bool value = false}) : super.dirty(value);
@override @override
ValidationError? validator(bool? value) { ValidationStandardError? validator(bool? value) =>
return value != null ? null : ValidationError.invalid; value != null ? null : ValidationStandardError.invalid;
}
} }

View File

@ -21,7 +21,7 @@ import 'package:wyatt_form_bloc/src/form/form.dart';
/// Form input for a confirmed password input. /// Form input for a confirmed password input.
/// {@endtemplate} /// {@endtemplate}
class ConfirmedPassword class ConfirmedPassword
extends FormInputValidator<String, ValidationError> { extends FormInputValidator<String, ValidationStandardError> {
/// {@macro confirmed_password} /// {@macro confirmed_password}
const ConfirmedPassword.pure({this.password = ''}) : super.pure(''); const ConfirmedPassword.pure({this.password = ''}) : super.pure('');
@ -33,7 +33,6 @@ class ConfirmedPassword
final String password; final String password;
@override @override
ValidationError? validator(String? value) { ValidationStandardError? validator(String? value) =>
return password == value ? null : ValidationError.invalid; password == value ? null : ValidationStandardError.invalid;
}
} }

View File

@ -15,24 +15,25 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/src/enums/enums.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/validators/inputs/base/regex_validator.dart';
/// {@template email} /// {@template email}
/// Form input for an email input. /// Form input for an email input.
/// {@endtemplate} /// {@endtemplate}
class Email extends FormInputValidator<String, ValidationError> { class Email extends RegexValidator<ValidationStandardError> {
/// {@macro email} /// {@macro email}
const Email.pure() : super.pure(''); const Email.pure() : super.pure();
/// {@macro email} /// {@macro email}
const Email.dirty([String value = '']) : super.dirty(value); const Email.dirty([super.value = '']) : super.dirty();
static final RegExp _emailRegExp = RegExp(
r'[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+',
);
@override @override
ValidationError? validator(String? value) { ValidationStandardError get onEmpty => ValidationStandardError.empty;
return _emailRegExp.hasMatch(value ?? '') ? null : ValidationError.invalid; @override
} ValidationStandardError get onError => ValidationStandardError.invalid;
@override
RegExp get regex => RegExp(
r'[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+',
);
} }

View File

@ -0,0 +1,32 @@
// 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:wyatt_form_bloc/src/enums/validation_error.dart';
import 'package:wyatt_form_bloc/src/validators/inputs/base/text_validator.dart';
class EnumValidator<E> extends TextValidator<ValidationStandardError> {
/// {@macro text_string}
const EnumValidator.pure() : super.pure();
/// {@macro text_string}
EnumValidator.dirty(E value) : super.dirty(value.toString());
@override
ValidationStandardError get onEmpty => ValidationStandardError.empty;
@override
ValidationStandardError get onNull => ValidationStandardError.invalid;
}

View File

@ -15,24 +15,25 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/src/enums/enums.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/validators/inputs/base/regex_validator.dart';
/// {@template iban} /// {@template iban}
/// Form input for an IBAN input. /// Form input for an IBAN input.
/// {@endtemplate} /// {@endtemplate}
class Iban extends FormInputValidator<String, ValidationError> { class Iban extends RegexValidator<ValidationStandardError> {
/// {@macro iban} /// {@macro iban}
const Iban.pure() : super.pure(''); const Iban.pure() : super.pure();
/// {@macro iban} /// {@macro iban}
const Iban.dirty([String value = '']) : super.dirty(value); const Iban.dirty([super.value = '']) : super.dirty();
static final RegExp _regExp = RegExp(
r'^(?:((?:IT|SM)\d{2}[A-Z]{1}\d{22})|(NL\d{2}[A-Z]{4}\d{10})|(LV\d{2}[A-Z]{4}\d{13})|((?:BG|GB|IE)\d{2}[A-Z]{4}\d{14})|(GI\d{2}[A-Z]{4}\d{15})|(RO\d{2}[A-Z]{4}\d{16})|(MT\d{2}[A-Z]{4}\d{23})|(NO\d{13})|((?:DK|FI)\d{16})|((?:SI)\d{17})|((?:AT|EE|LU|LT)\d{18})|((?:HR|LI|CH)\d{19})|((?:DE|VA)\d{20})|((?:AD|CZ|ES|MD|SK|SE)\d{22})|(PT\d{23})|((?:IS)\d{24})|((?:BE)\d{14})|((?:FR|MC|GR)\d{25})|((?:PL|HU|CY)\d{26}))$',
);
@override @override
ValidationError? validator(String? value) { ValidationStandardError get onEmpty => ValidationStandardError.empty;
return _regExp.hasMatch(value ?? '') ? null : ValidationError.invalid; @override
} ValidationStandardError get onError => ValidationStandardError.invalid;
@override
RegExp get regex => RegExp(
r'^(?:((?:IT|SM)\d{2}[A-Z]{1}\d{22})|(NL\d{2}[A-Z]{4}\d{10})|(LV\d{2}[A-Z]{4}\d{13})|((?:BG|GB|IE)\d{2}[A-Z]{4}\d{14})|(GI\d{2}[A-Z]{4}\d{15})|(RO\d{2}[A-Z]{4}\d{16})|(MT\d{2}[A-Z]{4}\d{23})|(NO\d{13})|((?:DK|FI)\d{16})|((?:SI)\d{17})|((?:AT|EE|LU|LT)\d{18})|((?:HR|LI|CH)\d{19})|((?:DE|VA)\d{20})|((?:AD|CZ|ES|MD|SK|SE)\d{22})|(PT\d{23})|((?:IS)\d{24})|((?:BE)\d{14})|((?:FR|MC|GR)\d{25})|((?:PL|HU|CY)\d{26}))$',
);
} }

View File

@ -20,7 +20,8 @@ import 'package:wyatt_form_bloc/src/form/form.dart';
/// {@template list_option} /// {@template list_option}
/// Form input for a list input /// Form input for a list input
/// {@endtemplate} /// {@endtemplate}
class ListOption<T> extends FormInputValidator<List<T>, ValidationError> { class ListOption<T>
extends FormInputValidator<List<T>, ValidationStandardError> {
/// {@macro list_option} /// {@macro list_option}
const ListOption.pure({List<T>? defaultValue}) const ListOption.pure({List<T>? defaultValue})
: super.pure(defaultValue ?? const []); : super.pure(defaultValue ?? const []);
@ -28,7 +29,7 @@ class ListOption<T> extends FormInputValidator<List<T>, ValidationError> {
/// {@macro list_option} /// {@macro list_option}
const ListOption.dirty({List<T>? value}) : super.dirty(value ?? const []); const ListOption.dirty({List<T>? value}) : super.dirty(value ?? const []);
ListOption select(T? v) { ListOption<T> select(T? v) {
if (v == null) { if (v == null) {
return this; return this;
} }
@ -42,7 +43,8 @@ class ListOption<T> extends FormInputValidator<List<T>, ValidationError> {
} }
@override @override
ValidationError? validator(List<T>? value) { ValidationStandardError? validator(List<T>? value) =>
return value?.isNotEmpty == true ? null : ValidationError.invalid; value?.isNotEmpty ?? false == true
} ? null
: ValidationStandardError.invalid;
} }

View File

@ -15,22 +15,23 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/src/enums/enums.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/validators/inputs/base/regex_validator.dart';
/// {@template name} /// {@template name}
/// Form input for a name input. /// Form input for a name input.
/// {@endtemplate} /// {@endtemplate}
class Name extends FormInputValidator<String, ValidationError> { class Name extends RegexValidator<ValidationStandardError> {
/// {@macro name} /// {@macro name}
const Name.pure() : super.pure(''); const Name.pure() : super.pure();
/// {@macro name} /// {@macro name}
const Name.dirty([String value = '']) : super.dirty(value); const Name.dirty([super.value = '']) : super.dirty();
static final RegExp _nameRegExp = RegExp(r"^([ \u00c0-\u01ffa-zA-Z'\-])+$");
@override @override
ValidationError? validator(String? value) { ValidationStandardError get onEmpty => ValidationStandardError.empty;
return _nameRegExp.hasMatch(value ?? '') ? null : ValidationError.invalid; @override
} ValidationStandardError get onError => ValidationStandardError.invalid;
@override
RegExp get regex => RegExp(r"^([ \u00c0-\u01ffa-zA-Z'\-])+$");
} }

View File

@ -15,25 +15,23 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/src/enums/enums.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/validators/inputs/base/regex_validator.dart';
/// {@template password} /// {@template password}
/// Form input for a password input. /// Form input for a password input.
/// {@endtemplate} /// {@endtemplate}
class Password extends FormInputValidator<String, ValidationError> { class Password extends RegexValidator<ValidationStandardError> {
/// {@macro password} /// {@macro password}
const Password.pure() : super.pure(''); const Password.pure() : super.pure();
/// {@macro password} /// {@macro password}
const Password.dirty([String value = '']) : super.dirty(value); const Password.dirty([super.value = '']) : super.dirty();
static final RegExp _passwordRegExp =
RegExp(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$');
@override @override
ValidationError? validator(String? value) { ValidationStandardError get onEmpty => ValidationStandardError.empty;
return _passwordRegExp.hasMatch(value ?? '') @override
? null ValidationStandardError get onError => ValidationStandardError.invalid;
: ValidationError.invalid;
} @override
RegExp get regex => RegExp(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$');
} }

View File

@ -15,23 +15,24 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/src/enums/enums.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/validators/inputs/base/regex_validator.dart';
/// {@template phone} /// {@template phone}
/// Form input for a phone input. /// Form input for a phone input.
/// {@endtemplate} /// {@endtemplate}
class Phone extends FormInputValidator<String, ValidationError> { class Phone extends RegexValidator<ValidationStandardError> {
/// {@macro phone} /// {@macro phone}
const Phone.pure() : super.pure(''); const Phone.pure() : super.pure();
/// {@macro phone} /// {@macro phone}
const Phone.dirty([String value = '']) : super.dirty(value); const Phone.dirty([super.value = '']) : super.dirty();
static final RegExp _phoneRegExp =
RegExp(r'^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$');
@override @override
ValidationError? validator(String? value) { ValidationStandardError get onEmpty => ValidationStandardError.empty;
return _phoneRegExp.hasMatch(value ?? '') ? null : ValidationError.invalid; @override
} ValidationStandardError get onError => ValidationStandardError.invalid;
@override
RegExp get regex =>
RegExp(r'^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$');
} }

View File

@ -15,22 +15,23 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/src/enums/enums.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/validators/inputs/base/regex_validator.dart';
/// {@template siren} /// {@template siren}
/// Form input for a SIREN input. /// Form input for a SIREN input.
/// {@endtemplate} /// {@endtemplate}
class Siren extends FormInputValidator<String, ValidationError> { class Siren extends RegexValidator<ValidationStandardError> {
/// {@macro siren} /// {@macro siren}
const Siren.pure() : super.pure(''); const Siren.pure() : super.pure();
/// {@macro siren} /// {@macro siren}
const Siren.dirty([String value = '']) : super.dirty(value); const Siren.dirty([super.value = '']) : super.dirty();
static final RegExp _regExp = RegExp(r'(\d{9}|\d{3}[ ]\d{3}[ ]\d{3})$');
@override @override
ValidationError? validator(String? value) { ValidationStandardError get onEmpty => ValidationStandardError.empty;
return _regExp.hasMatch(value ?? '') ? null : ValidationError.invalid; @override
} ValidationStandardError get onError => ValidationStandardError.invalid;
@override
RegExp get regex => RegExp(r'(\d{9}|\d{3}[ ]\d{3}[ ]\d{3})$');
} }

View File

@ -15,20 +15,21 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/src/enums/enums.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/validators/inputs/base/text_validator.dart';
/// {@template text_string} /// {@template text_string}
/// Form input for a text input /// Form input for a text input
/// {@endtemplate} /// {@endtemplate}
class TextString extends FormInputValidator<String, ValidationError> { class TextString extends TextValidator<ValidationStandardError> {
/// {@macro text_string} /// {@macro text_string}
const TextString.pure() : super.pure(''); const TextString.pure() : super.pure();
/// {@macro text_string} /// {@macro text_string}
const TextString.dirty([String value = '']) : super.dirty(value); const TextString.dirty([super.value = '']) : super.dirty();
@override @override
ValidationError? validator(String? value) { ValidationStandardError get onEmpty => ValidationStandardError.empty;
return (value?.isNotEmpty ?? false) ? null : ValidationError.invalid;
} @override
ValidationStandardError get onNull => ValidationStandardError.invalid;
} }

View File

@ -14,13 +14,16 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
export 'boolean.dart'; export 'inputs/base/regex_validator.dart';
export 'confirmed_password.dart'; export 'inputs/base/text_validator.dart';
export 'email.dart'; export 'inputs/boolean.dart';
export 'iban.dart'; export 'inputs/confirmed_password.dart';
export 'list_option.dart'; export 'inputs/email.dart';
export 'name.dart'; export 'inputs/enum_validator.dart';
export 'password.dart'; export 'inputs/iban.dart';
export 'phone.dart'; export 'inputs/list_option.dart';
export 'siren.dart'; export 'inputs/name.dart';
export 'text_string.dart'; export 'inputs/password.dart';
export 'inputs/phone.dart';
export 'inputs/siren.dart';
export 'inputs/text_string.dart';

View File

@ -9,7 +9,6 @@ environment:
dependencies: dependencies:
bloc: ^8.0.3 bloc: ^8.0.3
equatable: ^2.0.3 equatable: ^2.0.3
meta: ^1.7.0
dev_dependencies: dev_dependencies:
test: ^1.21.4 test: ^1.21.4