DEV Community

loading...

Form validations with Bloc + Verify

danielcardonarojas profile image Daniel Cardona Rojas ・3 min read

Not too long ago I published a Dart package to pub.dev called verify,
Its a package to do validations. In this post I want to overview some of its
capabilities and explain some approaches on how this could be used in a real
world application.

Verify overview

Verify defines a Validator as any function with the following signature:
This type of ideas arise naturally in function programming languages.

typedef Validator<S, T> = Either<List<ValidationError>, T> Function(S subject);
Enter fullscreen mode Exit fullscreen mode

As can be observed the Validator is generic over the input and output type of the function
and has a contract on what errors through an abstract class ValidationError.

For those who are familiar to these abstractions can already see how definitions of map, flatMap etc... can easily be derived.

Everything else boils down to creating different ways to combine these functions into more complex validators and having good ergonomics for developers to easily create custom validators.

Check the official API for more details.

Let build something

I won't get to theoretical with Verify instead jump straight into an example.
I will demonstrate how a sign up form page could be built and validated with
bloc and verify packages.

Are form will be real simple with an email, password and password confirmation input fields.
If you want to see the full source for this checkout out the examples
folder of the package repository.

Bloc setup

As you probably already know when building a reactive UI with Bloc, first thing is modeling state, and events.

For this we will use the freezed package to have value equality and some other goodies.

Events

Lets start out with modeling out events. We will need to be able to modify our states email, password and password confirmation fields. We usually would have submit event too. But lets keep it simple since we won't be connecting to service in this example.

@freezed
abstract class SignUpEvent with _$SignUpEvent {
  const factory SignUpEvent.setEmail(String email) = SignUpSetEmail;
  const factory SignUpEvent.setPassword(String password) = SignUpSetPassword;
  const factory SignUpEvent.setPasswordConfirmation(String password) =
      SignUpSetPasswordConfirmation;
}
Enter fullscreen mode Exit fullscreen mode

State

Lets start modeling our state. We now we have to track the email, password and password
confirmation field so we put all those in our state. We will also need to display validation errors this seems logical to track with a Map that contains errors grouped by
input field type, so that we can easily get the errors for an specific field.

enum SignUpFormField {
  email,
  password,
  passwordConfirmation,
}

@freezed
abstract class SignUpState with _$SignUpState {
  const factory SignUpState(
      {String email,
      String password,
      String confirmation,
      Map<SignUpFormField, List<String>> errors}) = _SignUpState;

  factory SignUpState.initial() {
    return SignUpState(errors: {
      SignUpFormField.email: [],
      SignUpFormField.password: [],
      SignUpFormField.passwordConfirmation: [],
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

the base bloc implementation should be pretty straight forward.

Stream<SignUpState> mapEventToState(SignUpEvent event) async* {
    final newState = event.when(
        setEmail: (str) => state.copyWith(email: str),
        setPassword: (str) => state.copyWith(password: str),
        setPasswordConfirmation: (str) => state.copyWith(confirmation: str),
    );
    yield newstate;

}
Enter fullscreen mode Exit fullscreen mode

Integrate validations with Verify

To start out with verify, you first must define some error type and implement
ValidationError. To do this we will equip our error type with the field it references.

class SignUpError implements ValidationError {
  final SignUpFormField field;
  final String message;

  SignUpError({@required this.field, @required this.message});

  @override
  String get errorDescription => message; // ValidationError requirement
}
Enter fullscreen mode Exit fullscreen mode

Now that we have our error type in place. Let's define a validator.
I will show one way of doing this, but the same result could be achieved using the
provided verify combinators in different ways. Checkout out the docs to see
all available options.

Notice how we can bypass validators for some conditions. For instance we want to
have matching password and confirmation, but also want to ignore these checks if the confirmation field is null or perhaps empty.

final Validator_<SignUpState> signUpValidation = Verify.all<SignUpState>([
  Verify.subject<SignUpState>().checkField(
      (state) => state.password,
      Verify.property((s) => s.length > 4,
          error: SignUpError(
            field: SignUpFormField.password,
            message: 'incorrect length',
          ))),
  Verify.subject<SignUpState>().checkField(
      (state) => state.email,
      Verify.property((s) => s.contains('@'),
          error: SignUpError(
            field: SignUpFormField.email,
            message: 'invalid format',
          ))),
  Verify.property((SignUpState state) => state.confirmation == state.password,
      error: SignUpError(
        field: SignUpFormField.passwordConfirmation,
        message: 'not match',
      )).ignoreWhen((state) => state.confirmation == null),
]);
Enter fullscreen mode Exit fullscreen mode

Bloc integration

Finally lets update our bloc to validate the state after input updates:

Stream<SignUpState> mapEventToState(SignUpEvent event) async* {
    final newState = event.when(
        setEmail: (str) => state.copyWith(email: str),
        setPassword: (str) => state.copyWith(password: str),
        setPasswordConfirmation: (str) => state.copyWith(confirmation: str),
    );

    final errors = signUpValidation
        .verify<SignUpError>(newState)
        .groupedErrorsBy((error) => error.field)
        .messages;

    yield newState.copyWith(errors: errors);

}
Enter fullscreen mode Exit fullscreen mode

The UI should be straightforward so I won't discuss that. If you want to see the complete example
head over to the examples folder of the repository.

GitHub logo DanielCardonaRojas / verify

A Dart validation DSL to validate your flutter app models.

Validations made simple

Build Status Pub Package Codecov MIT License

A fp inspired validation DSL. For Dart and Flutter projects.

Requirements

The implementation of Verify relies heavily on dart extension methods, which are available for Dart versions >= 2.6

Features

  • Completely extensible (create your own combinators, validator primitives, etc)
  • Flexible Verify is an extension based API (There is not single class created its all pure functions)
  • Customizable (Define you own error types) organize validators how ever you want
  • Bloc friendly (See examples for a concrete implementation)

Usage

Creating validators

A Validator is just a simple function alias:

// S is the input type and T the output type
typedef Validator<S, T> = Either<List<ValidationError>, T> Function(S subject);
Enter fullscreen mode Exit fullscreen mode

So you can create your own validator by just specifying a function for example:

final Validator_<String> emailValidator = (String email) {
  return email.contains
Enter fullscreen mode Exit fullscreen mode

Discussion

pic
Editor guide