Skip to main content

advanced-forms-build-forms

Build Flutter forms with the advanced_forms package (AdvancedFormController, AdvancedTextFieldController, AdvancedFieldBuilder). Use whenever the user creates a form, adds or edits form fields, wires validation (sync, async, or cross-field), chooses when errors appear (ValidationMode), builds dropdowns/checkboxes/multi-selects/sliders bound to field controllers, handles submit buttons or server-side errors, or works with subforms in any Flutter project — whether or not it depends on advanced_forms yet (add it with `flutter pub add advanced_forms`) — even if they never say the word "form".

معلومات المصدر

المستودع
leancodepl/advanced_forms
آخر نشاط في المصدر
٢٩ سبتمبر ٢٠٢٦ في ١٠:٤٦
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٢٠
التفرعات
٣

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
advanced_forms-build-forms
description
Build Flutter forms with the advanced_forms package (AdvancedFormController, AdvancedTextFieldController, AdvancedFieldBuilder). Use whenever the user creates a form, adds or edits form fields, wires validation (sync, async, or cross-field), chooses when errors appear (ValidationMode), builds dropdowns/checkboxes/multi-selects/sliders bound to field controllers, handles submit buttons or server-side errors, or works with subforms in any Flutter project — whether or not it depends on advanced_forms yet (add it with `flutter pub add advanced_forms`) — even if they never say the word "form".
# advanced_forms Typed form controllers for Flutter, no framework on top. Everything is `ChangeNotifier` + `ValueListenable` from the SDK. One import: `package:advanced_forms/advanced_forms.dart`. Requires Flutter >= 3.19.0 (Dart 3.3). A **form controller** (`AdvancedFormController`) owns **field controllers** (`AdvancedFieldController<T, E>`). `T` is the value type, `E` is *your* error type (`E extends Object`, never nullable — `null` means "no error"). Plain `String` errors are fine; an enum or sealed class scales better. Widgets subscribe to one field each via `AdvancedFieldBuilder`; the form aggregates tree-wide state. Snippets assume in scope: `MyError`, your error enum; `form`, a form controller; `translate`, an `ErrorTranslator<MyError>`. ## Setup Before writing form code, check that `pubspec.yaml` lists `advanced_forms` under `dependencies`. If it does not, add it from the project root — do not hand-write a version: ```sh flutter pub add advanced_forms ``` That pins the current release and runs `pub get`. Then import `package:advanced_forms/advanced_forms.dart` and continue below. If the project has `leancode_forms` instead, it is the discontinued predecessor: the API in this skill replaces it (`AdvancedFormController` for `FormGroupCubit`, `Advanced*Controller`s for the `*Cubit`s), see the migration guide in the repository before porting. ## Quick start ```dart import 'package:advanced_forms/advanced_forms.dart'; enum SignupError { required, tooShort } 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; } // values were checked — safe to send: email.fieldValue, password.fieldValue return true; } } ``` ```dart // Inside SignupScreen.build: final form = context.read<SignupFormController>(); return Column( children: [ _SignupField(field: form.email, label: 'Email'), _SignupField(field: form.password, label: 'Password'), ElevatedButton( // A block body that awaits. `onPressed: form.submit` compiles but drops the // future, losing the result and turning any throw into an unhandled error. onPressed: () async { await form.submit(); }, child: const Text('Sign up'), ), ], ); ``` ```dart // One reusable field widget, at top level. 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, // NEVER your own TextEditingController focusNode: field.focusNode, readOnly: state.readOnly, // add enabled: !state.readOnly to also look disabled decoration: InputDecoration( labelText: label, errorText: state.error == null ? null : translate(state.error!), ), ), ); } } ``` Default UX: **nothing validates until `submit`.** Errors appear on the first `validate()`, and editing a field clears its error — but the error only returns on the next `validate()`. For live feedback, set a `ValidationMode`; see *Validation*. Two rules prevent most bugs: - **Call `registerFields()` once, in the constructor, with every field.** The parameter is `List<AdvancedFieldController<dynamic, dynamic>>`, so mixed value types go in one list — let the literal infer its type rather than annotating it. The form then owns their lifecycle — disposal, `wasModified` tracking, `validate()`, `resetAll()`. A second call *replaces* the list: earlier fields stop participating but are still disposed with the form. (Deliberately re-registering the *same* list to re-baseline an edit form is the one exception — see *Prefilling an edit form*.) A field never registered at all belongs to nobody and is never disposed. - **Bind text widgets to `field.textController`.** The field owns a `TextEditingController` and keeps it in two-way sync, so do not allocate your own and do not wire `onChanged` + `initialValue` for text fields. ## Field controllers | Controller | Value type | Constructor | | --- | --- | --- | | `AdvancedTextFieldController<E>` | `String` | `{initialValue = '', validator, asyncValidation, focusNode, name}`; owns `textController` | | `AdvancedBooleanFieldController<E>` | `bool` | `{initialValue = false, validator, asyncValidation, focusNode, name}` | | `AdvancedSingleSelectFieldController<V, E>` | `V?` | `{required V? initialValue, required List<V> options, validator, asyncValidation, focusNode, name}`; set with `select(V?)`, `null` clears; `setOptions(List<V>)` swaps the list | | `AdvancedMultiSelectFieldController<V, E>` | `Set<V>` | `{required Set<V> initialValue, required List<V> options, validator, asyncValidation, focusNode, name}`; set with `toggleElement` / `addValue` / `removeValue`; `setOptions(List<V>)` swaps the list | All support `reset()`, `prefill()`, `markReadOnly()` / `unmarkReadOnly()`, `setError()`, `clearErrors()`, `setValidationMode()`, `validate()`, `subscribeToFields()`, `revalidateSync()`, `focus()`, `handleUnfocus()`, `getValueSetter()` (a `ValueSetter<T>?`, `null` while read-only, so a widget with a nullable `onChanged` disables itself), and the getters `fieldValue`, `error`, `name`, `lastFailure`, `isDisposed`. - **`focusNode` is on every controller**, not just the text one — the field makes and owns it; pass `focusNode:` to bind one you own and the field never disposes it. `focus()` only moves focus if **some widget actually binds `field.focusNode`**: a `TextField`, or a `Focus` / `FocusableActionDetector` around a slider, chip group or dropdown. A field nobody bound is a silent no-op — that is why "focus the first invalid field" seems to do nothing on non-text fields. - `name` is a readable `String?`. It labels the field in package error reports and as the `FocusNode` debug label, and is the natural key for error summaries and server-error maps. - `V` is unbounded on both selects. `options` is a **non-nullable** `List<V>`; only `initialValue` is nullable, on the single select. A required dropdown is `initialValue: null` plus `validator: notNull(MyError.required)`. - `select` and `addValue` **assert** the argument is one of `options` — and so does `toggleElement` when it adds. `prefill` does not assert, so check server-supplied values. - Both selects copy the `options` (and the multi-select the set) you pass, so mutating them later never reaches the field. `const {}` is a **Map**: an empty initial selection is `const <Topping>{}`. - **Options that depend on another field or on a request:** `field.setOptions(newList)`, e.g. from `aircraft.addListener(() => instructor.setOptions(instructorsFor(aircraft.fieldValue)))`. A selected value not on the new list is cleared (dropped, on the multi-select) as a *program* write, like `prefill`: an untouched field stays untouched, an edited one shows its `notNull` error at once; read-only fields are cleared too. Widgets rebuild even if the value stayed. Membership is `==` — a server copy of an option is not the option on the list, so use value types or resolve by id. - With no `validator`, `E` infers to its bound `Object`. That compiles and then breaks every `switch` on your error enum, so spell it out: `AdvancedTextFieldController<MyError>()`. - `AdvancedFieldController<T, E>` is **concrete** — construct it directly for any value with no controller of its own (a slider, stepper, rating, derived total): `AdvancedFieldController<int, MyError>(initialValue: 0, name: 'total')`. Full constructor: `{required T initialValue, Validator<T, E>? validator, AsyncValidation<T, E>? asyncValidation, FocusNode? focusNode, String? name}`. Ordinary writes need no `force:` — that is only for writing to a field you marked read-only. To transform text as the user types (normalize, mask, uppercase), extend `AdvancedTextFieldController<E>` and override `setValue`: ```dart class PhoneFieldController<E extends Object> extends AdvancedTextFieldController<E> { PhoneFieldController({ String initialValue = '', super.validator, super.asyncValidation, super.focusNode, super.name, }) : super(initialValue: _digits(initialValue)); static String _digits(String value) => value.replaceAll(RegExp(r'\D'), ''); @override void setValue(String newValue, {bool force = false}) => super.setValue(_digits(newValue), force: force); } ``` Every write to `textController` — keystroke, paste, programmatic `.text =` — goes through the public `setValue`, so the override always runs and the transformed value is written back with the caret kept on the same characters. Three writes **bypass** `setValue` and need the same treatment: the constructor's `initialValue` (done above), `reset()` (returns to that same normalized value), and `prefill()`. Everything you do not transform forwards as a super-parameter; `initialValue` cannot, because it has to pass through `_digits` first, so it goes in the explicit `super(...)`. The two mix freely. Signatures for overriding: `void setValue(T newValue, {bool force = false})`, `void prefill(T newValue, {bool force = false})`. ### Field state — `AdvancedFieldState<T, E>` `AdvancedFieldBuilder` hands you this; `field.value` **is** this state object. State is value-equal, so a no-op write notifies nobody. | Member | Meaning | | --- | --- | | `value` | the current value — `field.fieldValue` is the shortcut | | `error` | `validationError ?? asyncError` — `field.error` is the shortcut | | `validationError` / `asyncError` | what the sync path (validator or `setError`) / the async round recorded | | `status` | `FieldStatus.valid` \| `invalid` \| `pending` \| `validating` \| `failedValidation` | | `isValid` / `isInvalid` | status is `valid` / `invalid` | | `isPending` / `isValidating` / `isInProgress` | waiting out the debounce / request in flight / either | | `isFailedValidation` | the async validator threw or timed out | | `readOnly` | `setValue` is a no-op (pass `force: true` to override) | | `validationMode` | the **effective** mode — a field whose form has validation switched off reports `manual` | `valid` means *no error recorded*, not *checked and passed*: a field nobody validated yet is `valid`. There is **no** "has this been checked" or "was this submitted" flag — `await validate()` is the guarantee; keep your own bool if the UI needs one. ## Binding widgets `AdvancedFieldBuilder<T, E>` wraps `ValueListenableBuilder` and rebuilds only when its own field notifies, so a keystroke rebuilds one subtree. Its third builder parameter is a `child:` passthrough for a subtree that does not depend on field state. Type arguments match the controller: `<String, E>` text, `<bool, E>` boolean, `<V?, E>` single select (over an `AdvancedSingleSelectFieldController<V, E>`), `<Set<V>, E>` multi select. Disable the widget while `state.readOnly`: pass `getValueSetter()` wherever the callback is a `ValueSetter<T>?` (`Switch.onChanged` takes it directly), and `state.readOnly ? null : …` where it is not. ```dart // Dropdown — DropdownButton in an InputDecorator compiles on every supported SDK. // Prefer it over DropdownButtonFormField, whose value parameter was renamed across versions. AdvancedFieldBuilder<Country?, MyError>( field: form.country, builder: (context, state, _) { final error = state.error; // promote E? to E: ErrorTranslator takes a non-null E return InputDecorator( decoration: InputDecoration(errorText: error == null ? null : translate(error)), child: DropdownButton<Country>( value: state.value, isExpanded: true, items: [ for (final option in form.country.options) DropdownMenuItem(value: option, child: Text(option.name)), ], onChanged: state.readOnly ? null : form.country.select, ), ); }, ) // A scalar field, wrapped in Focus so focus() can reach it. AdvancedFieldBuilder<int, MyError>( field: form.rating, builder: (context, state, _) { final setter = form.rating.getValueSetter(); return Focus( focusNode: form.rating.focusNode, // without this, form.rating.focus() does nothing child: InputDecorator( decoration: InputDecoration( errorText: state.error == null ? null : translate(state.error!), ), child: Slider( value: state.value.toDouble(), min: 0, // Slider asserts min <= value <= max — cover the field's initial value max: 5, divisions: 5, onChanged: setter == null ? null : (v) => setter(v.round()), ), ), ); }, ) ``` A `Focus` wrapper makes the node reachable so `focus()` lands there; whether the widget inside paints a focus ring is its own business. Widgets with no `errorText` slot — `Switch`, `CheckboxListTile`, a `Wrap` of `FilterChip`s for a multi-select — render `state.error` yourself or wrap them in an `InputDecorator` as above. Adapt the setter where the signature differs: `CheckboxListTile.onChanged` is `ValueChanged<bool?>?`, so `onChanged: setter == null ? null : (v) => setter(v ?? false)`. `ErrorTranslator<E>` is `String Function(E)` — a plain function, so write it wherever the labels belong: `String translate(MyError e) => switch (e) { MyError.required => 'Required', … };`. It takes a **non-null** `E` while `state.error` is `E?`, so promote before calling. One shared translator per enum is the cheapest start; pass a field-specific one where the wording differs ("Required" vs "Pick a country"). In a real app wrap each binding once in a reusable widget taking `field` + `ErrorTranslator<E>`. Do not wrap fields in Flutter's `Form` — no `Form`, no `GlobalKey<FormState>`, no `autovalidateMode`. This package *is* the validation system and a second one fights it. (A `TextFormField` outside a `Form` is fine — unregistered, it is a `TextField` with a decoration.) ## Validation ```dart
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub