From 13d20c50af45c47b5b1e1a1ece9ef0c151747483 Mon Sep 17 00:00:00 2001 From: Jop Middelkamp Date: Wed, 24 Aug 2022 14:08:26 +0200 Subject: [PATCH 01/11] Added async formz --- .vscode/launch.json | 27 + .../data/repositories/email_repository.dart | 8 + example/lib/async/main.dart | 6 + example/lib/async/ui/app.dart | 19 + .../register/cubit/register_cubit.dart | 68 +++ .../register/cubit/register_state.dart | 14 + .../cubit/register_state.freezed.dart | 175 ++++++ .../feature/register/view/register_page.dart | 23 + .../feature/register/view/register_view.dart | 164 ++++++ .../ui/shared/components/form_label.dart | 28 + .../ui/shared/forms/inputs/amount_input.dart | 145 +++++ .../ui/shared/forms/inputs/email_input.dart | 106 ++++ .../validation_errors/amount_errors.dart | 19 + .../amount_errors.freezed.dart | 512 ++++++++++++++++++ .../forms/validation_errors/email_errors.dart | 14 + .../email_errors.freezed.dart | 439 +++++++++++++++ .../forms/validators/amount_validator.dart | 28 + .../forms/validators/email_validator.dart | 38 ++ example/lib/{ => sync}/main.dart | 0 example/pubspec.yaml | 9 +- lib/formz.dart | 226 +++++++- 21 files changed, 2044 insertions(+), 24 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 example/lib/async/data/repositories/email_repository.dart create mode 100644 example/lib/async/main.dart create mode 100644 example/lib/async/ui/app.dart create mode 100644 example/lib/async/ui/feature/register/cubit/register_cubit.dart create mode 100644 example/lib/async/ui/feature/register/cubit/register_state.dart create mode 100644 example/lib/async/ui/feature/register/cubit/register_state.freezed.dart create mode 100644 example/lib/async/ui/feature/register/view/register_page.dart create mode 100644 example/lib/async/ui/feature/register/view/register_view.dart create mode 100644 example/lib/async/ui/shared/components/form_label.dart create mode 100644 example/lib/async/ui/shared/forms/inputs/amount_input.dart create mode 100644 example/lib/async/ui/shared/forms/inputs/email_input.dart create mode 100644 example/lib/async/ui/shared/forms/validation_errors/amount_errors.dart create mode 100644 example/lib/async/ui/shared/forms/validation_errors/amount_errors.freezed.dart create mode 100644 example/lib/async/ui/shared/forms/validation_errors/email_errors.dart create mode 100644 example/lib/async/ui/shared/forms/validation_errors/email_errors.freezed.dart create mode 100644 example/lib/async/ui/shared/forms/validators/amount_validator.dart create mode 100644 example/lib/async/ui/shared/forms/validators/email_validator.dart rename example/lib/{ => sync}/main.dart (100%) 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/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..49aab18 --- /dev/null +++ b/example/lib/async/ui/feature/register/cubit/register_cubit.dart @@ -0,0 +1,68 @@ +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.copyWithExceptError( + value: value, + validating: true, + ), + ), + ); + emit( + state.copyWith( + amount: state.amount.copyWith( + error: await _amountValidator.validate(state.amount), + validating: false, + ), + ), + ); + _updateFormState(); + } + + Future emailChanged(String value) async { + emit( + state.copyWith( + email: state.email.copyWithExceptError( + value: value, + validating: true, + ), + ), + ); + emit( + state.copyWith( + email: state.email.copyWith( + error: await _emailValidator.validate(state.email), + validating: false, + ), + ), + ); + _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..728cf7f --- /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.pure(max: 100)) Amount amount, + @Default(Email.pure()) 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..1b580d0 --- /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.pure(max: 100), + this.email = const Email.pure()}); + + @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..dca6462 --- /dev/null +++ b/example/lib/async/ui/feature/register/view/register_view.dart @@ -0,0 +1,164 @@ +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: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: const Text('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: const Text('Email'), + child: TextFormField( + controller: textController, + focusNode: textFocusNode, + onChanged: cubit.emailChanged, + decoration: InputDecoration( + hintText: 'Enter your email', + helperText: email.validating ? 'Validating...' : null, + errorText: !email.validating + ? email.error?.map( + invalidFormat: (_) => 'Invalid email format', + alreadyExists: (_) => 'Email already exists', + required: (_) => 'Email is required', + ) + : null, + ), + keyboardType: TextInputType.emailAddress, + ), + ); + } +} 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..b498868 --- /dev/null +++ b/example/lib/async/ui/shared/forms/inputs/amount_input.dart @@ -0,0 +1,145 @@ +import 'package:decimal/decimal.dart'; +import 'package:example/async/ui/shared/forms/validation_errors/amount_errors.dart'; +import 'package:formz/formz.dart'; + +const _kDefaultRequired = true; +const _kDefaultMin = 0.0; +const _kDefaultMax = double.maxFinite; + +class Amount extends AsyncFormzInput { + const Amount( + String value, { + bool isPure = true, + AmountValidationError? error, + bool validating = false, + this.isRequired = _kDefaultRequired, + this.min = _kDefaultMin, + this.max = _kDefaultMax, + }) : super( + value, + isPure: isPure, + error: error, + validating: validating, + ); + + const Amount.pure({ + bool isRequired = _kDefaultRequired, + double min = _kDefaultMin, + double max = _kDefaultMax, + }) : this( + '', + isPure: true, + isRequired: isRequired, + min: min, + max: max, + validating: false, + ); + + const Amount.dirty( + String value, { + bool isRequired = _kDefaultRequired, + double min = _kDefaultMin, + double max = _kDefaultMax, + AmountValidationError? error, + bool validating = false, + }) : this( + value, + isPure: false, + isRequired: isRequired, + min: min, + max: max, + error: error, + validating: validating, + ); + + final bool isRequired; + final double min; + final double max; + + Amount copyWith({ + String? value, + AmountValidationError? error, + bool? validating, + bool? isRequired, + double? min, + double? max, + }) { + final newValue = value ?? this.value; + return Amount( + newValue, + isPure: isPure && newValue.isEmpty, + error: error ?? this.error, + validating: validating ?? this.validating, + isRequired: isRequired ?? this.isRequired, + min: min ?? this.min, + max: max ?? this.max, + ); + } + + Amount copyWithExceptError({ + String? value, + bool? validating, + bool? isRequired, + double? min, + double? max, + }) { + final newValue = value ?? this.value; + return Amount( + newValue, + isPure: isPure && newValue.isEmpty, + validating: validating ?? this.validating, + isRequired: isRequired ?? this.isRequired, + min: min ?? this.min, + max: max ?? this.max, + ); + } + + double toDouble() => double.parse(value); + + Decimal toDecimal() => Decimal.parse(value); + + double? toDoubleOrNull() => double.tryParse(value); + + Decimal? toDecimalOrNull() => Decimal.tryParse(value); + + Decimal toDecimalOr(Decimal or) => Decimal.tryParse(value) ?? or; + + Decimal toDecimalOrZero() => toDecimalOr(Decimal.zero); + + @override + String toString() { + return ''' +Amount( + isPure: $isPure, + value: '$value', + error: ${error.runtimeType}, + validating: $validating, + isRequired: $isRequired, + min: $min, + max: $max +)'''; + } + + @override + int get hashCode => + value.hashCode ^ + isPure.hashCode ^ + error.hashCode ^ + validating.hashCode ^ + isRequired.hashCode ^ + min.hashCode ^ + max.hashCode; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) return false; + return other is Amount && + other.value == value && + other.isPure == isPure && + other.error == error && + other.validating == validating && + other.isRequired == isRequired && + other.min == min && + other.max == 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..283f922 --- /dev/null +++ b/example/lib/async/ui/shared/forms/inputs/email_input.dart @@ -0,0 +1,106 @@ +import 'package:example/async/ui/shared/forms/validation_errors/email_errors.dart'; +import 'package:formz/formz.dart'; + +const _kDefaultRequired = true; + +class Email extends AsyncFormzInput { + const Email( + String value, { + bool isPure = true, + EmailValidationError? error, + bool validating = false, + this.isRequired = _kDefaultRequired, + }) : super( + value, + isPure: isPure, + error: error, + validating: validating, + ); + + const Email.pure({ + bool isRequired = _kDefaultRequired, + }) : this( + '', + isPure: true, + isRequired: isRequired, + validating: false, + ); + + const Email.dirty( + String value, { + bool isRequired = _kDefaultRequired, + EmailValidationError? error, + bool validating = false, + }) : this( + value, + isPure: false, + isRequired: isRequired, + error: error, + validating: validating, + ); + + final bool isRequired; + + Email copyWith({ + String? value, + EmailValidationError? error, + bool? validating, + bool? isRequired, + }) { + final newValue = value ?? this.value; + return Email( + newValue, + isPure: isPure && newValue.isEmpty, + error: error ?? this.error, + validating: validating ?? this.validating, + isRequired: isRequired ?? this.isRequired, + ); + } + + Email copyWithExceptError({ + String? value, + bool? validating, + bool? isRequired, + }) { + final newValue = value ?? this.value; + return Email( + newValue, + isPure: isPure && newValue.isEmpty, + validating: validating ?? this.validating, + isRequired: isRequired ?? this.isRequired, + ); + } + + @override + String toString() { + return ''' +Email( + isPure: $isPure, + value: '$value', + error: ${error.runtimeType}, + validating: $validating, + isRequired: $isRequired +)'''; + } + + @override + int get hashCode => + value.hashCode ^ + isPure.hashCode ^ + error.hashCode ^ + validating.hashCode ^ + isRequired.hashCode; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is Email && + other.value == value && + other.isPure == isPure && + other.error == error && + other.validating == validating && + other.isRequired == 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..b68606b --- /dev/null +++ b/example/lib/async/ui/shared/forms/validators/email_validator.dart @@ -0,0 +1,38 @@ +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 + 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 100% rename from example/lib/main.dart rename to example/lib/sync/main.dart diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 9bf771e..5ca1cbe 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,17 +4,24 @@ version: 1.0.0+1 publish_to: none environment: - sdk: ">=2.15.1 <3.0.0" + sdk: ">=2.17.6 <3.0.0" dependencies: + decimal: ^2.2.0 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..a62df96 100644 --- a/lib/formz.dart +++ b/lib/formz.dart @@ -38,6 +38,57 @@ 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; + + /// If the [FormzInput] is pure (has been touched/modified). + /// Typically when the `FormzInput` is initially created, + /// it is created using the `FormzInput.pure` constructor to + /// signify that the user has not modified it. + /// + /// For subsequent changes (in response to user input), the + /// `FormzInput.dirty` constructor should be used to signify that + /// the `FormzInput` has been manipulated. + bool get 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. + 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 +109,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,35 +118,19 @@ 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). - /// Typically when the `FormzInput` is initially created, - /// it is created using the `FormzInput.pure` constructor to - /// signify that the user has not modified it. - /// - /// For subsequent changes (in response to user input), the - /// `FormzInput.dirty` constructor should be used to signify that - /// the `FormzInput` has been manipulated. + @override 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 +165,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); } } @@ -168,3 +203,148 @@ mixin FormzMixin { /// validated automatically. 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 alreadyExist = await _emailRepository.findByAddress(input.value); + /// if (alreadyExist) { + /// return const EmailValidationError.alreadyExists(); + /// } + /// + /// return null; + ///} + /// ``` + @protected + Future validate(TInput input); +} + +/// {@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 +/// enum FirstNameError { empty } +/// class FirstName extends FormzInput { +/// const FirstName.pure({String value = ''}) : super.pure(value); +/// const FirstName.dirty({String value = ''}) : super.dirty(value); +/// +/// @override +/// FirstNameError? validator(String value) { +/// return value.isEmpty ? FirstNameError.empty : null; +/// } +/// } +/// ``` +/// {@endtemplate} +@immutable +abstract class AsyncFormzInput implements FormzInputBase { + /// Constructor which create a [FormzInput] with a given value. + const AsyncFormzInput( + this.value, { + this.isPure = true, + this.error, + this.validating = false, + }); + + /// Constructor which create a `pure` [AsyncFormzInput] with a given value. + const AsyncFormzInput.pure(T value) : this(value); + + /// Constructor which create a `dirty` [AsyncFormzInput] with a given value. + const AsyncFormzInput.dirty( + T value, { + E? error, + bool validating = false, + }) : this( + value, + isPure: false, + error: error, + validating: validating, + ); + + @override + final T value; + + @override + final bool isPure; + + @override + bool get isValid => error == null; + + @override + bool get isNotValid => !isValid; + + @override + final E? error; + + /// Whether the [FormzInput] value is busy validating. + final bool validating; + + @override + int get hashCode => + value.hashCode ^ isPure.hashCode ^ error.hashCode ^ validating.hashCode; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is AsyncFormzInput && + other.value == value && + other.isPure == isPure && + other.error == error && + other.validating == validating; + } + + @override + String toString() => ''' +$runtimeType( + value: $value, + isPure: $isPure, + error: $error, + validating: $validating +)'''; +} From 9ccabb4a2d76052bcf5971d29abe07954d4fe56e Mon Sep 17 00:00:00 2001 From: Jop Middelkamp Date: Thu, 25 Aug 2022 01:04:34 +0200 Subject: [PATCH 02/11] Naming improvement --- .../register/cubit/register_cubit.dart | 8 +++---- .../feature/register/view/register_view.dart | 4 ++-- .../ui/shared/forms/inputs/amount_input.dart | 24 +++++++++---------- .../ui/shared/forms/inputs/email_input.dart | 24 +++++++++---------- lib/formz.dart | 16 ++++++------- 5 files changed, 38 insertions(+), 38 deletions(-) diff --git a/example/lib/async/ui/feature/register/cubit/register_cubit.dart b/example/lib/async/ui/feature/register/cubit/register_cubit.dart index 49aab18..d1a57fa 100644 --- a/example/lib/async/ui/feature/register/cubit/register_cubit.dart +++ b/example/lib/async/ui/feature/register/cubit/register_cubit.dart @@ -20,7 +20,7 @@ class RegisterCubit extends Cubit { state.copyWith( amount: state.amount.copyWithExceptError( value: value, - validating: true, + isValidating: true, ), ), ); @@ -28,7 +28,7 @@ class RegisterCubit extends Cubit { state.copyWith( amount: state.amount.copyWith( error: await _amountValidator.validate(state.amount), - validating: false, + isValidating: false, ), ), ); @@ -40,7 +40,7 @@ class RegisterCubit extends Cubit { state.copyWith( email: state.email.copyWithExceptError( value: value, - validating: true, + isValidating: true, ), ), ); @@ -48,7 +48,7 @@ class RegisterCubit extends Cubit { state.copyWith( email: state.email.copyWith( error: await _emailValidator.validate(state.email), - validating: false, + isValidating: false, ), ), ); diff --git a/example/lib/async/ui/feature/register/view/register_view.dart b/example/lib/async/ui/feature/register/view/register_view.dart index dca6462..bd96f32 100644 --- a/example/lib/async/ui/feature/register/view/register_view.dart +++ b/example/lib/async/ui/feature/register/view/register_view.dart @@ -148,8 +148,8 @@ class _EmailTextFieldState extends State<_EmailTextField> { onChanged: cubit.emailChanged, decoration: InputDecoration( hintText: 'Enter your email', - helperText: email.validating ? 'Validating...' : null, - errorText: !email.validating + helperText: email.isValidating ? 'Validating...' : null, + errorText: !email.isValidating ? email.error?.map( invalidFormat: (_) => 'Invalid email format', alreadyExists: (_) => 'Email already exists', diff --git a/example/lib/async/ui/shared/forms/inputs/amount_input.dart b/example/lib/async/ui/shared/forms/inputs/amount_input.dart index b498868..061c682 100644 --- a/example/lib/async/ui/shared/forms/inputs/amount_input.dart +++ b/example/lib/async/ui/shared/forms/inputs/amount_input.dart @@ -11,7 +11,7 @@ class Amount extends AsyncFormzInput { String value, { bool isPure = true, AmountValidationError? error, - bool validating = false, + bool isValidating = false, this.isRequired = _kDefaultRequired, this.min = _kDefaultMin, this.max = _kDefaultMax, @@ -19,7 +19,7 @@ class Amount extends AsyncFormzInput { value, isPure: isPure, error: error, - validating: validating, + isValidating: isValidating, ); const Amount.pure({ @@ -32,7 +32,7 @@ class Amount extends AsyncFormzInput { isRequired: isRequired, min: min, max: max, - validating: false, + isValidating: false, ); const Amount.dirty( @@ -41,7 +41,7 @@ class Amount extends AsyncFormzInput { double min = _kDefaultMin, double max = _kDefaultMax, AmountValidationError? error, - bool validating = false, + bool isValidating = false, }) : this( value, isPure: false, @@ -49,7 +49,7 @@ class Amount extends AsyncFormzInput { min: min, max: max, error: error, - validating: validating, + isValidating: isValidating, ); final bool isRequired; @@ -59,7 +59,7 @@ class Amount extends AsyncFormzInput { Amount copyWith({ String? value, AmountValidationError? error, - bool? validating, + bool? isValidating, bool? isRequired, double? min, double? max, @@ -69,7 +69,7 @@ class Amount extends AsyncFormzInput { newValue, isPure: isPure && newValue.isEmpty, error: error ?? this.error, - validating: validating ?? this.validating, + isValidating: isValidating ?? this.isValidating, isRequired: isRequired ?? this.isRequired, min: min ?? this.min, max: max ?? this.max, @@ -78,7 +78,7 @@ class Amount extends AsyncFormzInput { Amount copyWithExceptError({ String? value, - bool? validating, + bool? isValidating, bool? isRequired, double? min, double? max, @@ -87,7 +87,7 @@ class Amount extends AsyncFormzInput { return Amount( newValue, isPure: isPure && newValue.isEmpty, - validating: validating ?? this.validating, + isValidating: isValidating ?? this.isValidating, isRequired: isRequired ?? this.isRequired, min: min ?? this.min, max: max ?? this.max, @@ -113,7 +113,7 @@ Amount( isPure: $isPure, value: '$value', error: ${error.runtimeType}, - validating: $validating, + isValidating: $isValidating, isRequired: $isRequired, min: $min, max: $max @@ -125,7 +125,7 @@ Amount( value.hashCode ^ isPure.hashCode ^ error.hashCode ^ - validating.hashCode ^ + isValidating.hashCode ^ isRequired.hashCode ^ min.hashCode ^ max.hashCode; @@ -137,7 +137,7 @@ Amount( other.value == value && other.isPure == isPure && other.error == error && - other.validating == validating && + other.isValidating == isValidating && other.isRequired == isRequired && other.min == min && other.max == 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 index 283f922..20adada 100644 --- a/example/lib/async/ui/shared/forms/inputs/email_input.dart +++ b/example/lib/async/ui/shared/forms/inputs/email_input.dart @@ -8,13 +8,13 @@ class Email extends AsyncFormzInput { String value, { bool isPure = true, EmailValidationError? error, - bool validating = false, + bool isValidating = false, this.isRequired = _kDefaultRequired, }) : super( value, isPure: isPure, error: error, - validating: validating, + isValidating: isValidating, ); const Email.pure({ @@ -23,20 +23,20 @@ class Email extends AsyncFormzInput { '', isPure: true, isRequired: isRequired, - validating: false, + isValidating: false, ); const Email.dirty( String value, { bool isRequired = _kDefaultRequired, EmailValidationError? error, - bool validating = false, + bool isValidating = false, }) : this( value, isPure: false, isRequired: isRequired, error: error, - validating: validating, + isValidating: isValidating, ); final bool isRequired; @@ -44,7 +44,7 @@ class Email extends AsyncFormzInput { Email copyWith({ String? value, EmailValidationError? error, - bool? validating, + bool? isValidating, bool? isRequired, }) { final newValue = value ?? this.value; @@ -52,21 +52,21 @@ class Email extends AsyncFormzInput { newValue, isPure: isPure && newValue.isEmpty, error: error ?? this.error, - validating: validating ?? this.validating, + isValidating: isValidating ?? this.isValidating, isRequired: isRequired ?? this.isRequired, ); } Email copyWithExceptError({ String? value, - bool? validating, + bool? isValidating, bool? isRequired, }) { final newValue = value ?? this.value; return Email( newValue, isPure: isPure && newValue.isEmpty, - validating: validating ?? this.validating, + isValidating: isValidating ?? this.isValidating, isRequired: isRequired ?? this.isRequired, ); } @@ -78,7 +78,7 @@ Email( isPure: $isPure, value: '$value', error: ${error.runtimeType}, - validating: $validating, + isValidating: $isValidating, isRequired: $isRequired )'''; } @@ -88,7 +88,7 @@ Email( value.hashCode ^ isPure.hashCode ^ error.hashCode ^ - validating.hashCode ^ + isValidating.hashCode ^ isRequired.hashCode; @override @@ -100,7 +100,7 @@ Email( other.value == value && other.isPure == isPure && other.error == error && - other.validating == validating && + other.isValidating == isValidating && other.isRequired == isRequired; } } diff --git a/lib/formz.dart b/lib/formz.dart index a62df96..c2afc29 100644 --- a/lib/formz.dart +++ b/lib/formz.dart @@ -287,7 +287,7 @@ abstract class AsyncFormzInput implements FormzInputBase { this.value, { this.isPure = true, this.error, - this.validating = false, + this.isValidating = false, }); /// Constructor which create a `pure` [AsyncFormzInput] with a given value. @@ -297,12 +297,12 @@ abstract class AsyncFormzInput implements FormzInputBase { const AsyncFormzInput.dirty( T value, { E? error, - bool validating = false, + bool isValidating = false, }) : this( value, isPure: false, error: error, - validating: validating, + isValidating: isValidating, ); @override @@ -320,12 +320,12 @@ abstract class AsyncFormzInput implements FormzInputBase { @override final E? error; - /// Whether the [FormzInput] value is busy validating. - final bool validating; + /// Whether the [FormzInput] value is busy isValidating. + final bool isValidating; @override int get hashCode => - value.hashCode ^ isPure.hashCode ^ error.hashCode ^ validating.hashCode; + value.hashCode ^ isPure.hashCode ^ error.hashCode ^ isValidating.hashCode; @override bool operator ==(Object other) { @@ -336,7 +336,7 @@ abstract class AsyncFormzInput implements FormzInputBase { other.value == value && other.isPure == isPure && other.error == error && - other.validating == validating; + other.isValidating == isValidating; } @override @@ -345,6 +345,6 @@ $runtimeType( value: $value, isPure: $isPure, error: $error, - validating: $validating + isValidating: $isValidating )'''; } From c7ce923f6b5f088ab6c11667bff85b7dbfbb39d7 Mon Sep 17 00:00:00 2001 From: Jop Middelkamp Date: Thu, 25 Aug 2022 21:53:08 +0200 Subject: [PATCH 03/11] Updated for a better version with extra control of when the validation happens --- example/analysis_options.yaml | 1 + .../register/cubit/register_cubit.dart | 54 ++++---- .../register/cubit/register_state.dart | 4 +- .../cubit/register_state.freezed.dart | 4 +- .../feature/register/view/register_view.dart | 6 +- .../ui/shared/forms/inputs/amount_input.dart | 121 ++---------------- .../ui/shared/forms/inputs/email_input.dart | 90 ++----------- .../forms/validators/email_validator.dart | 12 ++ example/pubspec.yaml | 1 - lib/formz.dart | 74 +++++------ 10 files changed, 105 insertions(+), 262 deletions(-) diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index ff9c544..53e8522 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -2,3 +2,4 @@ include: package:very_good_analysis/analysis_options.3.1.0.yaml linter: rules: public_member_api_docs: false + require_trailing_commas: false diff --git a/example/lib/async/ui/feature/register/cubit/register_cubit.dart b/example/lib/async/ui/feature/register/cubit/register_cubit.dart index d1a57fa..3e5bed1 100644 --- a/example/lib/async/ui/feature/register/cubit/register_cubit.dart +++ b/example/lib/async/ui/feature/register/cubit/register_cubit.dart @@ -16,42 +16,38 @@ class RegisterCubit extends Cubit { final EmailValidator _emailValidator; Future amountChanged(String value) async { - emit( - state.copyWith( - amount: state.amount.copyWithExceptError( - value: value, - isValidating: true, - ), + emit(state.copyWith( + amount: state.amount.copyWithErrorReset(value: value), + )); + final error = await _amountValidator.validate(state.amount); + emit(state.copyWith( + amount: state.amount.copyWith( + error: error, ), - ); - emit( - state.copyWith( - amount: state.amount.copyWith( - error: await _amountValidator.validate(state.amount), - isValidating: false, - ), - ), - ); + )); _updateFormState(); } Future emailChanged(String value) async { - emit( - state.copyWith( - email: state.email.copyWithExceptError( - value: value, - isValidating: true, - ), + emit(state.copyWith( + email: state.email.copyWithErrorReset(value: value), + )); + final canValidate = _emailValidator.canValidate(state.email); + if (!canValidate) { + return; + } + emit(state.copyWith( + email: state.email.copyWith( + validationStatus: AsyncFormzInputValidationStatus.validating, ), - ); - emit( - state.copyWith( - email: state.email.copyWith( - error: await _emailValidator.validate(state.email), - isValidating: false, - ), + )); + final error = await _emailValidator.validate(state.email); + emit(state.copyWith( + email: state.email.copyWith( + error: error, + validationStatus: AsyncFormzInputValidationStatus.validated, ), - ); + )); _updateFormState(); } diff --git a/example/lib/async/ui/feature/register/cubit/register_state.dart b/example/lib/async/ui/feature/register/cubit/register_state.dart index 728cf7f..6c919f2 100644 --- a/example/lib/async/ui/feature/register/cubit/register_state.dart +++ b/example/lib/async/ui/feature/register/cubit/register_state.dart @@ -8,7 +8,7 @@ part 'register_state.freezed.dart'; class RegisterState with _$RegisterState { factory RegisterState({ @Default(false) bool isFormValid, - @Default(Amount.pure(max: 100)) Amount amount, - @Default(Email.pure()) Email email, + @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 index 1b580d0..938b827 100644 --- a/example/lib/async/ui/feature/register/cubit/register_state.freezed.dart +++ b/example/lib/async/ui/feature/register/cubit/register_state.freezed.dart @@ -114,8 +114,8 @@ class __$$_RegisterStateCopyWithImpl<$Res> class _$_RegisterState implements _RegisterState { _$_RegisterState( {this.isFormValid = false, - this.amount = const Amount.pure(max: 100), - this.email = const Email.pure()}); + this.amount = const Amount('', max: 0.5), + this.email = const Email('')}); @override @JsonKey() diff --git a/example/lib/async/ui/feature/register/view/register_view.dart b/example/lib/async/ui/feature/register/view/register_view.dart index bd96f32..2064749 100644 --- a/example/lib/async/ui/feature/register/view/register_view.dart +++ b/example/lib/async/ui/feature/register/view/register_view.dart @@ -3,6 +3,7 @@ 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 { @@ -148,8 +149,9 @@ class _EmailTextFieldState extends State<_EmailTextField> { onChanged: cubit.emailChanged, decoration: InputDecoration( hintText: 'Enter your email', - helperText: email.isValidating ? 'Validating...' : null, - errorText: !email.isValidating + helperText: + email.validationStatus.isValidating ? 'Validating...' : null, + errorText: email.validationStatus.isNotValidating ? email.error?.map( invalidFormat: (_) => 'Invalid email format', alreadyExists: (_) => 'Email already exists', diff --git a/example/lib/async/ui/shared/forms/inputs/amount_input.dart b/example/lib/async/ui/shared/forms/inputs/amount_input.dart index 061c682..a74974e 100644 --- a/example/lib/async/ui/shared/forms/inputs/amount_input.dart +++ b/example/lib/async/ui/shared/forms/inputs/amount_input.dart @@ -1,56 +1,15 @@ -import 'package:decimal/decimal.dart'; import 'package:example/async/ui/shared/forms/validation_errors/amount_errors.dart'; import 'package:formz/formz.dart'; -const _kDefaultRequired = true; -const _kDefaultMin = 0.0; -const _kDefaultMax = double.maxFinite; - class Amount extends AsyncFormzInput { const Amount( - String value, { - bool isPure = true, - AmountValidationError? error, - bool isValidating = false, - this.isRequired = _kDefaultRequired, - this.min = _kDefaultMin, - this.max = _kDefaultMax, - }) : super( - value, - isPure: isPure, - error: error, - isValidating: isValidating, - ); - - const Amount.pure({ - bool isRequired = _kDefaultRequired, - double min = _kDefaultMin, - double max = _kDefaultMax, - }) : this( - '', - isPure: true, - isRequired: isRequired, - min: min, - max: max, - isValidating: false, - ); - - const Amount.dirty( - String value, { - bool isRequired = _kDefaultRequired, - double min = _kDefaultMin, - double max = _kDefaultMax, - AmountValidationError? error, - bool isValidating = false, - }) : this( - value, - isPure: false, - isRequired: isRequired, - min: min, - max: max, - error: error, - isValidating: isValidating, - ); + super.value, { + super.error, + super.validationStatus, + this.isRequired = true, + this.min = 0.0, + this.max = double.maxFinite, + }); final bool isRequired; final double min; @@ -59,87 +18,35 @@ class Amount extends AsyncFormzInput { Amount copyWith({ String? value, AmountValidationError? error, + AsyncFormzInputValidationStatus? validationStatus, bool? isValidating, bool? isRequired, double? min, double? max, }) { - final newValue = value ?? this.value; return Amount( - newValue, - isPure: isPure && newValue.isEmpty, + value ?? this.value, error: error ?? this.error, - isValidating: isValidating ?? this.isValidating, + validationStatus: validationStatus ?? this.validationStatus, isRequired: isRequired ?? this.isRequired, min: min ?? this.min, max: max ?? this.max, ); } - Amount copyWithExceptError({ + Amount copyWithErrorReset({ String? value, - bool? isValidating, + AsyncFormzInputValidationStatus? validationStatus, bool? isRequired, double? min, double? max, }) { - final newValue = value ?? this.value; return Amount( - newValue, - isPure: isPure && newValue.isEmpty, - isValidating: isValidating ?? this.isValidating, + value ?? this.value, + validationStatus: validationStatus ?? this.validationStatus, isRequired: isRequired ?? this.isRequired, min: min ?? this.min, max: max ?? this.max, ); } - - double toDouble() => double.parse(value); - - Decimal toDecimal() => Decimal.parse(value); - - double? toDoubleOrNull() => double.tryParse(value); - - Decimal? toDecimalOrNull() => Decimal.tryParse(value); - - Decimal toDecimalOr(Decimal or) => Decimal.tryParse(value) ?? or; - - Decimal toDecimalOrZero() => toDecimalOr(Decimal.zero); - - @override - String toString() { - return ''' -Amount( - isPure: $isPure, - value: '$value', - error: ${error.runtimeType}, - isValidating: $isValidating, - isRequired: $isRequired, - min: $min, - max: $max -)'''; - } - - @override - int get hashCode => - value.hashCode ^ - isPure.hashCode ^ - error.hashCode ^ - isValidating.hashCode ^ - isRequired.hashCode ^ - min.hashCode ^ - max.hashCode; - - @override - bool operator ==(Object other) { - if (other.runtimeType != runtimeType) return false; - return other is Amount && - other.value == value && - other.isPure == isPure && - other.error == error && - other.isValidating == isValidating && - other.isRequired == isRequired && - other.min == min && - other.max == 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 index 20adada..5842e7f 100644 --- a/example/lib/async/ui/shared/forms/inputs/email_input.dart +++ b/example/lib/async/ui/shared/forms/inputs/email_input.dart @@ -1,106 +1,40 @@ import 'package:example/async/ui/shared/forms/validation_errors/email_errors.dart'; import 'package:formz/formz.dart'; -const _kDefaultRequired = true; - class Email extends AsyncFormzInput { const Email( - String value, { - bool isPure = true, - EmailValidationError? error, - bool isValidating = false, - this.isRequired = _kDefaultRequired, - }) : super( - value, - isPure: isPure, - error: error, - isValidating: isValidating, - ); - - const Email.pure({ - bool isRequired = _kDefaultRequired, - }) : this( - '', - isPure: true, - isRequired: isRequired, - isValidating: false, - ); - - const Email.dirty( - String value, { - bool isRequired = _kDefaultRequired, - EmailValidationError? error, - bool isValidating = false, - }) : this( - value, - isPure: false, - isRequired: isRequired, - error: error, - isValidating: isValidating, - ); + super.value, { + super.error, + super.validationStatus, + this.isRequired = true, + }); final bool isRequired; Email copyWith({ String? value, EmailValidationError? error, + AsyncFormzInputValidationStatus? validationStatus, bool? isValidating, bool? isRequired, }) { - final newValue = value ?? this.value; return Email( - newValue, - isPure: isPure && newValue.isEmpty, + value ?? this.value, error: error ?? this.error, - isValidating: isValidating ?? this.isValidating, + validationStatus: validationStatus ?? this.validationStatus, isRequired: isRequired ?? this.isRequired, ); } - Email copyWithExceptError({ + Email copyWithErrorReset({ String? value, - bool? isValidating, + AsyncFormzInputValidationStatus? validationStatus, bool? isRequired, }) { - final newValue = value ?? this.value; return Email( - newValue, - isPure: isPure && newValue.isEmpty, - isValidating: isValidating ?? this.isValidating, + value ?? this.value, + validationStatus: validationStatus ?? this.validationStatus, isRequired: isRequired ?? this.isRequired, ); } - - @override - String toString() { - return ''' -Email( - isPure: $isPure, - value: '$value', - error: ${error.runtimeType}, - isValidating: $isValidating, - isRequired: $isRequired -)'''; - } - - @override - int get hashCode => - value.hashCode ^ - isPure.hashCode ^ - error.hashCode ^ - isValidating.hashCode ^ - isRequired.hashCode; - - @override - bool operator ==(Object other) { - if (other.runtimeType != runtimeType) { - return false; - } - return other is Email && - other.value == value && - other.isPure == isPure && - other.error == error && - other.isValidating == isValidating && - other.isRequired == isRequired; - } } diff --git a/example/lib/async/ui/shared/forms/validators/email_validator.dart b/example/lib/async/ui/shared/forms/validators/email_validator.dart index b68606b..77b4859 100644 --- a/example/lib/async/ui/shared/forms/validators/email_validator.dart +++ b/example/lib/async/ui/shared/forms/validators/email_validator.dart @@ -14,6 +14,18 @@ class EmailValidator 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) { diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 5ca1cbe..d179d4d 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -7,7 +7,6 @@ environment: sdk: ">=2.17.6 <3.0.0" dependencies: - decimal: ^2.2.0 flutter: sdk: flutter flutter_bloc: ^8.0.0 diff --git a/lib/formz.dart b/lib/formz.dart index c2afc29..1c4d3d1 100644 --- a/lib/formz.dart +++ b/lib/formz.dart @@ -62,16 +62,6 @@ abstract class FormzInputBase { /// the value could be 'Joe'. T get value; - /// If the [FormzInput] is pure (has been touched/modified). - /// Typically when the `FormzInput` is initially created, - /// it is created using the `FormzInput.pure` constructor to - /// signify that the user has not modified it. - /// - /// For subsequent changes (in response to user input), the - /// `FormzInput.dirty` constructor should be used to signify that - /// the `FormzInput` has been manipulated. - bool get isPure; - /// Whether the [FormzInput] value is valid according to the /// overridden `validator`. /// @@ -121,7 +111,14 @@ abstract class FormzInput implements FormzInputBase { @override final T value; - @override + /// If the [FormzInput] is pure (has been touched/modified). + /// Typically when the `FormzInput` is initially created, + /// it is created using the `FormzInput.pure` constructor to + /// signify that the user has not modified it. + /// + /// For subsequent changes (in response to user input), the + /// `FormzInput.dirty` constructor should be used to signify that + /// the `FormzInput` has been manipulated. final bool isPure; @override @@ -256,8 +253,24 @@ abstract class AsyncFormzInputValidator< /// return null; ///} /// ``` - @protected Future validate(TInput input); + + bool canValidate(TInput input) => true; +} + +enum AsyncFormzInputValidationStatus { + pure, + validating, + validated, +} + +extension AsyncFormzInputValidationStatusX on AsyncFormzInputValidationStatus { + bool get isPure => this == AsyncFormzInputValidationStatus.pure; + bool get isNotPure => !isPure; + bool get isValidating => this == AsyncFormzInputValidationStatus.validating; + bool get isNotValidating => !isValidating; + bool get isValidated => this == AsyncFormzInputValidationStatus.validated; + bool get isNotValidated => !isValidated; } /// {@template form_input} @@ -285,34 +298,16 @@ abstract class AsyncFormzInput implements FormzInputBase { /// Constructor which create a [FormzInput] with a given value. const AsyncFormzInput( this.value, { - this.isPure = true, this.error, - this.isValidating = false, - }); - - /// Constructor which create a `pure` [AsyncFormzInput] with a given value. - const AsyncFormzInput.pure(T value) : this(value); - - /// Constructor which create a `dirty` [AsyncFormzInput] with a given value. - const AsyncFormzInput.dirty( - T value, { - E? error, - bool isValidating = false, - }) : this( - value, - isPure: false, - error: error, - isValidating: isValidating, - ); + AsyncFormzInputValidationStatus? validationStatus, + }) : validationStatus = + validationStatus ?? AsyncFormzInputValidationStatus.pure; @override final T value; @override - final bool isPure; - - @override - bool get isValid => error == null; + bool get isValid => validationStatus.isValidated && error == null; @override bool get isNotValid => !isValid; @@ -320,12 +315,11 @@ abstract class AsyncFormzInput implements FormzInputBase { @override final E? error; - /// Whether the [FormzInput] value is busy isValidating. - final bool isValidating; + final AsyncFormzInputValidationStatus validationStatus; @override int get hashCode => - value.hashCode ^ isPure.hashCode ^ error.hashCode ^ isValidating.hashCode; + value.hashCode ^ error.hashCode ^ validationStatus.hashCode; @override bool operator ==(Object other) { @@ -334,17 +328,15 @@ abstract class AsyncFormzInput implements FormzInputBase { } return other is AsyncFormzInput && other.value == value && - other.isPure == isPure && other.error == error && - other.isValidating == isValidating; + other.validationStatus == validationStatus; } @override String toString() => ''' $runtimeType( value: $value, - isPure: $isPure, error: $error, - isValidating: $isValidating + validationStatus: ${validationStatus.name} )'''; } From 0762086f00394d28186ca7f6ef239e2074941056 Mon Sep 17 00:00:00 2001 From: Jop Middelkamp Date: Thu, 25 Aug 2022 22:11:40 +0200 Subject: [PATCH 04/11] Improved the async example --- .../register/cubit/register_cubit.dart | 6 ++- .../feature/register/view/register_view.dart | 42 ++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/example/lib/async/ui/feature/register/cubit/register_cubit.dart b/example/lib/async/ui/feature/register/cubit/register_cubit.dart index 3e5bed1..6e01bec 100644 --- a/example/lib/async/ui/feature/register/cubit/register_cubit.dart +++ b/example/lib/async/ui/feature/register/cubit/register_cubit.dart @@ -17,12 +17,16 @@ class RegisterCubit extends Cubit { Future amountChanged(String value) async { emit(state.copyWith( - amount: state.amount.copyWithErrorReset(value: value), + amount: state.amount.copyWithErrorReset( + 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(); diff --git a/example/lib/async/ui/feature/register/view/register_view.dart b/example/lib/async/ui/feature/register/view/register_view.dart index 2064749..49c9e34 100644 --- a/example/lib/async/ui/feature/register/view/register_view.dart +++ b/example/lib/async/ui/feature/register/view/register_view.dart @@ -92,7 +92,10 @@ class _AmountTextFieldState extends State<_AmountTextField> { textController.text = amount.value; } return FormLabel( - label: const Text('Amount'), + label: _Label( + text: 'Amount', + input: amount, + ), child: TextFormField( controller: textController, focusNode: textFocusNode, @@ -142,7 +145,10 @@ class _EmailTextFieldState extends State<_EmailTextField> { textController.text = email.value; } return FormLabel( - label: const Text('Email'), + label: _Label( + text: 'E-mail', + input: email, + ), child: TextFormField( controller: textController, focusNode: textFocusNode, @@ -164,3 +170,35 @@ class _EmailTextFieldState extends State<_EmailTextField> { ); } } + +class _Label extends StatelessWidget { + const _Label({ + required this.text, + required this.input, + }); + + final String text; + final AsyncFormzInput 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'; + } +} From 1472b84ccf83958a6cc609bee298c802be84be2b Mon Sep 17 00:00:00 2001 From: Jop Middelkamp Date: Thu, 25 Aug 2022 22:29:29 +0200 Subject: [PATCH 05/11] Renamed copyWithErrorReset to copyWithResetError --- .../lib/async/ui/feature/register/cubit/register_cubit.dart | 4 ++-- example/lib/async/ui/shared/forms/inputs/amount_input.dart | 2 +- example/lib/async/ui/shared/forms/inputs/email_input.dart | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/example/lib/async/ui/feature/register/cubit/register_cubit.dart b/example/lib/async/ui/feature/register/cubit/register_cubit.dart index 6e01bec..68d2e6c 100644 --- a/example/lib/async/ui/feature/register/cubit/register_cubit.dart +++ b/example/lib/async/ui/feature/register/cubit/register_cubit.dart @@ -17,7 +17,7 @@ class RegisterCubit extends Cubit { Future amountChanged(String value) async { emit(state.copyWith( - amount: state.amount.copyWithErrorReset( + amount: state.amount.copyWithResetError( value: value, validationStatus: AsyncFormzInputValidationStatus.validating, ), @@ -34,7 +34,7 @@ class RegisterCubit extends Cubit { Future emailChanged(String value) async { emit(state.copyWith( - email: state.email.copyWithErrorReset(value: value), + email: state.email.copyWithResetError(value: value), )); final canValidate = _emailValidator.canValidate(state.email); if (!canValidate) { diff --git a/example/lib/async/ui/shared/forms/inputs/amount_input.dart b/example/lib/async/ui/shared/forms/inputs/amount_input.dart index a74974e..a39c25c 100644 --- a/example/lib/async/ui/shared/forms/inputs/amount_input.dart +++ b/example/lib/async/ui/shared/forms/inputs/amount_input.dart @@ -34,7 +34,7 @@ class Amount extends AsyncFormzInput { ); } - Amount copyWithErrorReset({ + Amount copyWithResetError({ String? value, AsyncFormzInputValidationStatus? validationStatus, bool? isRequired, diff --git a/example/lib/async/ui/shared/forms/inputs/email_input.dart b/example/lib/async/ui/shared/forms/inputs/email_input.dart index 5842e7f..4b4044d 100644 --- a/example/lib/async/ui/shared/forms/inputs/email_input.dart +++ b/example/lib/async/ui/shared/forms/inputs/email_input.dart @@ -26,7 +26,7 @@ class Email extends AsyncFormzInput { ); } - Email copyWithErrorReset({ + Email copyWithResetError({ String? value, AsyncFormzInputValidationStatus? validationStatus, bool? isRequired, From 3557684acce0960ef986ed800bd98169d59fffac Mon Sep 17 00:00:00 2001 From: Jop Middelkamp Date: Thu, 25 Aug 2022 22:49:50 +0200 Subject: [PATCH 06/11] Added comments to the public apis --- lib/formz.dart | 58 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/lib/formz.dart b/lib/formz.dart index 1c4d3d1..a6dbdfd 100644 --- a/lib/formz.dart +++ b/lib/formz.dart @@ -245,6 +245,14 @@ abstract class AsyncFormzInputValidator< /// 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(); @@ -255,22 +263,49 @@ abstract class AsyncFormzInputValidator< /// ``` 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; - bool get isNotPure => !isPure; + + /// 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; - bool get isNotValidated => !isValidated; } /// {@template form_input} @@ -281,16 +316,16 @@ extension AsyncFormzInputValidationStatusX on AsyncFormzInputValidationStatus { /// instances. /// /// ```dart -/// enum FirstNameError { empty } -/// class FirstName extends FormzInput { -/// const FirstName.pure({String value = ''}) : super.pure(value); -/// const FirstName.dirty({String value = ''}) : super.dirty(value); +///class Email extends AsyncFormzInput { +/// const Email( +/// super.value, { +/// super.error, +/// super.validationStatus, +/// this.isRequired = true, +/// }); /// -/// @override -/// FirstNameError? validator(String value) { -/// return value.isEmpty ? FirstNameError.empty : null; -/// } -/// } +/// final bool isRequired; +///} /// ``` /// {@endtemplate} @immutable @@ -315,6 +350,7 @@ abstract class AsyncFormzInput implements FormzInputBase { @override final E? error; + /// The validation status of the [AsyncFormzInput]. final AsyncFormzInputValidationStatus validationStatus; @override From 6300e7361a7823ca9f747e44b1dbb9fa55dc8f5c Mon Sep 17 00:00:00 2001 From: Jop Middelkamp Date: Thu, 25 Aug 2022 23:42:08 +0200 Subject: [PATCH 07/11] Added tests for the async additions --- lib/formz.dart | 10 +- pubspec.yaml | 2 +- test/formz_test.dart | 368 ++++++++++++++++++++++++++--- test/helpers/helpers.dart | 1 + test/helpers/name_async_input.dart | 19 ++ 5 files changed, 358 insertions(+), 42 deletions(-) create mode 100644 test/helpers/name_async_input.dart diff --git a/lib/formz.dart b/lib/formz.dart index a6dbdfd..acc27e0 100644 --- a/lib/formz.dart +++ b/lib/formz.dart @@ -198,7 +198,7 @@ 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} @@ -354,9 +354,7 @@ abstract class AsyncFormzInput implements FormzInputBase { final AsyncFormzInputValidationStatus validationStatus; @override - int get hashCode => - value.hashCode ^ error.hashCode ^ validationStatus.hashCode; - + int get hashCode => Object.hashAll([value, error, validationStatus]); @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) { @@ -371,8 +369,8 @@ abstract class AsyncFormzInput implements FormzInputBase { @override String toString() => ''' $runtimeType( - value: $value, + value: '$value', error: $error, - validationStatus: ${validationStatus.name} + 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..a4fe2f7 100644 --- a/test/formz_test.dart +++ b/test/formz_test.dart @@ -158,61 +158,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 +504,33 @@ 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); + }); + }); + }); }); } 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..6beef68 --- /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(AsyncFormzInput input) async { + throw UnimplementedError(); + } +} From d8db87b522e1c57ec85ffbe059f7804118db92c0 Mon Sep 17 00:00:00 2001 From: Jop Middelkamp Date: Thu, 25 Aug 2022 23:45:00 +0200 Subject: [PATCH 08/11] Analyzer fixes --- example/analysis_options.yaml | 1 - .../register/cubit/register_cubit.dart | 54 +++++++++++-------- .../feature/register/view/register_view.dart | 22 +++++--- test/formz_test.dart | 4 +- 4 files changed, 49 insertions(+), 32 deletions(-) diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 53e8522..ff9c544 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -2,4 +2,3 @@ include: package:very_good_analysis/analysis_options.3.1.0.yaml linter: rules: public_member_api_docs: false - require_trailing_commas: false diff --git a/example/lib/async/ui/feature/register/cubit/register_cubit.dart b/example/lib/async/ui/feature/register/cubit/register_cubit.dart index 68d2e6c..d0fa5cd 100644 --- a/example/lib/async/ui/feature/register/cubit/register_cubit.dart +++ b/example/lib/async/ui/feature/register/cubit/register_cubit.dart @@ -16,42 +16,52 @@ class RegisterCubit extends Cubit { final EmailValidator _emailValidator; Future amountChanged(String value) async { - emit(state.copyWith( - amount: state.amount.copyWithResetError( - value: value, - validationStatus: AsyncFormzInputValidationStatus.validating, + 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, + 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), - )); + 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, + 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, + emit( + state.copyWith( + email: state.email.copyWith( + error: error, + validationStatus: AsyncFormzInputValidationStatus.validated, + ), ), - )); + ); _updateFormState(); } diff --git a/example/lib/async/ui/feature/register/view/register_view.dart b/example/lib/async/ui/feature/register/view/register_view.dart index 49c9e34..54d62b6 100644 --- a/example/lib/async/ui/feature/register/view/register_view.dart +++ b/example/lib/async/ui/feature/register/view/register_view.dart @@ -183,15 +183,23 @@ class _Label extends StatelessWidget { @override Widget build(BuildContext context) { final valStatus = _getValidationStatusText(input.validationStatus); - return Text.rich(TextSpan(children: [ - const TextSpan( - text: 'Email', - ), + return Text.rich( TextSpan( - text: ' (is_valid: ${input.isValid}, validation_status: $valStatus)', - style: const TextStyle(color: Colors.grey, fontWeight: FontWeight.w200), + 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) { diff --git a/test/formz_test.dart b/test/formz_test.dart index a4fe2f7..c8a2d39 100644 --- a/test/formz_test.dart +++ b/test/formz_test.dart @@ -213,7 +213,7 @@ void main() { }); test( - 'isValid is false when status not is validated and error is not null', + '''isValid is false when status not is validated and error is not null''', () { final input = NameAsyncInput( 'joe', @@ -254,7 +254,7 @@ void main() { }); test( - 'isNotValid is true when status not is validated and error is not null', + '''isNotValid is true when status not is validated and error is not null''', () { final input = NameAsyncInput( 'joe', From cfb94c7d9310dda2f5a6ef7b5ef9fb1bb7d075ef Mon Sep 17 00:00:00 2001 From: Jop Middelkamp Date: Mon, 29 Aug 2022 08:57:53 +0200 Subject: [PATCH 09/11] Added some async description to the readme --- README.md | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) 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 From 2a84427f5e27219677e1a27b0e1500833bb89cab Mon Sep 17 00:00:00 2001 From: Jop Middelkamp Date: Thu, 10 Nov 2022 20:15:05 +0100 Subject: [PATCH 10/11] Fixed analyzer issues --- .../feature/register/view/register_view.dart | 52 +++++++++++++++---- example/lib/sync/main.dart | 8 ++- lib/formz.dart | 4 +- test/helpers/name_async_input.dart | 2 +- 4 files changed, 52 insertions(+), 14 deletions(-) diff --git a/example/lib/async/ui/feature/register/view/register_view.dart b/example/lib/async/ui/feature/register/view/register_view.dart index 54d62b6..d6dad3c 100644 --- a/example/lib/async/ui/feature/register/view/register_view.dart +++ b/example/lib/async/ui/feature/register/view/register_view.dart @@ -92,8 +92,7 @@ class _AmountTextFieldState extends State<_AmountTextField> { textController.text = amount.value; } return FormLabel( - label: _Label( - text: 'Amount', + label: _AmountLabel( input: amount, ), child: TextFormField( @@ -145,8 +144,7 @@ class _EmailTextFieldState extends State<_EmailTextField> { textController.text = email.value; } return FormLabel( - label: _Label( - text: 'E-mail', + label: _EmailLabel( input: email, ), child: TextFormField( @@ -171,14 +169,12 @@ class _EmailTextFieldState extends State<_EmailTextField> { } } -class _Label extends StatelessWidget { - const _Label({ - required this.text, +class _EmailLabel extends StatelessWidget { + const _EmailLabel({ required this.input, }); - final String text; - final AsyncFormzInput input; + final Email input; @override Widget build(BuildContext context) { @@ -210,3 +206,41 @@ class _Label extends StatelessWidget { : '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/sync/main.dart b/example/lib/sync/main.dart index c982d39..31fee5c 100644 --- a/example/lib/sync/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/lib/formz.dart b/lib/formz.dart index acc27e0..513ca22 100644 --- a/lib/formz.dart +++ b/lib/formz.dart @@ -162,7 +162,7 @@ abstract class FormzInput implements FormzInputBase { 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); } } @@ -198,7 +198,7 @@ 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} diff --git a/test/helpers/name_async_input.dart b/test/helpers/name_async_input.dart index 6beef68..28a6462 100644 --- a/test/helpers/name_async_input.dart +++ b/test/helpers/name_async_input.dart @@ -13,7 +13,7 @@ class NameAsyncInput extends AsyncFormzInput { class NameAsyncFormzInputValidator extends AsyncFormzInputValidator< NameAsyncInput, String, NameAsyncInputError> { @override - Future validate(AsyncFormzInput input) async { + Future validate(NameAsyncInput input) async { throw UnimplementedError(); } } From 2f777361a81e3886b8b5984a6b990ecf58b39b93 Mon Sep 17 00:00:00 2001 From: Jop Middelkamp Date: Wed, 16 Nov 2022 14:46:09 +0100 Subject: [PATCH 11/11] Added one more test to get code coverage to 100% --- test/formz_test.dart | 10 ++++++++++ test/helpers/formz_input_test_object.dart | 16 ++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 test/helpers/formz_input_test_object.dart diff --git a/test/formz_test.dart b/test/formz_test.dart index c8a2d39..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() { @@ -532,5 +533,14 @@ NameAsyncInput( }); }); }); + + 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(); +}