Form Validation + Async Save
This walkthrough shows .set(...), computed, effect, and useAsyncData working together for a real form.
Model & validation
dart
final name = signal(context, '');
final email = signal(context, '');
final dirty = signal(context, false);
final isValid = computed<bool>(context, (_) {
final n = name().trim();
final e = email().trim();
return n.length >= 2 && e.contains('@');
});
final canSubmit = computed<bool>(context, (_) {
return dirty() && isValid();
});Async save
dart
final submit = useAsyncData<String>(context, () async {
await Future.delayed(const Duration(milliseconds: 600));
return 'Saved ${name()} <${email()}>';
});Track saves with an effect:
dart
final lastSaved = signal(context, 'never');
effect(context, () {
if (submit.status == AsyncStatus.success) {
lastSaved.set('just now');
}
});UI wiring
dart
TextField(
onChanged: (value) {
name.set(value);
dirty.set(true);
},
decoration: const InputDecoration(labelText: 'Name'),
);
TextField(
onChanged: (value) {
email.set(value);
dirty.set(true);
},
decoration: const InputDecoration(labelText: 'Email'),
);
SignalBuilder(
builder: (context) => ElevatedButton(
onPressed: canSubmit() ? () => submit.refresh() : null,
child: const Text('Save'),
),
);
SignalBuilder(
builder: (context) => switch (submit.state) {
AsyncDataSuccess(value: final msg) => Text(msg),
AsyncDataError(error: final err) => Text('Error: $err'),
AsyncDataLoading() => const Text('Saving...'),
_ => const SizedBox.shrink(),
},
);This pattern keeps validation in computed, writes through .set(...), triggers async work via useAsyncData, and updates UI with SignalBuilder.
Flutter Example (from the example app)
dart
class FormWorkflowSection extends StatelessWidget {
const FormWorkflowSection({super.key});
@override
Widget build(BuildContext context) {
final name = signal<String>(context, '');
final email = signal<String>(context, '');
final dirty = signal<bool>(context, false);
final lastSaved = signal<String>(context, 'never');
final isValid = computed<bool>(context, (_) {
final n = name().trim();
final e = email().trim();
return n.length >= 2 && e.contains('@');
});
final canSubmit = computed<bool>(context, (_) {
return dirty() && isValid();
});
final submit = useAsyncData<String>(context, () async {
await Future<void>.delayed(const Duration(milliseconds: 600));
return 'Saved ${name()} <${email()}>';
});
effect(context, () {
if (submit.status == AsyncStatus.success) {
lastSaved.set('just now');
}
});
final nameController = useMemoized(context, () => TextEditingController());
final emailController = useMemoized(context, () => TextEditingController());
effect(context, () {
onEffectDispose(() {
nameController.dispose();
emailController.dispose();
});
});
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
key: const Key('form-name'),
controller: nameController,
decoration: const InputDecoration(labelText: 'Name'),
onChanged: (value) {
name.set(value);
dirty.set(true);
},
),
const SizedBox(height: 8),
TextField(
key: const Key('form-email'),
controller: emailController,
decoration: const InputDecoration(labelText: 'Email'),
onChanged: (value) {
email.set(value);
dirty.set(true);
},
),
const SizedBox(height: 8),
SignalBuilder(
builder: (context) {
final valid = isValid();
final can = canSubmit();
final status = switch (submit.status) {
AsyncStatus.idle => 'Idle',
AsyncStatus.pending => 'Saving...',
AsyncStatus.success => submit.data ?? 'Saved',
AsyncStatus.error => 'Error: ${submit.error?.error ?? 'unknown'}',
};
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('valid: ${valid ? 'yes' : 'no'}'),
Text('can submit: ${can ? 'yes' : 'no'}'),
Text('status: $status'),
Text('last saved: ${lastSaved()}'),
const SizedBox(height: 8),
ElevatedButton(
onPressed: can ? () async => submit.refresh() : null,
child: const Text('Save'),
),
],
);
},
),
],
);
}
}