Skip to main content

advanced-forms-build-forms

Build Flutter forms on advanced_forms. Use when building or changing a form; when wiring validation, cross-field rules, server field errors, or an edit-form prefill; when a section or wizard step joins the form; when UI collects structured input without being called a form; or when the Flutter project does not depend on advanced_forms yet.

Datos de origen

Repositorio
leancodepl/advanced_forms
Última actividad en el origen
21 de octubre de 2026 a las 09:58
Idioma detectado de SKILL.md
inglés
Estrellas
21
Forks
4

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Explorador de archivos
8 archivos

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
advanced-forms-build-forms
description
Build Flutter forms on advanced_forms. Use when building or changing a form; when wiring validation, cross-field rules, server field errors, or an edit-form prefill; when a section or wizard step joins the form; when UI collects structured input without being called a form; or when the Flutter project does not depend on advanced_forms yet.
# advanced_forms Typed form controllers. One import: `package:advanced_forms/advanced_forms.dart`. Flutter >= 3.19.0 (Dart 3.3). Controllers are `ChangeNotifier` and `ValueListenable` from the SDK. A **form** (`AdvancedFormController`) owns **fields** (`AdvancedFieldController<T, E>`). `T` is the value. `E extends Object` is your error — `null` means no error. Each widget subscribes to one field through `AdvancedFieldBuilder`. Reference files use `MyError`, `form`, and `translate` (`String Function(MyError)`). Porting `leancode_forms` or advanced_forms 0.1.x is a rewrite onto this shape: `AdvancedFormController` replaces `FormGroupCubit`, and `Advanced*Controller` replaces the `*Cubit`s. Follow `MIGRATION.md` in the package root, and `CHANGELOG.md` for version history. ## 1. Depend `advanced_forms` is under `dependencies`. When it is missing, add it from the project root: ```sh flutter pub add advanced_forms ``` **Done when** `package:advanced_forms/advanced_forms.dart` resolves. ## 2. Own the fields Subclass `AdvancedFormController`. Call `registerFields` once, in the constructor, with every field. The parameter is `List<AdvancedFieldController<dynamic, dynamic>>` — let the literal infer. The form then owns disposal, `validate()`, `resetAll()`, and the `wasModified` baseline — each field's value at this call. A later `registerFields` replaces the list. Dropped fields stop participating and are still disposed with the form. Re-registering the **same** list sets a new baseline — [server errors and prefilling](references/server.md). A field left out of `registerFields` is never disposed. Spell `E`. With no `validator` it infers to `Object`, and every `switch` on your error enum breaks: `AdvancedTextFieldController<MyError>()`. | Controller | `T` | Write | | --- | --- | --- | | `AdvancedTextFieldController<E>` | `String` | `textController` | | `AdvancedBooleanFieldController<E>` | `bool` | `getValueSetter()` | | `AdvancedSingleSelectFieldController<V, E>` | `V?` | `select(V?)` | | `AdvancedMultiSelectFieldController<V, E>` | `Set<V>` | `toggleElement` / `addValue` / `removeValue` | | `AdvancedFieldController<T, E>` | any | `setValue` — slider, stepper, rating, derived total | Dispose only the form. It disposes every field from `registerFields` and every subform ever passed to `addSubform`, attached or not. `ChangeNotifierProvider(create: (_) => MyForm())` disposes it; a `StatefulWidget` disposes it in `State.dispose`. `context.read` for actions. `AdvancedFieldBuilder`, `ValueListenableBuilder`, or `context.select` for rebuilds. `registerFields`, `addSubform`, `removeSubform`, `setValidationEnabled`, `subscribeToFields`, and `setValue` throw `StateError` on a disposed controller. `validate()` returns `false`. **Done when** every field is in that one `registerFields` call and each `E` is your error type. Select options, field state, or a text transform: [fields](references/fields.md). ## 3. Bind One `AdvancedFieldBuilder<T, E>` per field. Type arguments match the controller: `String`, `bool`, `V?`, `Set<V>`. Its third builder argument is a `child` that does not depend on field state. Text inputs take `controller: field.textController` and `focusNode: field.focusNode`. The field owns both and keeps the text in two-way sync. Pass `readOnly: state.readOnly`. Map `state.error` through `translate` after promoting `E?` to `E`. Validate through these controllers. Flutter's `Form`, `GlobalKey<FormState>`, and `autovalidateMode` are a second system. A `TextFormField` with no `Form` ancestor is a `TextField` plus a decoration. **Done when** each field rebuilds from its own builder and every text input uses `field.textController`. Dropdown, checkbox, multi-select, slider, or focus: [binding](references/binding.md). ## 4. Validate, then submit `ValidationMode` on the form decides which events validate a field. It reaches every field and subform, including ones added later. The mode stays what you set — the first submit does not turn on live validation. An **untouched** field validates nothing on its own, in every mode. Only `setValue` marks a field edited. A **program write** (`prefill`, or `setOptions` dropping a selection that is no longer listed) stores the value and leaves the field untouched. `reset()` makes it untouched again. A round runs the sync validator first, then the async validator only if sync passed. | `ValidationMode` | on edit | on unfocus | when a dependency changes | | --- | --- | --- | --- | | `manual` (default) | — | — | — | | `onUserInteraction` | validate (async debounced) | — | sync only | | `onUnfocus` | — | validate | sync only | `await form.validate()` checks every field and subform, untouched included, and returns `false` when anything is invalid. It neither reads nor changes the mode. `state.isValid` and `form.value.canSubmit` mean no error is recorded, including on a field nobody has checked. Read values only after `validate()` returns `true` — that is what licenses a `!` on a nullable value. A second call re-runs the sync validators and awaits async checks already in flight. `submit()` is your method: `validate()`, then send. `onPressed` is a block body that awaits it. A tear-off (`onPressed: form.submit`) or an arrow (`() async => form.submit()`) drops the future, so the result is lost and a throw becomes an unhandled error. Gate the button on `state.validating`, which is true while an async validator round is in flight. `canSubmit` is false in that window and while any field is in `failedValidation`, which also disables the retry. A save request is your own flag, set before `await validate()` — `validating` covers validators only. **Done when** submit awaits `validate()` and field values are read only after `true`. A built-in validator, a combined validator, or an async check: [validation](references/validation.md). A rule or a value that follows another field: [cross-field](references/cross-field.md). Failure and retry on the button, an error summary, or focus on the first invalid field: [submit](references/submit.md). ## 5. Open the branch this screen needs - Server errors, read-only, reset, or prefilling an edit form: [server](references/server.md). - A section, a switch, or a wizard step: [subforms](references/subforms.md). **Done when** server errors, prefill, read-only, and subform behavior match that file, or the screen has none of them. ## Shape Steps 2–4, in code. ```dart import 'package:advanced_forms/advanced_forms.dart'; enum SignupError { required, tooShort } String translate(SignupError error) => switch (error) { SignupError.required => 'Required', SignupError.tooShort => 'Too short', }; class SignupFormController extends AdvancedFormController { SignupFormController() { registerFields([email, password]); } final email = AdvancedTextFieldController( validator: filled(SignupError.required), ); final password = AdvancedTextFieldController( validator: filled(SignupError.required) & atLeastLength(8, SignupError.tooShort), ); Future<bool> submit() async { if (!await validate()) { return false; } // email.fieldValue, password.fieldValue return true; } } ``` ```dart final form = context.read<SignupFormController>(); return Column( children: [ _SignupField(field: form.email, label: 'Email'), _SignupField(field: form.password, label: 'Password'), ValueListenableBuilder<AdvancedFormState>( valueListenable: form, builder: (context, state, _) => ElevatedButton( onPressed: state.validating ? null : () async { await form.submit(); }, child: const Text('Sign up'), ), ), ], ); ``` ```dart class _SignupField extends StatelessWidget { const _SignupField({required this.field, required this.label}); final AdvancedTextFieldController<SignupError> field; final String label; @override Widget build(BuildContext context) { return AdvancedFieldBuilder<String, SignupError>( field: field, builder: (context, state, _) => TextFormField( controller: field.textController, focusNode: field.focusNode, readOnly: state.readOnly, decoration: InputDecoration( labelText: label, errorText: state.error == null ? null : translate(state.error!), ), ), ); } } ```
Ver en GitHub