Functional input validation library based on Fpdart, which is inspired by Formz.
For the most part, FPFormz is similar to the original Formz library, with a few notable differences:
-
FPFormz allows specifying different types for input and validated values, which can be convenient when using non-string type values (e.g.
int
,enum
, value class, etc.). -
It exposes validated values and errors as functional constructs such as
Either
orOption
, making it easier to manipulate them declaratively. -
It also provides a way to write validation logic as mixins, which you can combine to handle more complex use cases.
You can install PFFormz by adding the following entry in your pubsec.yaml
:
# pubspec.yaml
dependencies:
fpformz: ^0.1.3
To define a validatable input, you need to write a class that extends FormInput<V, I, E>
whose
generic parameters correspond to the type of resulting value, input value, and potential errors,
respectively:
class AgeInput extends FormInput<int, String, ValidationError> {
const AgeInput.pristine(name, value) : super.pristine(name, value);
const AgeInput.dirty(name, value) : super.dirty(name, value);
Either<ValidationError, int> validate(String value) =>
Either.tryCatch(() => int.parse(value),
(e, s) => ValidationError(name, '$name should be a number.'));
}
After declaring your input, you can use either pristine
or dirty
constructor to create an
instance:
void example() {
// To create an unmodified ('pristine') input instance:
final age = AgeInput.pristine('age', '');
// Or you can create a modified ('dirty') input instance as below:
final editedAge = AgeInput.dirty('age', '23');
print(age.isPristine); // returns 'true'
print(editedAge.isPristine); // returns 'false'
}
You can access validation information either as a functional construct, or as a nullable:
void example() {
print(editedAge.isValid); // returns true
print(editedAge.result); // returns Right(23)
print(editedAge.resultOrNull); // returns 23
print(editedAge.error); // returns None
print(editedAge.errorOrNull); // returns null
print(age.isValid); // returns false
print(age.result); // returns Left(ValidationError)
print(age.resultOrNull); // returns null
print(age.error); // returns Some(ValidationError)
print(age.errorOrNull); // returns ValidationError
}
And because most input components treat the user input as a String
instance, you can simplify the
type signature by extending from StringFormInput
:
class NameInput extends StringFormInput<String, ValidationError> {
const NameInput.pristine(name, value) : super.pristine(name, value);
const NameInput.dirty(name, value) : super.dirty(name, value);
@override
String convert(String value) => value;
@override
Either<ValidationError, String> validate(String value) =>
value.isEmpty
? Either.left(ValidationError(name, 'The name cannot be empty.'))
: super.validate(value);
}
Like with Formz, you can create a form class to host multiple input fields and validate them together:
class RegistrationForm extends Form {
final NameInput name;
final EmailInput email;
const RegistrationForm({
this.name = const NameInput.pristine('name', ''),
this.email = const EmailInput.pristine('email', '')
});
@override
get inputs => [name, email];
}
Then you can validate it using a similar API like that of FormInput
:
void example() {
final form = RegistrationForm();
print(form.isPristine); // returns true
print(form.isValid); // returns false
print(form.result); // it 'short circuits' at the first error encountered
print(form.errors); // but you can get all errors this way.
}
Form.result
returns a map of all validated input values which you can use to invoke a service
method:
void example() {
final form = RegistrationForm();
final params = form.resultOrNull!;
service.register(params[form.email.name], params[form.password.name]);
// Or even like this, provided that the input names match those of the parameters:
Function.apply(service.register, [], params);
}
You can also write reusable validation logic as a mixin:
@immutable
mixin NonEmptyString<V> on FormInput<V, String, ValidationError> {
ValidationError get whenEmpty => ValidationError(name, 'Please enter $name.');
@override
Either<ValidationError, V> validate(String value) =>
value.isEmpty ? Either.left(whenEmpty) : super.validate(value);
}
And build a concrete input field by adding them to either BaseFormInput
or StringFormInput
as
shown below:
class EmailInput extends StringFormInput<Email, ValidationError>
with EmailString, NonEmptyString {
const EmailInput.pristine(name, value) : super.pristine(name, value);
const EmailInput.dirty(name, value) : super.dirty(name, value);
@override
Email convert(String value) => Email.parse(value);
}
It's recommended to split each validation logic into a separate mixin rather than putting all into an input class to maximise code reuse and achieve separation of concerns (i.e. the 'S' in SOLID principles).
FPFormz also ships with a small collection of ready-to-use mixins like NonEmptyString
, StringShorterThan
, which might be expanded in future versions.
You can find more code examples in our test cases.