Screenshot

screenshot

screenshot2

Simple Usage

add dependency

flutter pub add forme

create forme

FormeKey key = FormeKey();// formekey is a global key , also can be used to control form
Widget child = formContent;
Widget forme = Forme(
	key:key,
	child:child,
)

Forme Attributes

AttributeRequiredTypeDescription
keyfalseFormeKeya global key, also used to control form
childtrueWidgetform content widget
readOnlyfalseboolwhether form should be readOnly,default is false
onValueChangedfalseFormeValueChangedlisten form field's value change
initialValuefalseMap<String,dynamic>initialValue , will override FormField's initialValue
onErrorChangedfalseFormeErrorChangedlisten form field's errorText change
onWillPopfalseWillPopCallbackSignature for a callback that verifies that it's OK to call Navigator.pop
quietlyValidatefalseboolif this attribute is true , will not display default error text
onFocusChangedfalseFormeFocusChangedlisten form field's focus change

Differences Between Form and Forme

Forme is a form widget, but forme is not wrapped in a Form , because I don't want to refresh whole form after field's value changed or a validate performed , so it is a bit more complexable than Form.

DifferenceFormForme
AutovalidateModesupport both Form and fieldonly support field
onChangedwon't fired if value changed via state.didChange or state.setValuefired whenever field's value changed
rebuild strategywhen field value changed or perform a validation on field , all form fields will be rebuildedonly rebuild field that value changed or validated

Forme Fields

field type

-StatefulField
	- ValueField
	- CommonField

attributes supported by all stateful fields

AttributeRequiredTypeDescription
nametrueStringfield's id,should be unique in form
buildertrueFieldContentBuilderbuild field content
readOnlyfalseboolwhether field should be readOnly,default is false
onFocusChangedfalseFormeFocusChangedlisten field's focus change
modeltrueFormeModelFormeModel used to provider widget render data
onInitialedfalseFormeFieldInitialedtriggered when FormeFieldController initialed , when you specific a initialValue on ValueField or Forme , valueListenable will not listen this value , you can handle this value in onInitialed

attributes supported by all value fields

AttributeRequiredTypeDescription
onValueChangedfalseFormeValueChangedlisten field's value change
onErrorChangedfalseFormeErrorChangedlisten field's errorText change
validatorfalseFormFieldValidatorvalidate field's value
autovalidateModefalseAutovalidateModeauto validate mode , default is AutovalidateMode.disabled
initialValuefalsedynamicinitialValue,can be overwritten by forme's initialValue
onSavedfalseFormFieldSettertriggered when call forme or field's save method
decoratorBuilderfalseFormeDecoratorBuilderused to decorate a field

currently supported fields

NameReturn ValueNullable
FormeTextFieldstringfalse
FormeDateTimeTextFieldDateTimetrue
FormeNumberTextFieldnumtrue
FormeTimeTextFieldTimeOfDaytrue
FormeDateRangeTextFieldDateTimeRangetrue
FormeSliderdoublefalse
FormeRangeSliderRangeValuesfalse
FormeFilterChipList< T>false
FormeChoiceChipTtrue
FormeSingleCheckboxboolfalse
FormeSingleSwitchboolfalse
FormeDropdownButtonTtrue
FormeListTileList< T>false
FormeRadioGroupTtrue
FormeCupertinoPickerintfalse
FormeCupertinoTimerFieldDurationtrue
FormeCupertinoDateFieldDateTimetrue
FormeCupertinoSegmentedControlTtrue
FormeCupertinoSlidingSegmentedControlTtrue

Forme Model

you can update a widget easily with FormeModel

eg: if you want to update labelText of a FormeTextField, you can do this :

FormeFieldController controller = formKey.field(fieldName);
controller.update(FormeTextFieldModel(decoration:InputDecoration(labelText:'New Label')));

if you want to update items of FormeDropdownButton:

controller.updateModel(FormeDropdownButtonModel<String>(
	icon: SizedBox(
	width: 14,
	height: 14,
	child: CircularProgressIndicator(),
)));
Future<List<DropdownMenuItem<String>>> future =
	Future.delayed(Duration(seconds: 2), () {
	return FormeUtils.toDropdownMenuItems(
		['java', 'dart', 'c#', 'python', 'flutter']);
	});
future.then((value) {
controller.updateModel(FormeDropdownButtonModel<String>(
	icon: Icon(Icons.arrow_drop_down), items: value));
});

update model will auto copywith old model's attribute

Custom way display error text

if default error text display can not fit your needs , you can implementing a custom error display via ValueField's onErrorChanged or FormeValueFieldController's errorTextListenable

