Skip to content

Instantly share code, notes, and snippets.

@herveGuigoz
Created July 16, 2024 21:35
Show Gist options
  • Save herveGuigoz/5c4c48dbabecbe997ff74824dbbd13ed to your computer and use it in GitHub Desktop.
Save herveGuigoz/5c4c48dbabecbe997ff74824dbbd13ed to your computer and use it in GitHub Desktop.
Form
import 'package:freezed_annotation/freezed_annotation.dart';
part 'form.freezed.dart';
@freezed
class FormStatus with _$FormStatus {
// Class representing the status of a form at any given point in time.
const FormStatus._();
/// The form has not yet been submitted.
const factory FormStatus.initial() = _Initial;
/// The form is in the process of being submitted.
const factory FormStatus.submissionInProgress() = _InProgress;
/// The form has been submitted successfully.
const factory FormStatus.submissionSucceed() = _Succeed;
/// The form submission failed.
const factory FormStatus.submissionFailled(Object error) = _Failled;
bool get isInProgress => this is _InProgress;
}
/// Mixin that handles validation.
mixin FormMixin {
List<FormInput<Object, Object>> get inputs;
bool get isValid {
return inputs.every((input) => input.isValid);
}
bool get isPure {
return inputs.every((input) => input.isPure);
}
}
enum FormInputStatus {
/// The form input has not been touched.
pure,
/// The form input is valid.
valid,
/// The form input is not valid.
invalid,
}
@immutable
abstract class FormInput<T, E> {
const FormInput({required this.value}) : isPure = false;
const FormInput.initial({required this.value}) : isPure = true;
/// The value of the given [FormInput].
/// For example, if you have a `FormInput` for `FirstName`,
/// the value could be 'Joe'.
final T value;
/// If the input has been modified.
final bool isPure;
/// A function that must return a validation error if the provided
/// [value] is invalid and `null` otherwise.
E? validator(T value);
E? get error => isPure ? null : validator(value);
@override
bool operator ==(covariant FormInput<T, E> other) {
if (other.runtimeType != runtimeType) return false;
return other.value == value && other.isPure == isPure;
}
@override
int get hashCode => Object.hashAll([value, isPure]);
@override
String toString() => '$runtimeType(value: $value, isValid: $isValid)';
}
extension FormInputExtension<T, E> on FormInput<T, E> {
/// Whether the [FormInput] value is valid according to the overridden `validator`.
bool get isValid => validator(value) == null;
/// Whether the [FormInput] value is not valid.
bool get invalid => !isValid;
}
import 'package:form/form.dart';
import 'package:test/test.dart';
class LoginForm with FormMixin {
LoginForm({
this.status = const FormStatus.initial(),
this.email = const EmailInput.initial(),
});
final FormStatus status;
final EmailInput email;
@override
List<FormInput<Object, Object>> get inputs => [email];
void main() {
group('Form', () {
group('Mixin', () {
test('is not valid when default constructor is used', () {
final form = LoginForm();
expect(form.isValid, isFalse);
});
test('is not valid when containing a invalid value', () {
final form = LoginForm(email: const EmailInput());
expect(form.isValid, isFalse);
});
test('is valid when containing a valid value', () {
final form = LoginForm(email: const EmailInput(value: '[email protected]'));
expect(form.isValid, isTrue);
});
test('is pure when none of the inputs were touched', () {
final form = LoginForm();
expect(form.isPure, isTrue);
});
test('is not pure when one or multiple inputs were touched', () {
final form = LoginForm(email: const EmailInput());
expect(form.isPure, isFalse);
});
test('isInProgress is true when status is _InProgress', () {
final form = LoginForm(status: const FormStatus.submissionInProgress());
expect(form.status.isInProgress, isTrue);
});
test('isInProgress is false when status is not _InProgress', () {
final form = LoginForm();
expect(form.status.isInProgress, isFalse);
});
});
group('EmailInput', () {
test('value is correct', () {
const initial = EmailInput.initial(value: '[email protected]');
const dirty = EmailInput(value: '[email protected]');
expect(initial.value, '[email protected]');
expect(dirty.value, '[email protected]');
});
test('isPure is true when super.initial is used', () {
const input = EmailInput.initial(value: '[email protected]');
expect(input.isPure, isTrue);
});
test('isPure is false when default constructor is used', () {
const input = EmailInput(value: '[email protected]');
expect(input.isPure, isFalse);
});
test('isValid is true if super.initial is used and value is valid', () {
const input = EmailInput.initial(value: '[email protected]');
expect(input.isValid, isTrue);
expect(input.invalid, isFalse);
expect(input.error, isNull);
});
test('isValid is false if super.initial is used and input is invalid', () {
const input = EmailInput.initial(value: 'jane.doe');
expect(input.isValid, isFalse);
expect(input.invalid, isTrue);
});
test('isValid is true if default constructor is used and input is valid', () {
const input = EmailInput(value: '[email protected]');
expect(input.isValid, isTrue);
expect(input.invalid, isFalse);
expect(input.error, isNull);
});
test('isValid is false if default constructor is used and input is invalid', () {
const input = EmailInput(value: 'jane.doe');
expect(input.isValid, isFalse);
expect(input.invalid, isTrue);
});
test('error is EmailError.empty if value is empty', () {
const input = EmailInput();
expect(input.error, EmailError.empty);
});
test('error is EmailError.invalid if value is invalid', () {
const input = EmailInput(value: 'jane.doe');
expect(input.error, EmailError.invalid);
});
test('hashCode is correct', () {
const initial = EmailInput.initial();
const dirty = EmailInput(value: '[email protected]');
expect(initial.hashCode, Object.hashAll([initial.value, initial.isPure]));
expect(dirty.hashCode, Object.hashAll([dirty.value, dirty.isPure]));
});
test('equality is correct', () {
expect(const EmailInput.initial(), equals(const EmailInput.initial()));
expect(const EmailInput(value: '[email protected]'), equals(const EmailInput(value: '[email protected]')));
expect(const EmailInput(value: 'jane'), isNot(equals(const EmailInput(value: '[email protected]'))));
});
test('toString is correct', () {
const initial = EmailInput.initial();
const dirty = EmailInput(value: '[email protected]');
expect(initial.toString(), equals('EmailInput(value: , isValid: false)'));
expect(dirty.toString(), equals('EmailInput(value: [email protected], isValid: true)'));
});
});
group('PasswordInput', () {
test('value is correct', () {
const initial = PasswordInput.initial(value: 'Pswd1234');
const dirty = EmailInput(value: 'Pswd1234');
expect(initial.value, 'Pswd1234');
expect(dirty.value, 'Pswd1234');
});
test('isPure is true when super.initial is used', () {
const input = PasswordInput.initial(value: 'Pswd1234');
expect(input.isPure, isTrue);
});
test('isPure is false when default constructor is used', () {
const input = PasswordInput(value: 'Pswd1234');
expect(input.isPure, isFalse);
});
test('isValid is true if super.initial is used and value is valid', () {
const input = PasswordInput.initial(value: 'Pswd1234');
expect(input.isValid, isTrue);
expect(input.invalid, isFalse);
expect(input.error, isNull);
});
test('isValid is false if super.initial is used and input is invalid', () {
const input = PasswordInput.initial(value: 'Pswd');
expect(input.isValid, isFalse);
expect(input.invalid, isTrue);
});
test('isValid is true if default constructor is used and input is valid', () {
const input = PasswordInput(value: 'Pswd1234');
expect(input.isValid, isTrue);
expect(input.invalid, isFalse);
expect(input.error, isNull);
});
test('isValid is false if default constructor is used and input is invalid', () {
const input = PasswordInput(value: 'Pswd');
expect(input.isValid, isFalse);
expect(input.invalid, isTrue);
});
test('error is PasswordError.empty if value is empty', () {
const input = PasswordInput();
expect(input.error, PasswordError.empty);
});
test('error is PasswordError.invalid if value is invalid', () {
const input = PasswordInput(value: 'Pswd');
expect(input.error, PasswordError.invalid);
});
test('hashCode is correct', () {
const initial = PasswordInput.initial();
const dirty = PasswordInput(value: 'Pswd1234');
expect(initial.hashCode, Object.hashAll([initial.value, initial.isPure]));
expect(dirty.hashCode, Object.hashAll([dirty.value, dirty.isPure]));
});
test('equality is correct', () {
expect(const PasswordInput.initial(), equals(const PasswordInput.initial()));
expect(const PasswordInput(value: 'Pswd'), equals(const PasswordInput(value: 'Pswd')));
expect(const PasswordInput(value: 'Pswd'), isNot(equals(const PasswordInput(value: 'Pswd1234'))));
});
test('toString is correct', () {
const initial = PasswordInput.initial();
const dirty = PasswordInput(value: 'Pswd1234');
expect(initial.toString(), equals('PasswordInput(value: , isValid: false)'));
expect(dirty.toString(), equals('PasswordInput(value: Pswd1234, isValid: true)'));
});
});
});
}
import 'package:form/src/form.dart';
class EmailInput extends FormInput<String, EmailError> {
const EmailInput({super.value = ''});
const EmailInput.initial({super.value = ''}) : super.initial();
static final RegExp _emailRegExp = RegExp(
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$',
);
@override
EmailError? validator(String value) {
if (value.isEmpty) {
return EmailError.empty;
}
if (!_emailRegExp.hasMatch(value)) {
return EmailError.invalid;
}
return null;
}
}
enum EmailError { empty, invalid }
class PasswordInput extends FormInput<String, PasswordError> {
const PasswordInput({super.value = ''});
const PasswordInput.initial({super.value = ''}) : super.initial();
static final RegExp _passwordRegExp = RegExp(
r'^[\S]{8,}$',
);
@override
PasswordError? validator(String value) {
if (value.isEmpty) {
return PasswordError.empty;
}
if (!_passwordRegExp.hasMatch(value)) {
return PasswordError.invalid;
}
return null;
}
}
enum PasswordError { empty, invalid }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment