diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..7eda004 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "formz", + "request": "launch", + "type": "dart" + }, + { + "name": "async_example", + "cwd": "example", + "request": "launch", + "type": "dart", + "program": "lib/async/main.dart" + }, + { + "name": "sync_example", + "cwd": "example", + "request": "launch", + "type": "dart", + "program": "lib/sync/main.dart" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index ef2bd15..7913766 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,118 @@ print(joe.error); // null print(name.displayError); // null ``` +## Create a AsyncFormzInput +```dart + +class Email extends AsyncFormzInput { + const Email( + super.value, { + super.error, + super.validationStatus, + this.isRequired = true, + }); + + final bool isRequired; + + Email copyWith({ + String? value, + EmailValidationError? error, + AsyncFormzInputValidationStatus? validationStatus, + bool? isValidating, + bool? isRequired, + }) { + return Email( + value ?? this.value, + error: error ?? this.error, + validationStatus: validationStatus ?? this.validationStatus, + isRequired: isRequired ?? this.isRequired, + ); + } + + Email copyWithResetError({ + String? value, + AsyncFormzInputValidationStatus? validationStatus, + bool? isRequired, + }) { + return Email( + value ?? this.value, + validationStatus: validationStatus ?? this.validationStatus, + isRequired: isRequired ?? this.isRequired, + ); + } +} +``` + +## Create a AsyncFormzInput +```dart +class EmailValidator extends AsyncFormzInputValidator { + const EmailValidator({ + required EmailRepository emailRepository, + }) : _emailRepository = emailRepository; + + final EmailRepository _emailRepository; + + @override + bool canValidate(Email input) { + if (input.validationStatus.isValidated) { + return true; + } + final validFormat = RegVal.hasMatch( + input.value, + _kEmailPattern, + ); + return validFormat; + } + + @override + Future validate(Email input) async { + if (input.isRequired && input.value.isEmpty) { + return const EmailValidationError.required(); + } + + final validFormat = RegVal.hasMatch( + input.value, + _kEmailPattern, + ); + if (!validFormat) { + return const EmailValidationError.invalidFormat(); + } + + final alreadyExist = await _emailRepository.findByAddress(input.value); + if (alreadyExist) { + return const EmailValidationError.alreadyExists(); + } + + return null; + } +} +``` + +## Interact with a AsyncFormzInput and AsyncFormzInputValidator using Bloc +```dart +Future emailChanged(String value) async { + emit(state.copyWith( + email: state.email.copyWithResetError(value: value), + )); + final canValidate = _emailValidator.canValidate(state.email); + if (!canValidate) { + return; + } + emit(state.copyWith( + email: state.email.copyWith( + validationStatus: AsyncFormzInputValidationStatus.validating, + ), + )); + final error = await _emailValidator.validate(state.email); + emit(state.copyWith( + email: state.email.copyWith( + error: error, + validationStatus: AsyncFormzInputValidationStatus.validated, + ), + )); +} +``` + ## Validate Multiple FormzInput Items ```dart diff --git a/example/lib/async/data/repositories/email_repository.dart b/example/lib/async/data/repositories/email_repository.dart new file mode 100644 index 0000000..d270457 --- /dev/null +++ b/example/lib/async/data/repositories/email_repository.dart @@ -0,0 +1,8 @@ +class EmailRepository { + const EmailRepository(); + + Future findByAddress(String emailAddress) async { + await Future.delayed(const Duration(seconds: 1)); + return true; + } +} diff --git a/example/lib/async/main.dart b/example/lib/async/main.dart new file mode 100644 index 0000000..427fb62 --- /dev/null +++ b/example/lib/async/main.dart @@ -0,0 +1,6 @@ +import 'package:example/async/ui/app.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(const App()); +} diff --git a/example/lib/async/ui/app.dart b/example/lib/async/ui/app.dart new file mode 100644 index 0000000..6f2ff69 --- /dev/null +++ b/example/lib/async/ui/app.dart @@ -0,0 +1,19 @@ +import 'package:example/async/data/repositories/email_repository.dart'; +import 'package:example/async/ui/feature/register/view/register_page.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class App extends StatelessWidget { + const App({super.key}); + + @override + Widget build(BuildContext context) { + return Provider.value( + value: const EmailRepository(), + child: const MaterialApp( + title: 'Example', + home: RegisterPage(), + ), + ); + } +} diff --git a/example/lib/async/ui/feature/register/cubit/register_cubit.dart b/example/lib/async/ui/feature/register/cubit/register_cubit.dart new file mode 100644 index 0000000..d0fa5cd --- /dev/null +++ b/example/lib/async/ui/feature/register/cubit/register_cubit.dart @@ -0,0 +1,78 @@ +import 'package:example/async/ui/feature/register/cubit/register_state.dart'; +import 'package:example/async/ui/shared/forms/validators/amount_validator.dart'; +import 'package:example/async/ui/shared/forms/validators/email_validator.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:formz/formz.dart'; + +class RegisterCubit extends Cubit { + RegisterCubit({ + required AmountValidator amountValidator, + required EmailValidator emailValidator, + }) : _amountValidator = amountValidator, + _emailValidator = emailValidator, + super(RegisterState()); + + final AmountValidator _amountValidator; + final EmailValidator _emailValidator; + + Future amountChanged(String value) async { + emit( + state.copyWith( + amount: state.amount.copyWithResetError( + value: value, + validationStatus: AsyncFormzInputValidationStatus.validating, + ), + ), + ); + final error = await _amountValidator.validate(state.amount); + emit( + state.copyWith( + amount: state.amount.copyWith( + error: error, + validationStatus: AsyncFormzInputValidationStatus.validated, + ), + ), + ); + _updateFormState(); + } + + Future emailChanged(String value) async { + emit( + state.copyWith( + email: state.email.copyWithResetError(value: value), + ), + ); + final canValidate = _emailValidator.canValidate(state.email); + if (!canValidate) { + return; + } + emit( + state.copyWith( + email: state.email.copyWith( + validationStatus: AsyncFormzInputValidationStatus.validating, + ), + ), + ); + final error = await _emailValidator.validate(state.email); + emit( + state.copyWith( + email: state.email.copyWith( + error: error, + validationStatus: AsyncFormzInputValidationStatus.validated, + ), + ), + ); + _updateFormState(); + } + + void _updateFormState() { + emit( + state.copyWith( + isFormValid: Formz.validate([ + state.amount, + state.email, + ]), + ), + ); + } +} diff --git a/example/lib/async/ui/feature/register/cubit/register_state.dart b/example/lib/async/ui/feature/register/cubit/register_state.dart new file mode 100644 index 0000000..6c919f2 --- /dev/null +++ b/example/lib/async/ui/feature/register/cubit/register_state.dart @@ -0,0 +1,14 @@ +import 'package:example/async/ui/shared/forms/inputs/amount_input.dart'; +import 'package:example/async/ui/shared/forms/inputs/email_input.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'register_state.freezed.dart'; + +@freezed +class RegisterState with _$RegisterState { + factory RegisterState({ + @Default(false) bool isFormValid, + @Default(Amount('', max: 0.5)) Amount amount, + @Default(Email('')) Email email, + }) = _RegisterState; +} diff --git a/example/lib/async/ui/feature/register/cubit/register_state.freezed.dart b/example/lib/async/ui/feature/register/cubit/register_state.freezed.dart new file mode 100644 index 0000000..938b827 --- /dev/null +++ b/example/lib/async/ui/feature/register/cubit/register_state.freezed.dart @@ -0,0 +1,175 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'register_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$RegisterState { + bool get isFormValid => throw _privateConstructorUsedError; + Amount get amount => throw _privateConstructorUsedError; + Email get email => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $RegisterStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $RegisterStateCopyWith<$Res> { + factory $RegisterStateCopyWith( + RegisterState value, $Res Function(RegisterState) then) = + _$RegisterStateCopyWithImpl<$Res>; + $Res call({bool isFormValid, Amount amount, Email email}); +} + +/// @nodoc +class _$RegisterStateCopyWithImpl<$Res> + implements $RegisterStateCopyWith<$Res> { + _$RegisterStateCopyWithImpl(this._value, this._then); + + final RegisterState _value; + // ignore: unused_field + final $Res Function(RegisterState) _then; + + @override + $Res call({ + Object? isFormValid = freezed, + Object? amount = freezed, + Object? email = freezed, + }) { + return _then(_value.copyWith( + isFormValid: isFormValid == freezed + ? _value.isFormValid + : isFormValid // ignore: cast_nullable_to_non_nullable + as bool, + amount: amount == freezed + ? _value.amount + : amount // ignore: cast_nullable_to_non_nullable + as Amount, + email: email == freezed + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as Email, + )); + } +} + +/// @nodoc +abstract class _$$_RegisterStateCopyWith<$Res> + implements $RegisterStateCopyWith<$Res> { + factory _$$_RegisterStateCopyWith( + _$_RegisterState value, $Res Function(_$_RegisterState) then) = + __$$_RegisterStateCopyWithImpl<$Res>; + @override + $Res call({bool isFormValid, Amount amount, Email email}); +} + +/// @nodoc +class __$$_RegisterStateCopyWithImpl<$Res> + extends _$RegisterStateCopyWithImpl<$Res> + implements _$$_RegisterStateCopyWith<$Res> { + __$$_RegisterStateCopyWithImpl( + _$_RegisterState _value, $Res Function(_$_RegisterState) _then) + : super(_value, (v) => _then(v as _$_RegisterState)); + + @override + _$_RegisterState get _value => super._value as _$_RegisterState; + + @override + $Res call({ + Object? isFormValid = freezed, + Object? amount = freezed, + Object? email = freezed, + }) { + return _then(_$_RegisterState( + isFormValid: isFormValid == freezed + ? _value.isFormValid + : isFormValid // ignore: cast_nullable_to_non_nullable + as bool, + amount: amount == freezed + ? _value.amount + : amount // ignore: cast_nullable_to_non_nullable + as Amount, + email: email == freezed + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as Email, + )); + } +} + +/// @nodoc + +class _$_RegisterState implements _RegisterState { + _$_RegisterState( + {this.isFormValid = false, + this.amount = const Amount('', max: 0.5), + this.email = const Email('')}); + + @override + @JsonKey() + final bool isFormValid; + @override + @JsonKey() + final Amount amount; + @override + @JsonKey() + final Email email; + + @override + String toString() { + return 'RegisterState(isFormValid: $isFormValid, amount: $amount, email: $email)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_RegisterState && + const DeepCollectionEquality() + .equals(other.isFormValid, isFormValid) && + const DeepCollectionEquality().equals(other.amount, amount) && + const DeepCollectionEquality().equals(other.email, email)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(isFormValid), + const DeepCollectionEquality().hash(amount), + const DeepCollectionEquality().hash(email)); + + @JsonKey(ignore: true) + @override + _$$_RegisterStateCopyWith<_$_RegisterState> get copyWith => + __$$_RegisterStateCopyWithImpl<_$_RegisterState>(this, _$identity); +} + +abstract class _RegisterState implements RegisterState { + factory _RegisterState( + {final bool isFormValid, + final Amount amount, + final Email email}) = _$_RegisterState; + + @override + bool get isFormValid; + @override + Amount get amount; + @override + Email get email; + @override + @JsonKey(ignore: true) + _$$_RegisterStateCopyWith<_$_RegisterState> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/example/lib/async/ui/feature/register/view/register_page.dart b/example/lib/async/ui/feature/register/view/register_page.dart new file mode 100644 index 0000000..de14deb --- /dev/null +++ b/example/lib/async/ui/feature/register/view/register_page.dart @@ -0,0 +1,23 @@ +import 'package:example/async/ui/feature/register/cubit/register_cubit.dart'; +import 'package:example/async/ui/feature/register/view/register_view.dart'; +import 'package:example/async/ui/shared/forms/validators/amount_validator.dart'; +import 'package:example/async/ui/shared/forms/validators/email_validator.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class RegisterPage extends StatelessWidget { + const RegisterPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => RegisterCubit( + amountValidator: const AmountValidator(), + emailValidator: EmailValidator( + emailRepository: context.read(), + ), + ), + child: const RegisterView(), + ); + } +} diff --git a/example/lib/async/ui/feature/register/view/register_view.dart b/example/lib/async/ui/feature/register/view/register_view.dart new file mode 100644 index 0000000..d6dad3c --- /dev/null +++ b/example/lib/async/ui/feature/register/view/register_view.dart @@ -0,0 +1,246 @@ +import 'package:example/async/ui/feature/register/cubit/register_cubit.dart'; +import 'package:example/async/ui/shared/components/form_label.dart'; +import 'package:example/async/ui/shared/forms/inputs/amount_input.dart'; +import 'package:example/async/ui/shared/forms/inputs/email_input.dart'; +import 'package:flutter/material.dart'; +import 'package:formz/formz.dart'; +import 'package:provider/provider.dart'; + +class RegisterView extends StatelessWidget { + const RegisterView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Example'), + ), + body: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 16, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + _FormStatusText(), + SizedBox(height: 16), + _AmountTextField(), + SizedBox(height: 16), + _EmailTextField(), + ], + ), + ), + ); + } +} + +class _FormStatusText extends StatelessWidget { + const _FormStatusText(); + + @override + Widget build(BuildContext context) { + final isFormValid = context.select( + (c) => c.state.isFormValid, + ); + return Text.rich( + TextSpan( + children: [ + const TextSpan( + text: 'Form status: ', + style: TextStyle( + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: isFormValid ? 'valid' : 'invalid', + style: TextStyle( + color: isFormValid ? Colors.green : Colors.red, + ), + ), + ], + ), + ); + } +} + +class _AmountTextField extends StatefulWidget { + const _AmountTextField(); + + @override + State<_AmountTextField> createState() => _AmountTextFieldState(); +} + +class _AmountTextFieldState extends State<_AmountTextField> { + late FocusNode textFocusNode; + late TextEditingController textController; + + @override + void initState() { + textController = TextEditingController(); + textFocusNode = FocusNode(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final cubit = context.read(); + final amount = context.select( + (c) => c.state.amount, + ); + if (!textFocusNode.hasFocus) { + textController.text = amount.value; + } + return FormLabel( + label: _AmountLabel( + input: amount, + ), + child: TextFormField( + controller: textController, + focusNode: textFocusNode, + onChanged: cubit.amountChanged, + decoration: InputDecoration( + hintText: r'Amount in $', + errorText: amount.error?.map( + outOfRange: (value) => + 'Amount must be between ${value.min} and ${value.max}', + parsing: (_) => 'Amount must be a number', + required: (_) => 'Amount is required', + ), + ), + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + ), + ); + } +} + +class _EmailTextField extends StatefulWidget { + const _EmailTextField(); + + @override + State<_EmailTextField> createState() => _EmailTextFieldState(); +} + +class _EmailTextFieldState extends State<_EmailTextField> { + late FocusNode textFocusNode; + late TextEditingController textController; + + @override + void initState() { + textController = TextEditingController(); + textFocusNode = FocusNode(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final cubit = context.read(); + final email = context.select( + (c) => c.state.email, + ); + if (!textFocusNode.hasFocus) { + textController.text = email.value; + } + return FormLabel( + label: _EmailLabel( + input: email, + ), + child: TextFormField( + controller: textController, + focusNode: textFocusNode, + onChanged: cubit.emailChanged, + decoration: InputDecoration( + hintText: 'Enter your email', + helperText: + email.validationStatus.isValidating ? 'Validating...' : null, + errorText: email.validationStatus.isNotValidating + ? email.error?.map( + invalidFormat: (_) => 'Invalid email format', + alreadyExists: (_) => 'Email already exists', + required: (_) => 'Email is required', + ) + : null, + ), + keyboardType: TextInputType.emailAddress, + ), + ); + } +} + +class _EmailLabel extends StatelessWidget { + const _EmailLabel({ + required this.input, + }); + + final Email input; + + @override + Widget build(BuildContext context) { + final valStatus = _getValidationStatusText(input.validationStatus); + return Text.rich( + TextSpan( + children: [ + const TextSpan( + text: 'Email', + ), + TextSpan( + text: + ' (is_valid: ${input.isValid}, validation_status: $valStatus)', + style: const TextStyle( + color: Colors.grey, + fontWeight: FontWeight.w200, + ), + ), + ], + ), + ); + } + + String _getValidationStatusText(AsyncFormzInputValidationStatus status) { + return status.isPure + ? 'pure' + : status.isValidating + ? 'validating' + : 'validated'; + } +} + +class _AmountLabel extends StatelessWidget { + const _AmountLabel({ + required this.input, + }); + + final Amount input; + + @override + Widget build(BuildContext context) { + final valStatus = _getValidationStatusText(input.validationStatus); + return Text.rich( + TextSpan( + children: [ + const TextSpan( + text: 'Amount', + ), + TextSpan( + text: + ' (is_valid: ${input.isValid}, validation_status: $valStatus)', + style: const TextStyle( + color: Colors.grey, + fontWeight: FontWeight.w200, + ), + ), + ], + ), + ); + } + + String _getValidationStatusText(AsyncFormzInputValidationStatus status) { + return status.isPure + ? 'pure' + : status.isValidating + ? 'validating' + : 'validated'; + } +} diff --git a/example/lib/async/ui/shared/components/form_label.dart b/example/lib/async/ui/shared/components/form_label.dart new file mode 100644 index 0000000..ccea4c4 --- /dev/null +++ b/example/lib/async/ui/shared/components/form_label.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +class FormLabel extends StatelessWidget { + const FormLabel({ + super.key, + required this.label, + required this.child, + }); + + final Widget label; + final Widget child; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultTextStyle.merge( + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + child: label, + ), + child, + ], + ); + } +} diff --git a/example/lib/async/ui/shared/forms/inputs/amount_input.dart b/example/lib/async/ui/shared/forms/inputs/amount_input.dart new file mode 100644 index 0000000..a39c25c --- /dev/null +++ b/example/lib/async/ui/shared/forms/inputs/amount_input.dart @@ -0,0 +1,52 @@ +import 'package:example/async/ui/shared/forms/validation_errors/amount_errors.dart'; +import 'package:formz/formz.dart'; + +class Amount extends AsyncFormzInput { + const Amount( + super.value, { + super.error, + super.validationStatus, + this.isRequired = true, + this.min = 0.0, + this.max = double.maxFinite, + }); + + final bool isRequired; + final double min; + final double max; + + Amount copyWith({ + String? value, + AmountValidationError? error, + AsyncFormzInputValidationStatus? validationStatus, + bool? isValidating, + bool? isRequired, + double? min, + double? max, + }) { + return Amount( + value ?? this.value, + error: error ?? this.error, + validationStatus: validationStatus ?? this.validationStatus, + isRequired: isRequired ?? this.isRequired, + min: min ?? this.min, + max: max ?? this.max, + ); + } + + Amount copyWithResetError({ + String? value, + AsyncFormzInputValidationStatus? validationStatus, + bool? isRequired, + double? min, + double? max, + }) { + return Amount( + value ?? this.value, + validationStatus: validationStatus ?? this.validationStatus, + isRequired: isRequired ?? this.isRequired, + min: min ?? this.min, + max: max ?? this.max, + ); + } +} diff --git a/example/lib/async/ui/shared/forms/inputs/email_input.dart b/example/lib/async/ui/shared/forms/inputs/email_input.dart new file mode 100644 index 0000000..4b4044d --- /dev/null +++ b/example/lib/async/ui/shared/forms/inputs/email_input.dart @@ -0,0 +1,40 @@ +import 'package:example/async/ui/shared/forms/validation_errors/email_errors.dart'; +import 'package:formz/formz.dart'; + +class Email extends AsyncFormzInput { + const Email( + super.value, { + super.error, + super.validationStatus, + this.isRequired = true, + }); + + final bool isRequired; + + Email copyWith({ + String? value, + EmailValidationError? error, + AsyncFormzInputValidationStatus? validationStatus, + bool? isValidating, + bool? isRequired, + }) { + return Email( + value ?? this.value, + error: error ?? this.error, + validationStatus: validationStatus ?? this.validationStatus, + isRequired: isRequired ?? this.isRequired, + ); + } + + Email copyWithResetError({ + String? value, + AsyncFormzInputValidationStatus? validationStatus, + bool? isRequired, + }) { + return Email( + value ?? this.value, + validationStatus: validationStatus ?? this.validationStatus, + isRequired: isRequired ?? this.isRequired, + ); + } +} diff --git a/example/lib/async/ui/shared/forms/validation_errors/amount_errors.dart b/example/lib/async/ui/shared/forms/validation_errors/amount_errors.dart new file mode 100644 index 0000000..5fbe90f --- /dev/null +++ b/example/lib/async/ui/shared/forms/validation_errors/amount_errors.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'amount_errors.freezed.dart'; + +@freezed +class AmountValidationError with _$AmountValidationError { + const factory AmountValidationError.outOfRange({ + required String amount, + required double min, + required double max, + }) = OutOfRangeAmountValidationError; + + const factory AmountValidationError.parsing({ + required String amount, + }) = ParsingAmountValidationError; + + const factory AmountValidationError.required() = + RequiredAmountValidationError; +} diff --git a/example/lib/async/ui/shared/forms/validation_errors/amount_errors.freezed.dart b/example/lib/async/ui/shared/forms/validation_errors/amount_errors.freezed.dart new file mode 100644 index 0000000..9674c31 --- /dev/null +++ b/example/lib/async/ui/shared/forms/validation_errors/amount_errors.freezed.dart @@ -0,0 +1,512 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'amount_errors.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$AmountValidationError { + @optionalTypeArgs + TResult when({ + required TResult Function(String amount, double min, double max) outOfRange, + required TResult Function(String amount) parsing, + required TResult Function() required, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function(String amount, double min, double max)? outOfRange, + TResult Function(String amount)? parsing, + TResult Function()? required, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String amount, double min, double max)? outOfRange, + TResult Function(String amount)? parsing, + TResult Function()? required, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(OutOfRangeAmountValidationError value) outOfRange, + required TResult Function(ParsingAmountValidationError value) parsing, + required TResult Function(RequiredAmountValidationError value) required, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(OutOfRangeAmountValidationError value)? outOfRange, + TResult Function(ParsingAmountValidationError value)? parsing, + TResult Function(RequiredAmountValidationError value)? required, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(OutOfRangeAmountValidationError value)? outOfRange, + TResult Function(ParsingAmountValidationError value)? parsing, + TResult Function(RequiredAmountValidationError value)? required, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AmountValidationErrorCopyWith<$Res> { + factory $AmountValidationErrorCopyWith(AmountValidationError value, + $Res Function(AmountValidationError) then) = + _$AmountValidationErrorCopyWithImpl<$Res>; +} + +/// @nodoc +class _$AmountValidationErrorCopyWithImpl<$Res> + implements $AmountValidationErrorCopyWith<$Res> { + _$AmountValidationErrorCopyWithImpl(this._value, this._then); + + final AmountValidationError _value; + // ignore: unused_field + final $Res Function(AmountValidationError) _then; +} + +/// @nodoc +abstract class _$$OutOfRangeAmountValidationErrorCopyWith<$Res> { + factory _$$OutOfRangeAmountValidationErrorCopyWith( + _$OutOfRangeAmountValidationError value, + $Res Function(_$OutOfRangeAmountValidationError) then) = + __$$OutOfRangeAmountValidationErrorCopyWithImpl<$Res>; + $Res call({String amount, double min, double max}); +} + +/// @nodoc +class __$$OutOfRangeAmountValidationErrorCopyWithImpl<$Res> + extends _$AmountValidationErrorCopyWithImpl<$Res> + implements _$$OutOfRangeAmountValidationErrorCopyWith<$Res> { + __$$OutOfRangeAmountValidationErrorCopyWithImpl( + _$OutOfRangeAmountValidationError _value, + $Res Function(_$OutOfRangeAmountValidationError) _then) + : super(_value, (v) => _then(v as _$OutOfRangeAmountValidationError)); + + @override + _$OutOfRangeAmountValidationError get _value => + super._value as _$OutOfRangeAmountValidationError; + + @override + $Res call({ + Object? amount = freezed, + Object? min = freezed, + Object? max = freezed, + }) { + return _then(_$OutOfRangeAmountValidationError( + amount: amount == freezed + ? _value.amount + : amount // ignore: cast_nullable_to_non_nullable + as String, + min: min == freezed + ? _value.min + : min // ignore: cast_nullable_to_non_nullable + as double, + max: max == freezed + ? _value.max + : max // ignore: cast_nullable_to_non_nullable + as double, + )); + } +} + +/// @nodoc + +class _$OutOfRangeAmountValidationError + implements OutOfRangeAmountValidationError { + const _$OutOfRangeAmountValidationError( + {required this.amount, required this.min, required this.max}); + + @override + final String amount; + @override + final double min; + @override + final double max; + + @override + String toString() { + return 'AmountValidationError.outOfRange(amount: $amount, min: $min, max: $max)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$OutOfRangeAmountValidationError && + const DeepCollectionEquality().equals(other.amount, amount) && + const DeepCollectionEquality().equals(other.min, min) && + const DeepCollectionEquality().equals(other.max, max)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(amount), + const DeepCollectionEquality().hash(min), + const DeepCollectionEquality().hash(max)); + + @JsonKey(ignore: true) + @override + _$$OutOfRangeAmountValidationErrorCopyWith<_$OutOfRangeAmountValidationError> + get copyWith => __$$OutOfRangeAmountValidationErrorCopyWithImpl< + _$OutOfRangeAmountValidationError>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String amount, double min, double max) outOfRange, + required TResult Function(String amount) parsing, + required TResult Function() required, + }) { + return outOfRange(amount, min, max); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function(String amount, double min, double max)? outOfRange, + TResult Function(String amount)? parsing, + TResult Function()? required, + }) { + return outOfRange?.call(amount, min, max); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String amount, double min, double max)? outOfRange, + TResult Function(String amount)? parsing, + TResult Function()? required, + required TResult orElse(), + }) { + if (outOfRange != null) { + return outOfRange(amount, min, max); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(OutOfRangeAmountValidationError value) outOfRange, + required TResult Function(ParsingAmountValidationError value) parsing, + required TResult Function(RequiredAmountValidationError value) required, + }) { + return outOfRange(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(OutOfRangeAmountValidationError value)? outOfRange, + TResult Function(ParsingAmountValidationError value)? parsing, + TResult Function(RequiredAmountValidationError value)? required, + }) { + return outOfRange?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(OutOfRangeAmountValidationError value)? outOfRange, + TResult Function(ParsingAmountValidationError value)? parsing, + TResult Function(RequiredAmountValidationError value)? required, + required TResult orElse(), + }) { + if (outOfRange != null) { + return outOfRange(this); + } + return orElse(); + } +} + +abstract class OutOfRangeAmountValidationError + implements AmountValidationError { + const factory OutOfRangeAmountValidationError( + {required final String amount, + required final double min, + required final double max}) = _$OutOfRangeAmountValidationError; + + String get amount; + double get min; + double get max; + @JsonKey(ignore: true) + _$$OutOfRangeAmountValidationErrorCopyWith<_$OutOfRangeAmountValidationError> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$ParsingAmountValidationErrorCopyWith<$Res> { + factory _$$ParsingAmountValidationErrorCopyWith( + _$ParsingAmountValidationError value, + $Res Function(_$ParsingAmountValidationError) then) = + __$$ParsingAmountValidationErrorCopyWithImpl<$Res>; + $Res call({String amount}); +} + +/// @nodoc +class __$$ParsingAmountValidationErrorCopyWithImpl<$Res> + extends _$AmountValidationErrorCopyWithImpl<$Res> + implements _$$ParsingAmountValidationErrorCopyWith<$Res> { + __$$ParsingAmountValidationErrorCopyWithImpl( + _$ParsingAmountValidationError _value, + $Res Function(_$ParsingAmountValidationError) _then) + : super(_value, (v) => _then(v as _$ParsingAmountValidationError)); + + @override + _$ParsingAmountValidationError get _value => + super._value as _$ParsingAmountValidationError; + + @override + $Res call({ + Object? amount = freezed, + }) { + return _then(_$ParsingAmountValidationError( + amount: amount == freezed + ? _value.amount + : amount // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$ParsingAmountValidationError implements ParsingAmountValidationError { + const _$ParsingAmountValidationError({required this.amount}); + + @override + final String amount; + + @override + String toString() { + return 'AmountValidationError.parsing(amount: $amount)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ParsingAmountValidationError && + const DeepCollectionEquality().equals(other.amount, amount)); + } + + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(amount)); + + @JsonKey(ignore: true) + @override + _$$ParsingAmountValidationErrorCopyWith<_$ParsingAmountValidationError> + get copyWith => __$$ParsingAmountValidationErrorCopyWithImpl< + _$ParsingAmountValidationError>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String amount, double min, double max) outOfRange, + required TResult Function(String amount) parsing, + required TResult Function() required, + }) { + return parsing(amount); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function(String amount, double min, double max)? outOfRange, + TResult Function(String amount)? parsing, + TResult Function()? required, + }) { + return parsing?.call(amount); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String amount, double min, double max)? outOfRange, + TResult Function(String amount)? parsing, + TResult Function()? required, + required TResult orElse(), + }) { + if (parsing != null) { + return parsing(amount); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(OutOfRangeAmountValidationError value) outOfRange, + required TResult Function(ParsingAmountValidationError value) parsing, + required TResult Function(RequiredAmountValidationError value) required, + }) { + return parsing(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(OutOfRangeAmountValidationError value)? outOfRange, + TResult Function(ParsingAmountValidationError value)? parsing, + TResult Function(RequiredAmountValidationError value)? required, + }) { + return parsing?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(OutOfRangeAmountValidationError value)? outOfRange, + TResult Function(ParsingAmountValidationError value)? parsing, + TResult Function(RequiredAmountValidationError value)? required, + required TResult orElse(), + }) { + if (parsing != null) { + return parsing(this); + } + return orElse(); + } +} + +abstract class ParsingAmountValidationError implements AmountValidationError { + const factory ParsingAmountValidationError({required final String amount}) = + _$ParsingAmountValidationError; + + String get amount; + @JsonKey(ignore: true) + _$$ParsingAmountValidationErrorCopyWith<_$ParsingAmountValidationError> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$RequiredAmountValidationErrorCopyWith<$Res> { + factory _$$RequiredAmountValidationErrorCopyWith( + _$RequiredAmountValidationError value, + $Res Function(_$RequiredAmountValidationError) then) = + __$$RequiredAmountValidationErrorCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$RequiredAmountValidationErrorCopyWithImpl<$Res> + extends _$AmountValidationErrorCopyWithImpl<$Res> + implements _$$RequiredAmountValidationErrorCopyWith<$Res> { + __$$RequiredAmountValidationErrorCopyWithImpl( + _$RequiredAmountValidationError _value, + $Res Function(_$RequiredAmountValidationError) _then) + : super(_value, (v) => _then(v as _$RequiredAmountValidationError)); + + @override + _$RequiredAmountValidationError get _value => + super._value as _$RequiredAmountValidationError; +} + +/// @nodoc + +class _$RequiredAmountValidationError implements RequiredAmountValidationError { + const _$RequiredAmountValidationError(); + + @override + String toString() { + return 'AmountValidationError.required()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RequiredAmountValidationError); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String amount, double min, double max) outOfRange, + required TResult Function(String amount) parsing, + required TResult Function() required, + }) { + return required(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function(String amount, double min, double max)? outOfRange, + TResult Function(String amount)? parsing, + TResult Function()? required, + }) { + return required?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String amount, double min, double max)? outOfRange, + TResult Function(String amount)? parsing, + TResult Function()? required, + required TResult orElse(), + }) { + if (required != null) { + return required(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(OutOfRangeAmountValidationError value) outOfRange, + required TResult Function(ParsingAmountValidationError value) parsing, + required TResult Function(RequiredAmountValidationError value) required, + }) { + return required(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(OutOfRangeAmountValidationError value)? outOfRange, + TResult Function(ParsingAmountValidationError value)? parsing, + TResult Function(RequiredAmountValidationError value)? required, + }) { + return required?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(OutOfRangeAmountValidationError value)? outOfRange, + TResult Function(ParsingAmountValidationError value)? parsing, + TResult Function(RequiredAmountValidationError value)? required, + required TResult orElse(), + }) { + if (required != null) { + return required(this); + } + return orElse(); + } +} + +abstract class RequiredAmountValidationError implements AmountValidationError { + const factory RequiredAmountValidationError() = + _$RequiredAmountValidationError; +} diff --git a/example/lib/async/ui/shared/forms/validation_errors/email_errors.dart b/example/lib/async/ui/shared/forms/validation_errors/email_errors.dart new file mode 100644 index 0000000..daaf9f1 --- /dev/null +++ b/example/lib/async/ui/shared/forms/validation_errors/email_errors.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'email_errors.freezed.dart'; + +@freezed +class EmailValidationError with _$EmailValidationError { + const factory EmailValidationError.invalidFormat() = + InvalidFormatEmailValidationError; + + const factory EmailValidationError.alreadyExists() = + AlreadyExistsEmailValidationError; + + const factory EmailValidationError.required() = RequiredEmailValidationError; +} diff --git a/example/lib/async/ui/shared/forms/validation_errors/email_errors.freezed.dart b/example/lib/async/ui/shared/forms/validation_errors/email_errors.freezed.dart new file mode 100644 index 0000000..f04d108 --- /dev/null +++ b/example/lib/async/ui/shared/forms/validation_errors/email_errors.freezed.dart @@ -0,0 +1,439 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'email_errors.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$EmailValidationError { + @optionalTypeArgs + TResult when({ + required TResult Function() invalidFormat, + required TResult Function() alreadyExists, + required TResult Function() required, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function()? invalidFormat, + TResult Function()? alreadyExists, + TResult Function()? required, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? invalidFormat, + TResult Function()? alreadyExists, + TResult Function()? required, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(InvalidFormatEmailValidationError value) + invalidFormat, + required TResult Function(AlreadyExistsEmailValidationError value) + alreadyExists, + required TResult Function(RequiredEmailValidationError value) required, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(InvalidFormatEmailValidationError value)? invalidFormat, + TResult Function(AlreadyExistsEmailValidationError value)? alreadyExists, + TResult Function(RequiredEmailValidationError value)? required, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(InvalidFormatEmailValidationError value)? invalidFormat, + TResult Function(AlreadyExistsEmailValidationError value)? alreadyExists, + TResult Function(RequiredEmailValidationError value)? required, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $EmailValidationErrorCopyWith<$Res> { + factory $EmailValidationErrorCopyWith(EmailValidationError value, + $Res Function(EmailValidationError) then) = + _$EmailValidationErrorCopyWithImpl<$Res>; +} + +/// @nodoc +class _$EmailValidationErrorCopyWithImpl<$Res> + implements $EmailValidationErrorCopyWith<$Res> { + _$EmailValidationErrorCopyWithImpl(this._value, this._then); + + final EmailValidationError _value; + // ignore: unused_field + final $Res Function(EmailValidationError) _then; +} + +/// @nodoc +abstract class _$$InvalidFormatEmailValidationErrorCopyWith<$Res> { + factory _$$InvalidFormatEmailValidationErrorCopyWith( + _$InvalidFormatEmailValidationError value, + $Res Function(_$InvalidFormatEmailValidationError) then) = + __$$InvalidFormatEmailValidationErrorCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$InvalidFormatEmailValidationErrorCopyWithImpl<$Res> + extends _$EmailValidationErrorCopyWithImpl<$Res> + implements _$$InvalidFormatEmailValidationErrorCopyWith<$Res> { + __$$InvalidFormatEmailValidationErrorCopyWithImpl( + _$InvalidFormatEmailValidationError _value, + $Res Function(_$InvalidFormatEmailValidationError) _then) + : super(_value, (v) => _then(v as _$InvalidFormatEmailValidationError)); + + @override + _$InvalidFormatEmailValidationError get _value => + super._value as _$InvalidFormatEmailValidationError; +} + +/// @nodoc + +class _$InvalidFormatEmailValidationError + implements InvalidFormatEmailValidationError { + const _$InvalidFormatEmailValidationError(); + + @override + String toString() { + return 'EmailValidationError.invalidFormat()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$InvalidFormatEmailValidationError); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() invalidFormat, + required TResult Function() alreadyExists, + required TResult Function() required, + }) { + return invalidFormat(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function()? invalidFormat, + TResult Function()? alreadyExists, + TResult Function()? required, + }) { + return invalidFormat?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? invalidFormat, + TResult Function()? alreadyExists, + TResult Function()? required, + required TResult orElse(), + }) { + if (invalidFormat != null) { + return invalidFormat(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(InvalidFormatEmailValidationError value) + invalidFormat, + required TResult Function(AlreadyExistsEmailValidationError value) + alreadyExists, + required TResult Function(RequiredEmailValidationError value) required, + }) { + return invalidFormat(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(InvalidFormatEmailValidationError value)? invalidFormat, + TResult Function(AlreadyExistsEmailValidationError value)? alreadyExists, + TResult Function(RequiredEmailValidationError value)? required, + }) { + return invalidFormat?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(InvalidFormatEmailValidationError value)? invalidFormat, + TResult Function(AlreadyExistsEmailValidationError value)? alreadyExists, + TResult Function(RequiredEmailValidationError value)? required, + required TResult orElse(), + }) { + if (invalidFormat != null) { + return invalidFormat(this); + } + return orElse(); + } +} + +abstract class InvalidFormatEmailValidationError + implements EmailValidationError { + const factory InvalidFormatEmailValidationError() = + _$InvalidFormatEmailValidationError; +} + +/// @nodoc +abstract class _$$AlreadyExistsEmailValidationErrorCopyWith<$Res> { + factory _$$AlreadyExistsEmailValidationErrorCopyWith( + _$AlreadyExistsEmailValidationError value, + $Res Function(_$AlreadyExistsEmailValidationError) then) = + __$$AlreadyExistsEmailValidationErrorCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$AlreadyExistsEmailValidationErrorCopyWithImpl<$Res> + extends _$EmailValidationErrorCopyWithImpl<$Res> + implements _$$AlreadyExistsEmailValidationErrorCopyWith<$Res> { + __$$AlreadyExistsEmailValidationErrorCopyWithImpl( + _$AlreadyExistsEmailValidationError _value, + $Res Function(_$AlreadyExistsEmailValidationError) _then) + : super(_value, (v) => _then(v as _$AlreadyExistsEmailValidationError)); + + @override + _$AlreadyExistsEmailValidationError get _value => + super._value as _$AlreadyExistsEmailValidationError; +} + +/// @nodoc + +class _$AlreadyExistsEmailValidationError + implements AlreadyExistsEmailValidationError { + const _$AlreadyExistsEmailValidationError(); + + @override + String toString() { + return 'EmailValidationError.alreadyExists()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AlreadyExistsEmailValidationError); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() invalidFormat, + required TResult Function() alreadyExists, + required TResult Function() required, + }) { + return alreadyExists(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function()? invalidFormat, + TResult Function()? alreadyExists, + TResult Function()? required, + }) { + return alreadyExists?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? invalidFormat, + TResult Function()? alreadyExists, + TResult Function()? required, + required TResult orElse(), + }) { + if (alreadyExists != null) { + return alreadyExists(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(InvalidFormatEmailValidationError value) + invalidFormat, + required TResult Function(AlreadyExistsEmailValidationError value) + alreadyExists, + required TResult Function(RequiredEmailValidationError value) required, + }) { + return alreadyExists(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(InvalidFormatEmailValidationError value)? invalidFormat, + TResult Function(AlreadyExistsEmailValidationError value)? alreadyExists, + TResult Function(RequiredEmailValidationError value)? required, + }) { + return alreadyExists?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(InvalidFormatEmailValidationError value)? invalidFormat, + TResult Function(AlreadyExistsEmailValidationError value)? alreadyExists, + TResult Function(RequiredEmailValidationError value)? required, + required TResult orElse(), + }) { + if (alreadyExists != null) { + return alreadyExists(this); + } + return orElse(); + } +} + +abstract class AlreadyExistsEmailValidationError + implements EmailValidationError { + const factory AlreadyExistsEmailValidationError() = + _$AlreadyExistsEmailValidationError; +} + +/// @nodoc +abstract class _$$RequiredEmailValidationErrorCopyWith<$Res> { + factory _$$RequiredEmailValidationErrorCopyWith( + _$RequiredEmailValidationError value, + $Res Function(_$RequiredEmailValidationError) then) = + __$$RequiredEmailValidationErrorCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$RequiredEmailValidationErrorCopyWithImpl<$Res> + extends _$EmailValidationErrorCopyWithImpl<$Res> + implements _$$RequiredEmailValidationErrorCopyWith<$Res> { + __$$RequiredEmailValidationErrorCopyWithImpl( + _$RequiredEmailValidationError _value, + $Res Function(_$RequiredEmailValidationError) _then) + : super(_value, (v) => _then(v as _$RequiredEmailValidationError)); + + @override + _$RequiredEmailValidationError get _value => + super._value as _$RequiredEmailValidationError; +} + +/// @nodoc + +class _$RequiredEmailValidationError implements RequiredEmailValidationError { + const _$RequiredEmailValidationError(); + + @override + String toString() { + return 'EmailValidationError.required()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RequiredEmailValidationError); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() invalidFormat, + required TResult Function() alreadyExists, + required TResult Function() required, + }) { + return required(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function()? invalidFormat, + TResult Function()? alreadyExists, + TResult Function()? required, + }) { + return required?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? invalidFormat, + TResult Function()? alreadyExists, + TResult Function()? required, + required TResult orElse(), + }) { + if (required != null) { + return required(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(InvalidFormatEmailValidationError value) + invalidFormat, + required TResult Function(AlreadyExistsEmailValidationError value) + alreadyExists, + required TResult Function(RequiredEmailValidationError value) required, + }) { + return required(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(InvalidFormatEmailValidationError value)? invalidFormat, + TResult Function(AlreadyExistsEmailValidationError value)? alreadyExists, + TResult Function(RequiredEmailValidationError value)? required, + }) { + return required?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(InvalidFormatEmailValidationError value)? invalidFormat, + TResult Function(AlreadyExistsEmailValidationError value)? alreadyExists, + TResult Function(RequiredEmailValidationError value)? required, + required TResult orElse(), + }) { + if (required != null) { + return required(this); + } + return orElse(); + } +} + +abstract class RequiredEmailValidationError implements EmailValidationError { + const factory RequiredEmailValidationError() = _$RequiredEmailValidationError; +} diff --git a/example/lib/async/ui/shared/forms/validators/amount_validator.dart b/example/lib/async/ui/shared/forms/validators/amount_validator.dart new file mode 100644 index 0000000..fa8ad80 --- /dev/null +++ b/example/lib/async/ui/shared/forms/validators/amount_validator.dart @@ -0,0 +1,28 @@ +import 'package:example/async/ui/shared/forms/inputs/amount_input.dart'; +import 'package:example/async/ui/shared/forms/validation_errors/amount_errors.dart'; +import 'package:formz/formz.dart'; + +class AmountValidator + extends AsyncFormzInputValidator { + const AmountValidator(); + + @override + Future validate(Amount input) async { + if (input.isRequired && input.value.isEmpty) { + return const AmountValidationError.required(); + } + final parsed = double.tryParse(input.value); + if (parsed == null || parsed.isNaN) { + return AmountValidationError.parsing( + amount: input.value, + ); + } else if (parsed < input.min || parsed > input.max) { + return AmountValidationError.outOfRange( + amount: input.value, + min: input.min, + max: input.max, + ); + } + return null; + } +} diff --git a/example/lib/async/ui/shared/forms/validators/email_validator.dart b/example/lib/async/ui/shared/forms/validators/email_validator.dart new file mode 100644 index 0000000..77b4859 --- /dev/null +++ b/example/lib/async/ui/shared/forms/validators/email_validator.dart @@ -0,0 +1,50 @@ +import 'package:example/async/data/repositories/email_repository.dart'; +import 'package:example/async/ui/shared/forms/inputs/email_input.dart'; +import 'package:example/async/ui/shared/forms/validation_errors/email_errors.dart'; +import 'package:formz/formz.dart'; +import 'package:regexpattern/regexpattern.dart'; + +const _kEmailPattern = r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'; + +class EmailValidator + extends AsyncFormzInputValidator { + const EmailValidator({ + required EmailRepository emailRepository, + }) : _emailRepository = emailRepository; + + final EmailRepository _emailRepository; + + @override + bool canValidate(Email input) { + if (input.validationStatus.isValidated) { + return true; + } + final validFormat = RegVal.hasMatch( + input.value, + _kEmailPattern, + ); + return validFormat; + } + + @override + Future validate(Email input) async { + if (input.isRequired && input.value.isEmpty) { + return const EmailValidationError.required(); + } + + final validFormat = RegVal.hasMatch( + input.value, + _kEmailPattern, + ); + if (!validFormat) { + return const EmailValidationError.invalidFormat(); + } + + final alreadyExist = await _emailRepository.findByAddress(input.value); + if (alreadyExist) { + return const EmailValidationError.alreadyExists(); + } + + return null; + } +} diff --git a/example/lib/main.dart b/example/lib/sync/main.dart similarity index 97% rename from example/lib/main.dart rename to example/lib/sync/main.dart index c982d39..31fee5c 100644 --- a/example/lib/main.dart +++ b/example/lib/sync/main.dart @@ -6,7 +6,7 @@ import 'package:formz/formz.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); + const MyApp({super.key}); @override Widget build(BuildContext context) { @@ -23,7 +23,7 @@ class MyApp extends StatelessWidget { } class MyForm extends StatefulWidget { - const MyForm({Key? key}) : super(key: key); + const MyForm({super.key}); @override State createState() => _MyFormState(); @@ -191,7 +191,9 @@ class MyFormState with FormzMixin { enum EmailValidationError { invalid } class Email extends FormzInput { + // ignore: use_super_parameters const Email.pure([String value = '']) : super.pure(value); + // ignore: use_super_parameters const Email.dirty([String value = '']) : super.dirty(value); static final _emailRegExp = RegExp( @@ -207,7 +209,9 @@ class Email extends FormzInput { enum PasswordValidationError { invalid } class Password extends FormzInput { + // ignore: use_super_parameters const Password.pure([String value = '']) : super.pure(value); + // ignore: use_super_parameters const Password.dirty([String value = '']) : super.dirty(value); static final _passwordRegex = diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 9bf771e..d179d4d 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,17 +4,23 @@ version: 1.0.0+1 publish_to: none environment: - sdk: ">=2.15.1 <3.0.0" + sdk: ">=2.17.6 <3.0.0" dependencies: flutter: sdk: flutter + flutter_bloc: ^8.0.0 formz: path: ../ + freezed_annotation: ^2.1.0 + provider: ^6.0.3 + regexpattern: ^2.0.1 dev_dependencies: + build_runner: ^2.2.0 flutter_test: sdk: flutter + freezed: ^2.1.0+1 very_good_analysis: ^3.1.0 flutter: diff --git a/lib/formz.dart b/lib/formz.dart index 0be61bd..513ca22 100644 --- a/lib/formz.dart +++ b/lib/formz.dart @@ -38,6 +38,47 @@ extension FormzSubmissionStatusX on FormzSubmissionStatus { bool get isCanceled => this == FormzSubmissionStatus.canceled; } +/// {@template form_input} +/// A [FormzInputBase] represents the base class of the [FormzInput] and +/// [AsyncFormzInput] classes. This class is used to validate by the [Formz] +/// class its validate method. +/// +/// ```dart +/// final FormzInput syncInput = NameInput.dirty(value: 'jan'); +/// final AsyncFormzInput asyncInput = EmailInput.dirty(value: 'test@test.com'), +/// +/// const inputs = [ +/// FormzInput, +/// AsyncFormzInput, +/// ]; +/// +/// final status = Formz.validate(validInputs); +/// ``` +/// {@endtemplate} +@immutable +abstract class FormzInputBase { + /// The value of the given [FormzInput]. + /// For example, if you have a `FormzInput` for `FirstName`, + /// the value could be 'Joe'. + T get value; + + /// Whether the [FormzInput] value is valid according to the + /// overridden `validator`. + /// + /// Returns `true` if `validator` returns `null` for the + /// current [FormzInput] value and `false` otherwise. + bool get isValid; + + /// Whether the [FormzInput] value is not valid. + /// A value is invalid when the overridden `validator` + /// returns an error (non-null value). + bool get isNotValid => !isValid; + + /// Returns a validation error if the [FormzInput] is invalid. + /// Returns `null` if the [FormzInput] is valid. + E? get error; +} + /// {@template form_input} /// A [FormzInput] represents the value of a single form input field. /// It contains information about the [value] as well as validity. @@ -58,7 +99,7 @@ extension FormzSubmissionStatusX on FormzSubmissionStatus { /// ``` /// {@endtemplate} @immutable -abstract class FormzInput { +abstract class FormzInput implements FormzInputBase { const FormzInput._({required this.value, this.isPure = true}); /// Constructor which create a `pure` [FormzInput] with a given value. @@ -67,9 +108,7 @@ abstract class FormzInput { /// Constructor which create a `dirty` [FormzInput] with a given value. const FormzInput.dirty(T value) : this._(value: value, isPure: false); - /// The value of the given [FormzInput]. - /// For example, if you have a `FormzInput` for `FirstName`, - /// the value could be 'Joe'. + @override final T value; /// If the [FormzInput] is pure (has been touched/modified). @@ -82,20 +121,13 @@ abstract class FormzInput { /// the `FormzInput` has been manipulated. final bool isPure; - /// Whether the [FormzInput] value is valid according to the - /// overridden `validator`. - /// - /// Returns `true` if `validator` returns `null` for the - /// current [FormzInput] value and `false` otherwise. + @override bool get isValid => validator(value) == null; - /// Whether the [FormzInput] value is not valid. - /// A value is invalid when the overridden `validator` - /// returns an error (non-null value). + @override bool get isNotValid => !isValid; - /// Returns a validation error if the [FormzInput] is invalid. - /// Returns `null` if the [FormzInput] is valid. + @override E? get error => validator(value); /// The error to display if the [FormzInput] value @@ -130,7 +162,7 @@ abstract class FormzInput { class Formz { /// Returns a [bool] given a list of [FormzInput] indicating whether /// the inputs are all valid. - static bool validate(List> inputs) { + static bool validate(List> inputs) { return inputs.every((input) => input.isValid); } } @@ -166,5 +198,179 @@ mixin FormzMixin { /// /// Override this and give it all [FormzInput]s in your class that should be /// validated automatically. - List> get inputs; + List> get inputs; +} + +/// {@template async_form_input} +/// A [AsyncFormzInputValidator] represents a validator to asynchronously +/// validate a [AsyncFormzInput] field. +/// +/// ```dart +///class EmailValidator +/// extends AsyncFormInputValidator { +/// const EmailValidator({ +/// required EmailRepository emailRepository, +/// }) : _emailRepository = emailRepository; +/// +/// final EmailRepository _emailRepository; +/// +/// @override +/// Future validate(Email input) async { +/// if (input.required && input.value.isEmpty) { +/// return const EmailValidationError.required(); +/// } +/// +/// final alreadyExist = await _emailRepository.findByAddress(input.value); +/// if (alreadyExist) { +/// return const EmailValidationError.alreadyExists(); +/// } +/// +/// return null; +/// } +///} +/// ``` +/// {@endtemplate} +abstract class AsyncFormzInputValidator< + TInput extends AsyncFormzInput, TValue, TError> { + /// Constructor which creates a [AsyncFormzInputValidator]. + const AsyncFormzInputValidator(); + + /// Validates the [input] and returns a [Future] containing the validation + /// error. Returns null if the [input] is valid. + /// + /// ```dart + ///@override + ///Future validate(Email input) async { + /// if (input.required && input.value.isEmpty) { + /// return const EmailValidationError.required(); + /// } + /// + /// final validFormat = RegVal.hasMatch( + /// input.value, + /// _kEmailPattern, + /// ); + /// if (!validFormat) { + /// return const EmailValidationError.invalidFormat(); + /// } + /// + /// final alreadyExist = await _emailRepository.findByAddress(input.value); + /// if (alreadyExist) { + /// return const EmailValidationError.alreadyExists(); + /// } + /// + /// return null; + ///} + /// ``` + Future validate(TInput input); + + /// Returns whether the [TInput] is ready for validation. + /// + /// ```dart + /// @override + ///bool canValidate(Email input) { + /// if (input.validationStatus.isValidated) { + /// return true; + /// } + /// final validFormat = RegVal.hasMatch( + /// input.value, + /// _kEmailPattern, + /// ); + /// return validFormat; + ///} + /// ``` + bool canValidate(TInput input) => true; +} + +/// Enum representing the validation status of a [AsyncFormzInput] object. +enum AsyncFormzInputValidationStatus { + /// Indicates whether the [AsyncFormzInput] has not been validated. + pure, + + /// Indicates whether the [AsyncFormzInput] is being validated. + validating, + + /// Indicates whether the [AsyncFormzInput] has been validated. + validated, +} + +/// Useful extensions on [AsyncFormzInputValidationStatus] +extension AsyncFormzInputValidationStatusX on AsyncFormzInputValidationStatus { + /// Indicates whether the [AsyncFormzInput] has not been validated. + bool get isPure => this == AsyncFormzInputValidationStatus.pure; + + /// Indicates whether the [AsyncFormzInput] is being validated. + bool get isValidating => this == AsyncFormzInputValidationStatus.validating; + + /// Indicates whether the [AsyncFormzInput] is not being validated. + bool get isNotValidating => !isValidating; + + /// Indicates whether the [AsyncFormzInput] is validated. + bool get isValidated => this == AsyncFormzInputValidationStatus.validated; +} + +/// {@template form_input} +/// A [AsyncFormzInput] represents the value of a single form input field. +/// It contains information about the [value] as well as validity. +/// +/// [AsyncFormzInput] should be extended to define custom [AsyncFormzInput] +/// instances. +/// +/// ```dart +///class Email extends AsyncFormzInput { +/// const Email( +/// super.value, { +/// super.error, +/// super.validationStatus, +/// this.isRequired = true, +/// }); +/// +/// final bool isRequired; +///} +/// ``` +/// {@endtemplate} +@immutable +abstract class AsyncFormzInput implements FormzInputBase { + /// Constructor which create a [FormzInput] with a given value. + const AsyncFormzInput( + this.value, { + this.error, + AsyncFormzInputValidationStatus? validationStatus, + }) : validationStatus = + validationStatus ?? AsyncFormzInputValidationStatus.pure; + + @override + final T value; + + @override + bool get isValid => validationStatus.isValidated && error == null; + + @override + bool get isNotValid => !isValid; + + @override + final E? error; + + /// The validation status of the [AsyncFormzInput]. + final AsyncFormzInputValidationStatus validationStatus; + + @override + int get hashCode => Object.hashAll([value, error, validationStatus]); + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is AsyncFormzInput && + other.value == value && + other.error == error && + other.validationStatus == validationStatus; + } + + @override + String toString() => ''' +$runtimeType( + value: '$value', + error: $error, + validationStatus: $validationStatus, +)'''; } diff --git a/pubspec.yaml b/pubspec.yaml index 9acd118..8e7f931 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,7 @@ documentation: https://github.com/VeryGoodOpenSource/formz version: 0.5.0-dev.1 environment: - sdk: ">=2.14.0 <3.0.0" + sdk: ">=2.17.6 <3.0.0" dependencies: meta: ^1.7.0 diff --git a/test/formz_test.dart b/test/formz_test.dart index d729bce..6105514 100644 --- a/test/formz_test.dart +++ b/test/formz_test.dart @@ -2,6 +2,7 @@ import 'package:formz/formz.dart'; import 'package:test/test.dart'; +import 'helpers/formz_input_test_object.dart'; import 'helpers/helpers.dart'; void main() { @@ -158,61 +159,331 @@ void main() { }); }); - group('validate', () { - test('returns valid for empty inputs', () { - expect(Formz.validate([]), isTrue); + group('AsyncFormzInput', () { + test('value is correct', () { + expect(NameAsyncInput('joe').value, 'joe'); }); - test('returns valid for a pure/valid input', () { - expect(Formz.validate([NameInput.pure(value: 'joe')]), isTrue); + test('error is correct', () { + const error = NameAsyncInputError.empty; + final input = NameAsyncInput( + 'joe', + error: error, + ); + expect(input.error, error); }); - test('returns valid for a dirty/valid input', () { - expect(Formz.validate([NameInput.dirty(value: 'joe')]), isTrue); + test('validationStatus is correct', () { + const validationStatus = AsyncFormzInputValidationStatus.validating; + final input = NameAsyncInput( + 'joe', + validationStatus: validationStatus, + ); + expect(input.validationStatus, validationStatus); + }); + + test('default validationStatus is correct', () { + final input = NameAsyncInput('joe'); + expect(input.validationStatus, AsyncFormzInputValidationStatus.pure); + }); + + group('isValid is correct', () { + test('isValid is true when status is validated and error is null', () { + final input = NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.validated, + ); + expect(input.isValid, isTrue); + }); + test('isValid is false when status is validated and error is not null', + () { + final input = NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.validated, + error: NameAsyncInputError.empty, + ); + expect(input.isValid, isFalse); + }); + test('isValid is false when status not is validated and error is null', + () { + final input = NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.pure, + ); + expect(input.isValid, isFalse); + }); + + test( + '''isValid is false when status not is validated and error is not null''', + () { + final input = NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.pure, + error: NameAsyncInputError.empty, + ); + expect(input.isValid, isFalse); + }); + }); + + group('isNotValid is correct', () { + test('isNotValid is false when status is validated and error is null', + () { + final input = NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.validated, + ); + expect(input.isNotValid, isFalse); + }); + test( + 'isNotValid is true when status is validated and error is not null', + () { + final input = NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.validated, + error: NameAsyncInputError.empty, + ); + expect(input.isNotValid, isTrue); + }); + test( + 'isNotValid is true when status not is validated and error is null', + () { + final input = NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.pure, + ); + expect(input.isNotValid, isTrue); + }); + + test( + '''isNotValid is true when status not is validated and error is not null''', + () { + final input = NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.pure, + error: NameAsyncInputError.empty, + ); + expect(input.isNotValid, isTrue); + }); }); - test('returns valid for multiple valid inputs', () { + test('hashCode is correct', () { + final input = NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.validated, + error: NameAsyncInputError.empty, + ); expect( - Formz.validate([ - NameInput.dirty(value: 'jen'), - NameInput.dirty(value: 'bob'), - NameInput.dirty(value: 'alex'), + input.hashCode, + Object.hashAll([ + input.value, + input.error, + input.validationStatus, ]), - isTrue, ); }); - test('returns invalid for a pure/invalid input', () { - expect(Formz.validate([NameInput.pure()]), isFalse); - }); - - test('returns invalid for a dirty/invalid input', () { - expect(Formz.validate([NameInput.dirty()]), isFalse); - }); - - test('returns invalid for multiple invalid inputs', () { + test('== is value based', () { expect( - Formz.validate([ - NameInput.dirty(), - NameInput.dirty(), - NameInput.dirty(), - ]), - isFalse, + NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.validated, + error: NameAsyncInputError.empty, + ), + equals( + NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.validated, + error: NameAsyncInputError.empty, + ), + ), + ); + expect( + NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.validated, + error: NameAsyncInputError.empty, + ), + isNot( + equals( + NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.pure, + error: NameAsyncInputError.empty, + ), + ), + ), + ); + expect( + NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.validated, + ), + isNot( + equals( + NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.validated, + error: NameAsyncInputError.empty, + ), + ), + ), + ); + expect( + NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.pure, + error: NameAsyncInputError.empty, + ), + isNot( + equals( + NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.validated, + ), + ), + ), + ); + expect( + NameAsyncInput( + 'jop', + validationStatus: AsyncFormzInputValidationStatus.validated, + error: NameAsyncInputError.empty, + ), + isNot( + equals( + NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.validated, + error: NameAsyncInputError.empty, + ), + ), + ), ); }); - test('returns invalid when at least one input is invalid', () { + test('toString is overridden correctly', () { expect( - Formz.validate([ - NameInput.dirty(value: 'jan'), - NameInput.dirty(value: 'jim'), - NameInput.dirty(), - ]), - isFalse, + NameAsyncInput( + 'joe', + error: NameAsyncInputError.empty, + validationStatus: AsyncFormzInputValidationStatus.pure, + ).toString(), + equals( + ''' +NameAsyncInput( + value: 'joe', + error: NameAsyncInputError.empty, + validationStatus: AsyncFormzInputValidationStatus.pure, +)''', + ), ); }); }); + group('Formz.validate', () { + group('with FormzInput', () { + test('returns valid for empty inputs', () { + expect(Formz.validate([]), isTrue); + }); + + test('returns valid for a pure/valid input', () { + expect(Formz.validate([NameInput.pure(value: 'joe')]), isTrue); + }); + + test('returns valid for a dirty/valid input', () { + expect(Formz.validate([NameInput.dirty(value: 'joe')]), isTrue); + }); + + test('returns valid for multiple valid inputs', () { + expect( + Formz.validate([ + NameInput.dirty(value: 'jen'), + NameInput.dirty(value: 'bob'), + NameInput.dirty(value: 'alex'), + ]), + isTrue, + ); + }); + + test('returns invalid for a pure/invalid input', () { + expect(Formz.validate([NameInput.pure()]), isFalse); + }); + + test('returns invalid for a dirty/invalid input', () { + expect(Formz.validate([NameInput.dirty()]), isFalse); + }); + + test('returns invalid for multiple invalid inputs', () { + expect( + Formz.validate([ + NameInput.dirty(), + NameInput.dirty(), + NameInput.dirty(), + ]), + isFalse, + ); + }); + + test('returns invalid when at least one input is invalid', () { + expect( + Formz.validate([ + NameInput.dirty(value: 'jan'), + NameInput.dirty(value: 'jim'), + NameInput.dirty(), + ]), + isFalse, + ); + }); + }); + group('with AsyncFormzInput', () { + test('returns valid for empty inputs', () { + expect(Formz.validate([]), isTrue); + }); + + test('returns valid for a valid input', () { + final input = NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.validated, + ); + expect(Formz.validate([input]), isTrue); + }); + + test('returns invalid for a invalid input', () { + final input = NameAsyncInput( + 'joe', + error: NameAsyncInputError.empty, + ); + expect(Formz.validate([input]), isFalse); + }); + + test('returns invalid for multiple invalid inputs', () { + final input = NameAsyncInput( + 'joe', + error: NameAsyncInputError.empty, + ); + expect( + Formz.validate([input, input, input]), + isFalse, + ); + }); + + test('returns invalid when at least one input is invalid', () { + final validInput = NameAsyncInput( + 'joe', + validationStatus: AsyncFormzInputValidationStatus.validated, + ); + final invalidInput = NameAsyncInput( + 'joe', + error: NameAsyncInputError.empty, + ); + expect( + Formz.validate([validInput, validInput, invalidInput]), + isFalse, + ); + }); + }); + }); + group('FormzSubmissionStatusX', () { test('isInitial returns true', () { expect(FormzSubmissionStatus.initial.isInitial, isTrue); @@ -234,5 +505,42 @@ void main() { expect(FormzSubmissionStatus.canceled.isCanceled, isTrue); }); }); + + group('AsyncFormzInputValidationStatusX', () { + test('isPure returns true', () { + expect(AsyncFormzInputValidationStatus.pure.isPure, isTrue); + }); + test('isValidating returns true', () { + expect(AsyncFormzInputValidationStatus.validating.isValidating, isTrue); + }); + test('isNotValidating returns false', () { + expect( + AsyncFormzInputValidationStatus.validating.isNotValidating, + isFalse, + ); + }); + test('isValidated returns true', () { + expect(AsyncFormzInputValidationStatus.validated.isValidated, isTrue); + }); + }); + + group('AsyncFormzInputValidator', () { + group('canValidate', () { + test('default value is correct', () { + final validator = NameAsyncFormzInputValidator(); + final input = NameAsyncInput('joe'); + expect(validator.canValidate(input), isTrue); + }); + }); + }); + + group('FormzInputBase', () { + test('isNotValid is reversed of isValid', () { + final input = FormzInputIsValidTestObject( + isValid: true, + ); + expect(input.isNotValid, !input.isValid); + }); + }); }); } diff --git a/test/helpers/formz_input_test_object.dart b/test/helpers/formz_input_test_object.dart new file mode 100644 index 0000000..62a4c43 --- /dev/null +++ b/test/helpers/formz_input_test_object.dart @@ -0,0 +1,16 @@ +import 'package:formz/formz.dart'; + +class FormzInputIsValidTestObject extends FormzInputBase { + FormzInputIsValidTestObject({ + required this.isValid, + }); + + @override + dynamic get error => throw UnimplementedError(); + + @override + final bool isValid; + + @override + dynamic get value => throw UnimplementedError(); +} diff --git a/test/helpers/helpers.dart b/test/helpers/helpers.dart index 82d2c49..acc4786 100644 --- a/test/helpers/helpers.dart +++ b/test/helpers/helpers.dart @@ -1 +1,2 @@ +export 'name_async_input.dart'; export 'name_input.dart'; diff --git a/test/helpers/name_async_input.dart b/test/helpers/name_async_input.dart new file mode 100644 index 0000000..28a6462 --- /dev/null +++ b/test/helpers/name_async_input.dart @@ -0,0 +1,19 @@ +import 'package:formz/formz.dart'; + +enum NameAsyncInputError { empty } + +class NameAsyncInput extends AsyncFormzInput { + const NameAsyncInput( + super.value, { + super.error, + super.validationStatus, + }); +} + +class NameAsyncFormzInputValidator extends AsyncFormzInputValidator< + NameAsyncInput, String, NameAsyncInputError> { + @override + Future validate(NameAsyncInput input) async { + throw UnimplementedError(); + } +}