don't forget to set Forme's quieltyValidate attribute to true

via onErrorChanged

onErrorChanged will triggered whenever errorText of field changes , it is suitable when you want to update a stateful field according to error state of field

eg: change border color when error state changes

FormeTextField(
	validator: validator,
	onErrorChanged: (m, a) {
		InputBorder border = OutlineInputBorder(
				borderRadius: BorderRadius.circular(30.0),
				borderSide: BorderSide(color: a == null ? Colors.green : Colors.red, width: 1));
		m.updateModel(FormeTextFieldModel(
			decoration: InputDecoration(
				focusedBorder: border, enabledBorder: border)));
	},
),

via errorTextListenable

errorTextListenable is more convenient than onErrorChanged sometimes.

eg: when your want to display an valid or invalid suffix icon according to error state of field, in onErrorChanged , update model will rebuild whole field, but with errorTextListenable, you can only rebuild the suffix icon, below is an example to do this:

suffixicon: Builder(
	builder: (context) {
		FormeValueFieldController<String, FormeModel>
			controller = FormeFieldController.of(context);
		return ValueListenableBuilder<FormeValidateError?>(
			valueListenable: controller.errorTextListenable,
			child: const IconButton(
				onPressed: null,
				icon: const Icon(
				Icons.check,
				color: Colors.green,
				)),
			builder: (context, errorText, child) {
			if (errorText == null)
				return SizedBox();
			else
				return errorText.isPresent
					? const IconButton(
						onPressed: null,
						icon: const Icon(
						Icons.error,
						color: Colors.red,
						))
					: child!;
			});
	},
),

you shouldn't use FormeValueFieldController's errorTextListenable out of field ,use FormfieldListenable.errorTextListenable instead

lifecycle of FormeValueFieldController's errorTextListenable is same as field,when used it on another widget , errorTextListenable will disposed before removeListener , which will cause an error in debug mode

FormfieldListenable is from formKey.fieldListenable(fieldName),it's lifecycle is same as Forme, typically used to build a widget which is not a stateful field but relies on state of field , eg: you want to display error of a field on a Text Widget

Column(
	children:[
		FormeTextField(validator:validator,name:name),
		Builder(builder:(context){
			return ValueListenableBuilder<FormeValidateError?>(
				valueListenable: formeKey.fieldListenable(name).errorTextListenable,
				build: (context,errorText,child){
					return errorText == null || errorText.isNotPresent ? SizedBox() : Text(errorText.value!);
				},
			);
		})
])

listenable

there are four listenables in field

  1. focusListenable
  2. readOnlyListenable
  3. errorTextListenable
  4. valueListenable

get listenable from FormeFieldController

you can get focusListenable and readOnlyListenable from FormeFieldController, get valueListenable and errorTextListenable from FormeValueFieldController

these listenables lifecycle is same as field , so you can't use them out of field (you will get a changenotifier was used after being disposed error)

right way use these listenables

FormeTextField(
	decoratorBuilder:FormeDecoratorBuilder(),//decorator is a part of field 
	mode:FormeTextFieldModel(
		decoration:InputDecoration(
			suffixIcon: Builder(builder: (context) {
				FormeFieldController controller = FormeFieldController.of(context);
				return ValueListenable<bool>(valueListenable:controller.		valueListenable,builder:(context,focus,child){
					if(focus) return Icon(Icons.clear);
					return SizedBox();
				})
			}),
		),
	),
)

get listenable from FormeController

you can get FormeFieldListenable via FormeController.fieldListenable(String fieldName) , this listenable's lifecycle is same as Forme ,it's safe to use it out of field but in Forme.

FormeFieldListenable's valueListenable doesn't support generic type due to field's type may be changed at runtime though this shouldn't happend

when field disposed and recreated but name not changed, these listenables will continue listen this field

get listenable from FormeKey

you can get a LazyFormeFieldListenable via FormeKey.lazyFieldListenable(name).

LazyFormeFieldListenable's behavior is same as FormeFieldListenable,the unique difference is LazyFormeFieldListenable no need to wrap in a Builder or other widgets.

eg:

Forme(
	key:formeKey,
	child:Column(
		children:[
			ValueListenableBuilder(valueListenable:formeKey.fieldListenable(name)),//will cause an error
			Builder(builder:(context){
				return ValueListenableBuilder(valueListenable:formeKey.fieldListenable(name));
			}),//will works
			ValueListenableBuilder(valueListenable:formeKey.lazyFieldListenable(name)),//will works
		]
	)
)

validate

validate is supported by FormeValidateUtils

