ux_kit_hooks
ux_kit_hooks
builds on top of ux_kit_core and provides Flutter Hooks support for managing form state, validation, and inputs.
It makes working with forms declarative, reactive, and simple, while keeping behavior separate from styling.
Getting started
Prerequisites:
- Flutter 3.0 or higher
- Dart 3.8 or higher
Usage
class SignUpForm {
const SignUpForm({
this.firstName = '',
this.lastName = '',
this.email = '',
this.password = '',
});
final String firstName;
final String lastName;
final String email;
final String password;
factory SignUpForm.formatter(List values) {
return SignUpForm(
firstName: values[0],
lastName: values[1],
email: values[2],
password: values[3],
);
}
static const FormConfig<SignUpForm> config = FormConfig(
list: [
TextConfig(
label: 'First Name',
hint: 'first name',
validation: Validation(
validator: Validator.required(),
message: 'First name is required',
),
),
TextConfig(
label: 'Last Name',
hint: 'last name',
validation: Validation(
validator: Validator.required(),
message: 'Last name is required',
),
),
EmailConfig(
label: 'Email',
hint: 'email',
validation: Validation.queue([
Validation(
validator: Validator.required(),
message: 'Email is required',
),
Validation(
validator: Validator.email,
message: 'Email is invalid',
),
]),
),
PasswordConfig(
label: 'Password',
hint: 'password',
validation: Validation.queue([
Validation(
key: 'password',
validator: Validator.required(),
message: 'Password is required',
),
Validation(
key: 'password',
validator: Validator.minLength(8),
message: 'Password must be at least 8 characters long',
),
]),
),
PasswordConfig(
label: 'Confirm Password',
hint: 'confirm password',
validation: Validation.relatedTo('password',
validatorBuilder: IsSameAsValidator.new,
message: 'Passwords do not match',
),
),
],
formatter: SignUpForm.formatter,
);
}
class SignUpFormWidget extends StatelessWidget {
const SignUpFormWidget({super.key});
@override
Widget build(BuildContext context) {
return MyForm(
config: SignUpForm.config,
onSubmit: (SignUpForm form) {
// TODO: submit form
},
);
}
}
class MyForm<T> extends HookWidget {
const MyForm({
super.key,
required this.config,
required this.onSubmit,
this.value,
});
final FormConfig<T> config;
final void Function(T formattedValue) onSubmit;
final FutureOr<List>? value;
@override
Widget build(BuildContext context) {
final form = useInput.form(
config: config,
value: value,
);
return ListView(
children: [
for (final input in form.list)
if (input case FieldInput())
MyTextField(input: input),
ElevatedButton(
onPressed: () {
if (form.validate()) {
onSubmit(form.formatted);
}
},
child: const Text('Submit'),
),
],
);
}
}
class MyTextField extends HookWidget {
const MyTextField({super.key, required this.input});
final FieldInput input;
@override
Widget build(BuildContext context) {
String? errorText;
final validationResult = useListenable(input.validationResult).value;
if (validationResult case HasMessage(isValid: final isValid, message: final message) when !isValid && message is String) {
errorText = message;
}
bool obscureText = false;
if (input case PasswordInput(obscure: final obscure)) {
obscureText = useListenable(obscure).value;
}
return TextField(
controller: input.textEditing,
focusNode: input.focusNode,
autocorrect: input is! PasswordInput,
obscureText: obscureText,
keyboardType: input.keyboardType,
inputFormatters: input.inputFormatters,
autofillHints: input.autofillHints,
decoration: InputDecoration(
labelText: input.config.label,
hintText: input.config.hint,
errorText: errorText,
),
);
}
}
extension on FieldInput {
TextInputType get keyboardType {
return switch (this) {
PasswordInput() => TextInputType.visiblePassword,
PhoneInput() => TextInputType.phone,
TextInput() => TextInputType.text,
NumberInput() => TextInputType.number,
EmailInput() => TextInputType.emailAddress,
DateInput() => TextInputType.datetime,
AddressInput() => TextInputType.streetAddress,
};
}
List<TextInputFormatter>? get inputFormatters {
return switch (this) {
PhoneInput() ||
NumberInput() => [FilteringTextInputFormatter.digitsOnly],
_ => null,
};
}
List<String>? get autofillHints {
return switch (this) {
PasswordInput() => [AutofillHints.password],
PhoneInput() => [
AutofillHints.telephoneNumberNational,
AutofillHints.telephoneNumber,
],
EmailInput() => [AutofillHints.email],
DateInput() => [AutofillHints.birthday],
AddressInput() => [AutofillHints.fullStreetAddress],
_ => null,
};
}
}
Dependencies
This package depends on: