Created
July 16, 2024 21:35
-
-
Save herveGuigoz/5c4c48dbabecbe997ff74824dbbd13ed to your computer and use it in GitHub Desktop.
Form
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)')); | |
}); | |
}); | |
}); | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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