Validator NameSupport TypeWhen ValidWhen Invalid
notNulldynamicvalue is not nullvalue is null
sizeIterable Map String1. value is null 2. max & min is null 3. String's length or Collection's size is in min,maxString's length or Collection's size is not in min,max
minnum1. value is null 2. value is bigger than minvalue is smaller than min
maxnum1. value is null 2. value is smaller than maxvalue is bigger than max
notEmptyIterable Map String1. value is not null 2. String's length or Collection's size is bigger than zero1. value is null 2. String's length or Collection's size is zero
notBlankString1. value is null 2. value.trim()'s length is not nullvalue'length is zero after trimed
positivenum1. value is null 2. value is bigger than zerovalue is smaller than or equals zero
positiveOrZeronum1. value is null 2. value is bigger than or equals zerovalue is smaller than zero
negativenum1. value null 2. value is smaller than zerovalue is bigger than or equals zero
negativeOrZeronum1. value null 2. value is smaller than or equals zerovalue is bigger than zero
patternString1. value null 2. value matches patternvalue does not matches pattern
emailString1. value null 2. value is a valid emailvalue is not a valid email
urlString1. value is null 2. value is empty or value is a valid urlvalue is not a valid url
anyTany validators is validevery validators is invalid
allTall validators is validany validators is invalid

when you use validators from FormeValidateUtils , you must specific at least one errorText , otherwise errorText is an empty string

FormeKey Methods

whether form has a name field

bool hasField = formeKey.hasField(String name);

whether current form is readOnly

bool readOnly = formeKey.readOnly;

set readOnly

formeKey.readOnly = bool readOnly;

get field's controller

T controller = formeKey.field<T extends FormeFieldController>(String name);

get value field's controller

T controller = formeKey.valueField<T extends FormeValueFieldController>(String name);

get form data

Map<String, dynamic> data = formeKey.data;

validate

Map<FormeValueFieldController,String> errors = formKey.validate({bool quietly = false});

set form data

formeKey.data = Map<String,dynamic> data;

reset form

formeKey.reset();

save form

formeKey.save();

whether validate is quietly

bool quietlyValidate = formKey.quietlyValidate;

set quietlyValidate

formeKey.quieltyValidate = bool quietlyValidate;

get a lazy FormeFieldListenable

FormeFieldListenable listenable = formeKey.lazyFieldListenable(String name);

Forme Field Methods

get forme controller

FormeController formeController = field.formeController;

get field's name

String name = field.name

whether current field is readOnly

bool readOnly = field.readOnly;

set readOnly on field

field.readOnly = bool readOnly;

whether current field is focused

bool hasFocus = field.hasFocus;

focus|unfocus current Field

field.requestFocus();
field.unfocus();

set field model

field.model = FormeModel model;

update field model

field.updateModel(FormeModel model);

get field model

FormeModel model = field.model;

ensure field is visible in viewport

Future<void> result = field.ensureVisibe({Duration? duration,
      Curve? curve,
      ScrollPositionAlignmentPolicy? alignmentPolicy,
      double? alignment});

get focusListenable

ValueListenable<bool> focusListenable = field.focusListenable;

get readOnlyListenable

ValueListenable<bool> readOnlyListenable = field.readOnlyListenable;

Forme Value Field Methods

FormeValueFieldController is extended FormeFieldController

get field value

dynamic value = valueField.value;

set field value

valueField.value = dynamic data;

reset field

valueField.reset();

validate field

String? errorText = valueField.validate({bool quietly = false});

get error

FormeValidateError? error = valueField.error;

get decorator controller

FormeDecoratorController decoratorController = valueField.decoratorController;

get errorTextListenable

ValueListenable<FormeValidateError?>  errorTextListenable = valueField.errorTextListenable;

get valueListenable

ValueListenable<dynamic> valueListenable = valueField.valueListenable;

build your field

  1. create your FormeModel , if you don't need it , use FormeEmptyModel instead
  2. create your ValueField<T,E> , T is your field return value's type, E is your FormeModel's type
  3. if you want to create your custom State,extends ValueFieldState<T,E>

links below is some examples to help you to build your field

common field

  1. https://github.com/wwwqyhme/forme/blob/main/lib/src/field/forme_visible.dart
  2. https://github.com/wwwqyhme/forme/blob/main/lib/src/field/forme_flex.dart

value field

  1. https://github.com/wwwqyhme/forme/blob/main/lib/src/field/forme_filter_chip.dart
  2. https://github.com/wwwqyhme/forme/blob/main/lib/src/field/forme_radio_group.dart

Libraries

forme