zema_forms 0.1.0
zema_forms: ^0.1.0 copied to clipboard
Flutter form widgets backed by Zema schemas. Surgical per-field rebuilds, isTouched UX, auto-focus on first error, and a form-level error banner.
zema_forms #
Flutter form widgets backed by Zema schemas.
- Surgical per-field rebuilds: only the field in error rebuilds on each keystroke.
- "First contact" UX: errors appear after the field loses focus or the form is submitted, never on the first character.
- Auto-focus: failed
submit()moves focus to the first field in error automatically. - Form-level error banner via
submitErrorsfor hidden or conditional fields. - Works with Flutter's native
Formwidget (zero migration cost).
Installation #
dependencies:
zema: ^0.5.0
zema_forms: ^0.1.0
Quick start #
import 'package:zema/zema.dart';
import 'package:zema_forms/zema_forms.dart';
final _schema = z.object({
'email': z.string().email(),
'password': z.string().min(8),
});
class LoginForm extends StatefulWidget {
const LoginForm({super.key});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
late final _ctrl = ZemaFormController(schema: _schema);
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
void _onSubmit() {
final data = _ctrl.submit();
if (data != null) {
// data is Map<String, dynamic> with validated values
}
}
@override
Widget build(BuildContext context) {
return ZemaForm(
controller: _ctrl,
child: Column(
children: [
ZemaTextField(
field: 'email',
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
),
ZemaTextField(
field: 'password',
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
),
ElevatedButton(
onPressed: _onSubmit,
child: const Text('Sign in'),
),
],
),
);
}
}
Error visibility #
Errors are validated on every keystroke but shown only when:
- The field has lost focus at least once (
isTouched), or submit()has been called (isSubmitted).
This prevents showing "Email invalide" after the user types the first character.
Form-level error banner #
When a form has conditional or hidden fields, pair submit() with a banner that reads submitErrors:
ValueListenableBuilder<List<ZemaIssue>>(
valueListenable: _ctrl.submitErrors,
builder: (context, issues, _) {
if (issues.isEmpty) return const SizedBox.shrink();
return Text('${issues.length} field(s) require attention.');
},
)
Native Form bridge #
TextFormField(
controller: _ctrl.controllerFor('email'),
validator: _ctrl.validatorFor('email'),
)
Numeric fields #
TextField always produces a String. Use the coercion layer for numeric fields:
z.object({
'age': z.coerce().integer(min: 0, max: 150),
'price': z.coerce().decimal(),
})
API reference #
| Member | Description |
|---|---|
ZemaFormController(schema:) |
Create a controller for the given ZemaObject |
controllerFor(field) |
TextEditingController for the field |
errorsFor(field) |
ValueNotifier<List<ZemaIssue>> for per-field errors |
touchedFor(field) |
ValueNotifier<bool> — true after field loses focus |
markTouched(field) |
Force-mark a field as touched |
isSubmitted |
ValueNotifier<bool> — true after first submit() |
submitErrors |
Issues from the last failed submit() call; empty on success |
submit() |
Validate, auto-focus first error, return typed output or null |
validatorFor(field) |
String? Function(String?) for TextFormField.validator |
setValue(field, value) |
Set field text programmatically |
hasErrors |
true when any field has active errors |
reset() |
Clear all text, errors, and state |
dispose() |
Release all resources |
Related packages #
zema: core schema libraryzema_forms: Flutter form integrationzema_hive: Hive integration