formz 0.8.0 formz: ^0.8.0 copied to clipboard
A unified form representation in Dart which aims to simplify form representation and validation in a generic way.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:formz/formz.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Formz Example')),
body: Padding(
padding: const EdgeInsets.all(24),
child: SingleChildScrollView(child: MyForm()),
),
),
);
}
}
class MyForm extends StatefulWidget {
MyForm({super.key, Random? seed}) : seed = seed ?? Random();
final Random seed;
@override
State<MyForm> createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
final _key = GlobalKey<FormState>();
late MyFormState _state;
late final TextEditingController _emailController;
late final TextEditingController _passwordController;
void _onEmailChanged() {
setState(() {
_state = _state.copyWith(email: Email.dirty(_emailController.text));
});
}
void _onPasswordChanged() {
setState(() {
_state = _state.copyWith(
password: Password.dirty(_passwordController.text),
);
});
}
Future<void> _onSubmit() async {
if (!_key.currentState!.validate()) return;
setState(() {
_state = _state.copyWith(status: FormzSubmissionStatus.inProgress);
});
try {
await _submitForm();
_state = _state.copyWith(status: FormzSubmissionStatus.success);
} catch (_) {
_state = _state.copyWith(status: FormzSubmissionStatus.failure);
}
if (!mounted) return;
setState(() {});
FocusScope.of(context)
..nextFocus()
..unfocus();
const successSnackBar = SnackBar(
content: Text('Submitted successfully! 🎉'),
);
const failureSnackBar = SnackBar(
content: Text('Something went wrong... 🚨'),
);
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
_state.status.isSuccess ? successSnackBar : failureSnackBar,
);
if (_state.status.isSuccess) _resetForm();
}
Future<void> _submitForm() async {
await Future<void>.delayed(const Duration(seconds: 1));
if (widget.seed.nextInt(2) == 0) throw Exception();
}
void _resetForm() {
_key.currentState!.reset();
_emailController.clear();
_passwordController.clear();
setState(() => _state = MyFormState());
}
@override
void initState() {
super.initState();
_state = MyFormState();
_emailController = TextEditingController(text: _state.email.value)
..addListener(_onEmailChanged);
_passwordController = TextEditingController(text: _state.password.value)
..addListener(_onPasswordChanged);
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Form(
key: _key,
child: Column(
children: [
TextFormField(
key: const Key('myForm_emailInput'),
controller: _emailController,
decoration: const InputDecoration(
icon: Icon(Icons.email),
labelText: 'Email',
helperText: 'A valid email e.g. joe.doe@gmail.com',
),
validator: (value) => _state.email.validator(value ?? '')?.text(),
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
),
TextFormField(
key: const Key('myForm_passwordInput'),
controller: _passwordController,
decoration: const InputDecoration(
icon: Icon(Icons.lock),
helperText:
'At least 8 characters including one letter and number',
helperMaxLines: 2,
labelText: 'Password',
errorMaxLines: 2,
),
validator: (value) =>
_state.password.validator(value ?? '')?.text(),
obscureText: true,
textInputAction: TextInputAction.done,
),
const SizedBox(height: 24),
if (_state.status.isInProgress)
const CircularProgressIndicator()
else
ElevatedButton(
key: const Key('myForm_submit'),
onPressed: _onSubmit,
child: const Text('Submit'),
),
],
),
);
}
}
class MyFormState with FormzMixin {
MyFormState({
Email? email,
this.password = const Password.pure(),
this.status = FormzSubmissionStatus.initial,
}) : email = email ?? Email.pure();
final Email email;
final Password password;
final FormzSubmissionStatus status;
MyFormState copyWith({
Email? email,
Password? password,
FormzSubmissionStatus? status,
}) {
return MyFormState(
email: email ?? this.email,
password: password ?? this.password,
status: status ?? this.status,
);
}
@override
List<FormzInput<dynamic, dynamic>> get inputs => [email, password];
}
enum EmailValidationError { invalid, empty }
class Email extends FormzInput<String, EmailValidationError>
with FormzInputErrorCacheMixin {
Email.pure([super.value = '']) : super.pure();
Email.dirty([super.value = '']) : super.dirty();
static final _emailRegExp = RegExp(
r'^[a-zA-Z\d.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z\d-]+(?:\.[a-zA-Z\d-]+)*$',
);
@override
EmailValidationError? validator(String value) {
if (value.isEmpty) {
return EmailValidationError.empty;
} else if (!_emailRegExp.hasMatch(value)) {
return EmailValidationError.invalid;
}
return null;
}
}
enum PasswordValidationError { invalid, empty }
class Password extends FormzInput<String, PasswordValidationError> {
const Password.pure([super.value = '']) : super.pure();
const Password.dirty([super.value = '']) : super.dirty();
static final _passwordRegex =
RegExp(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$');
@override
PasswordValidationError? validator(String value) {
if (value.isEmpty) {
return PasswordValidationError.empty;
} else if (!_passwordRegex.hasMatch(value)) {
return PasswordValidationError.invalid;
}
return null;
}
}
extension on EmailValidationError {
String text() {
switch (this) {
case EmailValidationError.invalid:
return 'Please ensure the email entered is valid';
case EmailValidationError.empty:
return 'Please enter an email';
}
}
}
extension on PasswordValidationError {
String text() {
switch (this) {
case PasswordValidationError.invalid:
return '''Password must be at least 8 characters and contain at least one letter and number''';
case PasswordValidationError.empty:
return 'Please enter a password';
}
}
}