feat(form): add list option validator

This commit is contained in:
Hugo Pointcheval 2022-05-02 14:41:24 +02:00
parent 5ea5c510cb
commit fe5fa692f7
8 changed files with 287 additions and 79 deletions

View File

@ -14,50 +14,48 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:form_bloc_example/constants.dart';
import 'package:form_bloc_example/cubit/custom_form_cubit.dart';
import 'package:form_bloc_example/sign_up/sign_up_page.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
static FormData getNormalFormData() {
return const FormData([
static List<FormEntry> getNormalEntries() {
return const [
FormEntry(formFieldName, Name.pure()),
FormEntry(formFieldEmail, Email.pure()),
FormEntry(formFieldPhone, Phone.pure()),
FormEntry(
formFieldList, ListOption<String>.pure(defaultValue: ['checkbox3'])),
FormEntry(formFieldRadio, TextString.pure()),
FormEntry(formFieldPro, Boolean.pure(), name: 'business'),
FormEntry(formFieldHidden, Boolean.pure(), export: false),
]);
];
}
static List<FormEntry> getBusinessEntries() {
const entries = [
FormEntry(formFieldSiren, Siren.pure()),
FormEntry(formFieldIban, Iban.pure()),
];
return getNormalEntries() + entries;
}
static FormData getNormalFormData() {
return FormData(getNormalEntries());
}
static FormData getProFormData() {
return const FormData([
FormEntry(formFieldName, Name.pure()),
FormEntry(formFieldEmail, Email.pure()),
FormEntry(formFieldPhone, Phone.pure()),
FormEntry(formFieldPro, Boolean.pure(), name: 'business'),
FormEntry(formFieldHidden, Boolean.pure(), export: false),
FormEntry(formFieldSiren, Siren.pure()),
FormEntry(formFieldIban, Iban.pure()),
]);
}
Future<bool> onSubmit(FormDataState state) async {
log(state.data.toMap().toString());
return true;
return FormData(getBusinessEntries());
}
@override
Widget build(BuildContext context) {
FormDataCubit _formCubit = FormDataCubit(
entries: getNormalFormData(),
onSubmit: onSubmit,
);
FormDataCubit _formCubit = CustomFormCubit(entries: getNormalFormData());
return BlocProvider(
create: (context) => _formCubit,

View File

@ -19,6 +19,8 @@ const String formFieldPhone = 'phone';
const String formFieldEmail = 'email';
const String formFieldSiren = 'siren';
const String formFieldIban = 'iban';
const String formFieldList = 'list';
const String formFieldRadio = 'radio';
const String formFieldHidden = 'hidden';
const String formFieldPro = 'isPro';

View File

@ -0,0 +1,33 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:developer';
import 'package:meta/meta.dart';
import 'package:wyatt_form_bloc/wyatt_form_bloc.dart';
part 'custom_form_state.dart';
class CustomFormCubit extends FormDataCubit {
CustomFormCubit({required FormData entries}) : super(entries: entries);
@override
Future<void> submitForm() {
log(state.data.toMap().toString());
return Future.value();
}
}

View File

@ -0,0 +1,22 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
part of 'custom_form_cubit.dart';
@immutable
abstract class CustomFormState {}
class CustomFormInitial extends CustomFormState {}

View File

@ -135,6 +135,112 @@ class _IbanInput extends StatelessWidget {
}
}
class _CheckListInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<FormDataCubit, FormDataState>(
builder: (context, state) {
final _input =
state.data.input<List<String>>(formFieldList) as ListOption<String>;
final _options = _input.value;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('Checkbox1'),
trailing: Checkbox(
value: _options.contains('checkbox1'),
onChanged: (_) {
context.read<FormDataCubit>().dataChanged(
formFieldList,
_input.select('checkbox1'),
);
}),
),
ListTile(
title: const Text('Checkbox2'),
trailing: Checkbox(
value: _options.contains('checkbox2'),
onChanged: (_) {
context.read<FormDataCubit>().dataChanged(
formFieldList,
_input.select('checkbox2'),
);
}),
),
ListTile(
title: const Text('Checkbox3 (default)'),
trailing: Checkbox(
value: _options.contains('checkbox3'),
onChanged: (_) {
context.read<FormDataCubit>().dataChanged(
formFieldList,
_input.select('checkbox3'),
);
}),
),
],
);
},
);
}
}
class _RadioListInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<FormDataCubit, FormDataState>(
builder: (context, state) {
final _input =
state.data.input<String>(formFieldRadio) as TextString;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: const Text('Radio1'),
trailing: Radio<bool>(
groupValue: true,
value: _input.value == 'radio1',
onChanged: (_) {
context.read<FormDataCubit>().dataChanged(
formFieldRadio,
const TextString.dirty('radio1'),
);
}),
),
ListTile(
title: const Text('Radio2'),
trailing: Radio<bool>(
groupValue: true,
value: _input.value == 'radio2',
onChanged: (_) {
context.read<FormDataCubit>().dataChanged(
formFieldRadio,
const TextString.dirty('radio2'),
);
}),
),
ListTile(
title: const Text('Radio3'),
trailing: Radio<bool>(
groupValue: true,
value: _input.value == 'radio3',
onChanged: (_) {
context.read<FormDataCubit>().dataChanged(
formFieldRadio,
const TextString.dirty('radio3'),
);
}),
),
],
);
},
);
}
}
class _CheckHiddenInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
@ -201,8 +307,7 @@ class _SignUpButton extends StatelessWidget {
? const CircularProgressIndicator()
: ElevatedButton(
onPressed: state.status.isValidated
? () =>
context.read<FormDataCubit>().submitForm()
? () => context.read<FormDataCubit>().submitForm()
: null,
child: const Text('SIGN UP'),
);
@ -244,42 +349,43 @@ class SignUpForm extends StatelessWidget {
);
}
},
child: SizedBox(
height: MediaQuery.of(context).size.height,
child: Align(
alignment: const Alignment(0, -1 / 3),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_NameInput(),
const SizedBox(height: 8),
_EmailInput(),
const SizedBox(height: 8),
_PhoneInput(),
const SizedBox(height: 8),
_CheckHiddenInput(),
const SizedBox(height: 8),
_CheckIsProInput(),
const SizedBox(height: 8),
BlocBuilder<FormDataCubit, FormDataState>(
builder: (context, state) {
if (state.data.input<bool>(formFieldPro).value) {
return Column(children: [
_SirenInput(),
const SizedBox(height: 8),
_IbanInput(),
const SizedBox(height: 8),
]);
}
return const SizedBox.shrink();
},
),
const SizedBox(height: 8),
_SignUpButton(),
const SizedBox(height: 8),
_DebugButton(),
],
),
child: Align(
alignment: const Alignment(0, -1 / 3),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_NameInput(),
const SizedBox(height: 8),
_EmailInput(),
const SizedBox(height: 8),
_PhoneInput(),
const SizedBox(height: 8),
_CheckListInput(),
const SizedBox(height: 8),
_RadioListInput(),
const SizedBox(height: 8),
_CheckHiddenInput(),
const SizedBox(height: 8),
_CheckIsProInput(),
const SizedBox(height: 8),
BlocBuilder<FormDataCubit, FormDataState>(
builder: (context, state) {
if (state.data.input<bool>(formFieldPro).value) {
return Column(children: [
_SirenInput(),
const SizedBox(height: 8),
_IbanInput(),
const SizedBox(height: 8),
]);
}
return const SizedBox.shrink();
},
),
const SizedBox(height: 8),
_SignUpButton(),
const SizedBox(height: 8),
_DebugButton(),
],
),
),
);

View File

@ -23,16 +23,16 @@ import 'package:wyatt_form_bloc/src/form/form.dart';
part 'form_data_state.dart';
class FormDataCubit extends Cubit<FormDataState> {
final Future<bool> Function(FormDataState state)? _onSubmit;
abstract class FormDataCubit extends Cubit<FormDataState> {
FormDataCubit({required FormData entries})
: super(FormDataState(data: entries));
FormDataCubit({
required FormData entries,
Future<bool> Function(FormDataState state)? onSubmit,
}) : _onSubmit = onSubmit,
super(FormDataState(data: entries));
void dataChanged<T>(String field, FormInput dirtyValue) {
/// Change value of a field.
///
/// Inputs:
/// - `field`: The key of the field to change.
/// - `dirtyValue`: The new value of the field.
void dataChanged(String field, FormInput dirtyValue) {
final _form = state.data.clone();
if (_form.contains(field)) {
@ -49,6 +49,11 @@ class FormDataCubit extends Cubit<FormDataState> {
);
}
/// Update entries list.
///
/// Inputs:
/// - `data`: The new entries list.
/// - `operation`: The operation to perform on the entries set.
void updateFormData(
FormData data, {
SetOperation operation = SetOperation.replace,
@ -78,13 +83,6 @@ class FormDataCubit extends Cubit<FormDataState> {
);
}
Future<void> submitForm() async {
unawaited(
_onSubmit?.call(state).then((bool reemit) {
if (reemit) {
emit(state);
}
}),
);
}
/// Submit the form.
Future<void> submitForm();
}

View File

@ -0,0 +1,48 @@
// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'package:wyatt_form_bloc/src/enums/enums.dart';
import 'package:wyatt_form_bloc/src/form/form.dart';
/// {@template list_option}
/// Form input for a list input
/// {@endtemplate}
class ListOption<T> extends FormInput<List<T>, ValidationError> {
/// {@macro list_option}
const ListOption.pure({List<T>? defaultValue})
: super.pure(defaultValue ?? const []);
/// {@macro list_option}
const ListOption.dirty({List<T>? value}) : super.dirty(value ?? const []);
ListOption select(T? v) {
if (v == null) {
return this;
}
if (value.contains(v)) {
final List<T> newValue = List.from(value)..remove(v);
return ListOption<T>.dirty(value: newValue);
} else {
final List<T> newValue = List.from(value)..add(v);
return ListOption<T>.dirty(value: newValue);
}
}
@override
ValidationError? validator(List<T>? value) {
return value?.isNotEmpty == true ? null : ValidationError.invalid;
}
}

View File

@ -18,6 +18,7 @@ export 'boolean.dart';
export 'confirmed_password.dart';
export 'email.dart';
export 'iban.dart';
export 'list_option.dart';
export 'name.dart';
export 'password.dart';
export 'phone.dart';