Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: async validation support #61

Closed
wants to merge 11 commits into from
27 changes: 27 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
112 changes: 112 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,118 @@ print(joe.error); // null
print(name.displayError); // null
```

## Create a AsyncFormzInput
```dart

class Email extends AsyncFormzInput<String, EmailValidationError> {
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<Email, String, EmailValidationError> {
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<EmailValidationError?> 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<void> 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
Expand Down
8 changes: 8 additions & 0 deletions example/lib/async/data/repositories/email_repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class EmailRepository {
const EmailRepository();

Future<bool> findByAddress(String emailAddress) async {
await Future<void>.delayed(const Duration(seconds: 1));
return true;
}
}
6 changes: 6 additions & 0 deletions example/lib/async/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'package:example/async/ui/app.dart';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the example app from a more "real world usage" point of view, but it defeats the purpose of the example of allowing people to understand what a package API looks like when used.

import 'package:flutter/material.dart';

void main() {
runApp(const App());
}
19 changes: 19 additions & 0 deletions example/lib/async/ui/app.dart
Original file line number Diff line number Diff line change
@@ -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(),
),
);
}
}
78 changes: 78 additions & 0 deletions example/lib/async/ui/feature/register/cubit/register_cubit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import 'package:example/async/ui/feature/register/cubit/register_state.dart';
import 'package:example/async/ui/shared/forms/validators/amount_validator.dart';
import 'package:example/async/ui/shared/forms/validators/email_validator.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:formz/formz.dart';

class RegisterCubit extends Cubit<RegisterState> {
RegisterCubit({
required AmountValidator amountValidator,
required EmailValidator emailValidator,
}) : _amountValidator = amountValidator,
_emailValidator = emailValidator,
super(RegisterState());

final AmountValidator _amountValidator;
final EmailValidator _emailValidator;

Future<void> amountChanged(String value) async {
emit(
state.copyWith(
amount: state.amount.copyWithResetError(
value: value,
validationStatus: AsyncFormzInputValidationStatus.validating,
),
),
);
final error = await _amountValidator.validate(state.amount);
emit(
state.copyWith(
amount: state.amount.copyWith(
error: error,
validationStatus: AsyncFormzInputValidationStatus.validated,
),
),
);
_updateFormState();
}

Future<void> emailChanged(String value) async {
emit(
state.copyWith(
email: state.email.copyWithResetError(value: value),
),
);
final canValidate = _emailValidator.canValidate(state.email);
if (!canValidate) {
return;
}
emit(
state.copyWith(
email: state.email.copyWith(
validationStatus: AsyncFormzInputValidationStatus.validating,
),
),
);
final error = await _emailValidator.validate(state.email);
emit(
state.copyWith(
email: state.email.copyWith(
error: error,
validationStatus: AsyncFormzInputValidationStatus.validated,
),
),
);
_updateFormState();
}

void _updateFormState() {
emit(
state.copyWith(
isFormValid: Formz.validate([
state.amount,
state.email,
]),
),
);
}
}
14 changes: 14 additions & 0 deletions example/lib/async/ui/feature/register/cubit/register_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:example/async/ui/shared/forms/inputs/amount_input.dart';
import 'package:example/async/ui/shared/forms/inputs/email_input.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'register_state.freezed.dart';

@freezed
class RegisterState with _$RegisterState {
factory RegisterState({
@Default(false) bool isFormValid,
@Default(Amount('', max: 0.5)) Amount amount,
@Default(Email('')) Email email,
}) = _RegisterState;
}
Loading