Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active December 10, 2020 02:05
Show Gist options
  • Save PlugFox/981a8d2145a9878dae2b52aebd9fae88 to your computer and use it in GitHub Desktop.
Save PlugFox/981a8d2145a9878dae2b52aebd9fae88 to your computer and use it in GitHub Desktop.
Simple form managment
/// http://dartpad.dev/981a8d2145a9878dae2b52aebd9fae88
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(App());
class App extends StatelessWidget {
@override
Widget build(BuildContext context) => const MaterialApp(
title: 'FORM DATA',
home: Scaffold(
body: SafeArea(
child: Center(
child: Card(
margin: EdgeInsets.all(16),
elevation: 4,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(
8,
),
),
),
child: Padding(
padding: EdgeInsets.all(18),
child: FoxyForm(),
),
),
),
),
),
);
}
/// Виджет формы
@immutable
class FoxyForm extends StatefulWidget {
const FoxyForm({Key key}) : super(key: key);
@override
State<FoxyForm> createState() => _FoxyFormState();
}
class _FoxyFormState extends State<FoxyForm> {
final ValueNotifier<bool> _allFilled = ValueNotifier(false);
FormData _formData;
FieldStatus<String, FormData> _nameField;
FieldStatus<String, FormData> _ageField;
Iterable<FieldStatus> _fields;
final FocusNode _buttonFocusNode = FocusNode();
@override
void initState() {
super.initState();
_formData = FormData(
name: 'Иван',
age: '15',
);
_nameField = FieldStatus<String, FormData>(
formData: _formData,
value: (data) => data.name,
onChanged: (data, name) => data.name = name,
validator: (data, name) => name?.isEmpty ?? true ? 'Поле имени не должно быть пустым' : null,
);
_ageField = FieldStatus<String, FormData>(
formData: _formData,
value: (data) => data.age,
onChanged: (data, age) => data.age = age,
validator: (data, age) => age?.isEmpty ?? true ? 'Поле возраста не должно быть пустым' : null,
);
_fields = <FieldStatus>[_nameField, _ageField];
_setValidationStatus();
}
@override
void dispose() {
_allFilled.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Form(
autovalidateMode: AutovalidateMode.onUserInteraction,
onChanged: _setValidationStatus,
child: Builder(
builder: (context) => SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextFormField(
autofocus: true,
focusNode: _nameField.focusNode,
onEditingComplete: _nextEmpty,
initialValue: _formData.name,
onSaved: _nameField.onChanged,
onChanged: _nameField.onChanged,
validator: _nameField.validator,
keyboardType: TextInputType.name,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp('[A-Za-zА-Яа-я]+')),
],
decoration: const InputDecoration(
labelText: 'Name',
),
),
const SizedBox(height: 13),
TextFormField(
focusNode: _ageField.focusNode,
onEditingComplete: _nextEmpty,
initialValue: _formData.age,
onSaved: _ageField.onChanged,
onChanged: _ageField.onChanged,
validator: _ageField.validator,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
decoration: const InputDecoration(
labelText: 'Age',
),
),
const SizedBox(height: 13),
ValueListenableBuilder<bool>(
valueListenable: _allFilled,
builder: (context, value, _) => RaisedButton(
focusNode: _buttonFocusNode,
onPressed: value ? () => _submit(context) : null,
child: const Text('SUBMIT FORM'),
),
),
],
),
),
),
);
void _nextEmpty() {
final fieldNode = _fields.firstWhere((field) => !field.filled, orElse: () => null)?.focusNode;
if (fieldNode == null) {
_unfocusAll();
//_buttonFocusNode.requestFocus();
} else {
fieldNode.requestFocus();
}
}
void _unfocusAll() =>
_fields.map<FocusNode>((field) => field.focusNode).forEach((node) => node.unfocus());
void _setValidationStatus() => _allFilled.value = _fields.every((field) => field.filled);
void _submit(BuildContext context) {
_unfocusAll();
final form = Form.of(context)..save();
if (!form.validate()) {
_nextEmpty();
return;
}
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('SUBMIT: $_formData'),
),
);
}
}
/// Данные формы
class FormData {
String name;
String age;
FormData({this.name = '', this.age = ''});
@override
String toString() => '{"name": "$name", "age": "$age"}';
}
/// Класс в который инкапсулировано изменение и верификация поля формы
@immutable
class FieldStatus<FieldType, FormDataType> {
/// Данные общие для формы
final FormDataType _formData;
/// Заполнено ли поле
bool get filled => !hasValidator || validator(value(_formData)) == null;
/// Получение текущего значения поля
final FieldType Function(FormDataType data) value;
/// Нода для перехода к полям (например незаполненным при [onEditingComplete]
final FocusNode focusNode;
/// При изменении поля
ValueChanged<FieldType> get onChanged => (value) => _onChanged(_formData, value);
final void Function(FormDataType data, FieldType value) _onChanged;
/// Проверка поля
/// validator может быть опциональный (для необязательных полей его не указывать)
FormFieldValidator<FieldType> get validator => (value) => hasValidator ? _validator(_formData, value) : null;
final String Function(FormDataType data, FieldType value) _validator;
/// Обладает ли валидатором
final bool hasValidator;
FieldStatus({
@required FormDataType formData,
@required this.value,
@required void Function(FormDataType data, FieldType value) onChanged,
String Function(FormDataType data, FieldType value) validator,
FocusNode focusNode,
}) : _formData = formData,
focusNode = focusNode ?? FocusNode(),
_onChanged = onChanged,
_validator = validator,
hasValidator = validator != null;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment