Skip to content

Instantly share code, notes, and snippets.

@sylvesterasiedu
Forked from roipeker/form_utils.dart
Created December 7, 2020 18:34
Show Gist options
  • Save sylvesterasiedu/98b3fd1b0d79fae33137d5042b68e006 to your computer and use it in GitHub Desktop.
Save sylvesterasiedu/98b3fd1b0d79fae33137d5042b68e006 to your computer and use it in GitHub Desktop.
TextField concept for GetX (WIP) ...
/// copyright 2020, roipeker
class FormValidations {
static String email(String val) {
val = val.trim();
if (val.isEmpty) return 'El email no puede estar vacío';
if (val.length <= 4) return 'El email es muy corto';
if (!val.isEmail) return 'El email no es válido';
return null;
}
static String matchPasswords(String val1, String val2) {
if (val1.isEmpty || val2.isEmpty)
return 'La contraseña no puede estar vacía';
if (val1 != val2) return 'Las contraseñas no coinciden';
return null;
}
static String password(String val) {
if (val.isEmpty) return 'La contraseña no puede estar vacía';
if (val.length <= 4) return 'La contraseña es muy corta';
if (val.isAlphabetOnly) return 'La contraseña debe ser alfanumérica';
return null;
}
static String name(String val, {String label}) {
val = val.trim();
if (val.isEmpty) return 'El $label no puede estar vacío';
if (val.length <= 2) return 'El $label es muy corto';
return null;
}
static String phone(String val) {
val = val.trim();
val = val.replaceAll(' ', '');
if (val.isEmpty) return 'El teléfono es requerido';
if (val.length <= 7) return 'El teléfono parece ser muy corto';
if (!val.isNumericOnly) return 'Usá solo números';
return null;
}
}
/// copyright 2020, roipeker
//import 'package:descontar_app/const/const.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
enum ValidationPlace {
/// calls [validate()] when you focus out the TextField.
focus,
/// calls [validate()] as you type the text.
change,
/// in [manual] you take care of calling of the errors on each TextField.
/// you can use [TextConfig.validateAll([])] to run the [validate()] on each
/// TextField, or [TextConfig.getErrors([])] to get run [onValidate()] and
/// get a List<String> of errors (and no TextField UI invalidation).
manual,
}
enum ErrorMode { none, fixed, float }
class GetInputTextConfig extends GetxController {
final InputBorder border;
final InputBorder focusedBorder;
final InputBorder enabledBorder;
final InputBorder errorBorder;
final InputBorder disabledBorder;
final InputBorder focusedErrorBorder;
final ValidationPlace validationPlace;
final TextInputType keyboardType;
final TextInputAction textInputAction;
final TextCapitalization textCapitalization;
TextStyle style;
TextStyle disabledStyle;
final bool showCounter;
final String label;
final IconData icon;
final bool isPassword;
final ErrorMode errorMode;
final bool autocorrect;
// another instance will control this one.
GetInputTextConfig _obscureController;
GetInputTextConfig get obscureController => _obscureController;
set obscureController(GetInputTextConfig value) {
_obscureController = value;
_obscureController?._childObscureToControl = this;
}
// this is the child instance to control by the obscureController.
GetInputTextConfig _childObscureToControl;
final Iterable<String> autofillHints;
final List<TextInputFormatter>
inputFormatters; //FilteringTextInputFormatter.digitsOnly,
// decoration stuffs.
final FloatingLabelBehavior floatingLabelBehavior;
/// should be in an InputDecoration object
final bool isCollapsed;
int maxLength;
bool _obscureText = false;
FocusNode _focus;
TextEditingController _controller;
FormFieldValidator<String> onValidate;
bool clearErrorOnFocus = true;
bool clearErrorOnTyping = true;
String lastError;
bool _enabled;
bool get enabled => _enabled;
TextStyle get _actualStyle {
if (_enabled ?? true) return style;
return disabledStyle;
}
set enabled(bool val) {
if (_enabled == val) return;
_enabled = val;
update();
}
static List<String> getErrors(List<GetInputTextConfig> inputs) {
final output = <String>[];
for (var i in inputs) {
final error = i.onValidate(i.value);
if (!error.isNullOrBlank) output.add(error);
}
return output;
}
/// Runs [validate()] on each element in [inputs].
/// [stopOnError] will return false on the first element with an error.
/// otherwise it will validate() the entire [inputs] List.
static bool validateAll(List<GetInputTextConfig> inputs, {bool stopOnError = true}) {
bool hasError = false;
for (var i in inputs) {
if (!i.validate()) {
hasError = true;
if (stopOnError) break;
}
}
return !hasError;
}
GetInputTextConfig({
this.onValidate,
this.validationPlace = ValidationPlace.manual,
this.errorMode = ErrorMode.fixed,
bool enabled,
this.showCounter = false,
this.isCollapsed = false,
this.floatingLabelBehavior = FloatingLabelBehavior.auto,
this.keyboardType,
this.textInputAction,
this.textCapitalization,
this.maxLength,
GetInputTextConfig obscureController,
this.autocorrect = true,
this.inputFormatters,
this.autofillHints,
this.style,
// this.disabledStyle = const TextStyle(color: Styles.darkGrey),
this.label,
this.icon,
this.border = const UnderlineInputBorder(),
this.focusedBorder,
this.enabledBorder,
this.errorBorder,
this.disabledBorder,
this.focusedErrorBorder,
this.isPassword = false,
}) {
_obscureText = isPassword;
obscureController?._childObscureToControl = this;
this.enabled = enabled;
}
FocusNode get focus => _focus ??= FocusNode();
TextEditingController get controller =>
_controller ??= TextEditingController();
String get value => controller.text;
set value(String val) {
val ??= '';
if (val == controller.text) return;
controller.value = controller.value.copyWith(text: val);
}
bool get obscureText => _obscureText;
set obscureText(bool flag) {
if (_obscureText == flag) return;
_obscureText = flag;
update();
}
@override
void onInit() {
focus.addListener(_handleFocus);
controller.addListener(_handleTextChange);
}
bool get hasFocus => focus.hasFocus;
String _value;
void _handleTextChange() {
var val = controller.text;
if (val == _value) return;
_value = val;
if (onChanged != null) onChanged(_value);
if (validationPlace == ValidationPlace.change) {
validate();
} else {
if (clearErrorOnTyping) error = '';
}
}
void _handleFocus() {
if (onFocus != null) onFocus(hasFocus);
if (!hasFocus) {
if (validationPlace == ValidationPlace.focus) {
validate();
}
} else {
if (hasError && clearErrorOnFocus) {
error = '';
}
}
}
bool validate() {
if (onValidate != null) {
error = onValidate(value);
lastError = error;
}
return !hasError;
}
bool get hasError {
// return !_error.isNullOrBlank;
return !lastError.isNullOrBlank;
// return _actualErrorText != null;
}
/// todo: give ability to access last error.../
/// make a private var, and do error public and holding the last
/// message.
String get error => _error;
set error(String val) {
if (_error == val) return;
_error = val;
if (onErrorChange != null)
onErrorChange(_error.isNullOrBlank ? null : _error);
update();
}
Widget get _counterWidget {
if (errorMode == ErrorMode.fixed) return null;
return (showCounter ?? false) ? null : Container();
}
String get _counterText {
if (errorMode == ErrorMode.fixed) return ' ';
return null; // ErrorMode.float.
}
// used by widget
String get _actualErrorText {
if (errorMode == ErrorMode.none) return null;
return _error.isNullOrBlank ? null : _error;
}
String _error;
Function(String) onChanged;
Function(bool) onFocus;
Function(String) onErrorChange;
@override
void onClose() {
controller?.removeListener(_handleTextChange);
controller?.dispose();
focus?.removeListener(_handleFocus);
focus?.dispose();
}
Widget getSuffix() {
if (!isPassword) return null;
if (obscureController != null) return null;
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
obscureText = !obscureText;
_childObscureToControl?.obscureText = obscureText;
},
child: Icon(
obscureText ? Icons.visibility : Icons.visibility_off,
// color: Styles.lightGrey,
size: 18,
).paddingSymmetric(horizontal: 10, vertical: 6),
);
}
}
class GetInputText extends StatelessWidget {
final GetInputTextConfig config;
const GetInputText({Key key, this.config}) : super(key: key);
@override
Widget build(BuildContext context) {
return GetBuilder(
init: config,
global: false,
assignId: true,
builder: (_) {
return TextField(
controller: config.controller,
focusNode: config.focus,
style: config._actualStyle,
obscureText: config.obscureText,
keyboardType: config.keyboardType,
maxLength: config.maxLength,
maxLengthEnforced: !config.maxLength.isNull,
autocorrect: config.autocorrect ?? true,
autofillHints: (config.enabled ?? true) ? config.autofillHints : null,
textInputAction: config.textInputAction,
inputFormatters: config.inputFormatters,
enabled: config.enabled,
textCapitalization:
config.textCapitalization ?? TextCapitalization.none,
//const InputDecoration()
decoration: InputDecoration(
labelText: config.label,
border: config.border,
focusedBorder: config.focusedBorder,
enabledBorder: config.enabledBorder,
errorBorder: config.errorBorder,
disabledBorder: config.disabledBorder,
focusedErrorBorder: config.focusedErrorBorder,
// errorBorder: UnderlineInputBorder(
// borderSide: BorderSide(color: Styles.lightGrey),
// ),
contentPadding: EdgeInsets.symmetric(vertical: 6),
alignLabelWithHint: true,
floatingLabelBehavior:
config.floatingLabelBehavior ?? FloatingLabelBehavior.auto,
errorText: config._actualErrorText,
errorMaxLines: 1,
counterText: config._counterText,
counter: config._counterWidget,
icon: config.icon != null
? Icon(config.icon, size: 20).paddingOnly(top: 10)
: null,
isCollapsed: config.isCollapsed ?? false,
suffixIconConstraints: BoxConstraints(maxWidth: 24, maxHeight: 24),
// suffixIcon: config.isPassword ? config.getSuffix() : null,
suffix: config.isPassword ? config.getSuffix() : null,
),
);
},
);
}
}
/// copyright 2020, roipeker
import 'package:get/get.dart';
import 'signup_fields.dart';
class SignupController extends GetxController {
final fields = SignupFields();
@override
void onReady() {
fields.phone.focus.requestFocus();
}
void onSend() {
if (fields.validate()) {
print("All valid, send form");
}
}
}
/// copyright 2020, roipeker
import 'package:descontar_app/const/const.dart';
import 'package:descontar_app/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'signup_controller.dart';
class SignupView extends GetView<SignupController> {
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
backgroundColor: Colors.transparent,
toolbarHeight: 40,
elevation: 0,
),
body: CommonBackground(
showBottom: false,
children: [
Center(
child: SingleChildScrollView(
child: SizedBox(
width: 250,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Image.asset(Images.logo, fit: BoxFit.contain),
GetInputText(config: controller.fields.name),
GetInputText(config: controller.fields.lastname),
GetInputText(config: controller.fields.email),
GetInputText(config: controller.fields.phone),
GetInputText(config: controller.fields.password),
GetInputText(config: controller.fields.repeatPassword),
const Gap(24),
LineButton(
label: 'REGISTRARSE',
onTap: controller.onSend,
),
Gap(40),
],
),
),
),
)
],
),
);
}
}
/// copyright 2020, roipeker
import 'package:descontar_app/const/const.dart';
import 'package:descontar_app/utils/form_utils.dart';
import 'package:descontar_app/utils/utils.dart';
import 'package:descontar_app/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class SignupFields {
GetInputTextConfig name = GetInputTextConfig(
label: AppStrings.name_label,
textCapitalization: TextCapitalization.words,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.name,
onValidate: (val) => FormValidations.name(val, label: 'nombre'),
maxLength: 15,
inputFormatters: [FormFormatters.filterName],
autofillHints: [
AutofillHints.name,
AutofillHints.creditCardName,
],
errorMode: ErrorMode.fixed,
// validationPlace: ValidationPlace.manual,
);
GetInputTextConfig lastname = GetInputTextConfig(
label: AppStrings.lastname_label,
onValidate: (val) => FormValidations.name(val, label: 'apellido'),
textCapitalization: TextCapitalization.words,
textInputAction: TextInputAction.next,
inputFormatters: [FormFormatters.filterName],
keyboardType: TextInputType.name,
maxLength: 26,
autofillHints: [
AutofillHints.familyName,
AutofillHints.givenName,
AutofillHints.creditCardFamilyName
],
errorMode: ErrorMode.fixed,
// validationPlace: ValidationPlace.manual,
);
GetInputTextConfig email = GetInputTextConfig(
label: AppStrings.email_label,
onValidate: (val) => FormValidations.email(val),
textCapitalization: TextCapitalization.none,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.emailAddress,
maxLength: 200,
autofillHints: [
AutofillHints.username,
AutofillHints.email,
],
// errorMode: ErrorMode.fixed,
// validationPlace: ValidationPlace.manual,
);
GetInputTextConfig phone = GetInputTextConfig(
label: AppStrings.phone_label,
onValidate: (val) => FormValidations.phone(val),
// inputFormatters: [FormFormatters.filterPhone],
textCapitalization: TextCapitalization.none,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.phone,
maxLength: 28,
autofillHints: [
AutofillHints.telephoneNumberDevice,
AutofillHints.telephoneNumber,
AutofillHints.telephoneNumberNational,
],
// errorMode: ErrorMode.fixed,
// validationPlace: ValidationPlace.manual,
);
GetInputTextConfig password = GetInputTextConfig(
onValidate: (val) => FormValidations.password(val),
label: AppStrings.password_label,
maxLength: 255,
isPassword: true,
textCapitalization: TextCapitalization.none,
textInputAction: TextInputAction.send,
keyboardType: TextInputType.visiblePassword,
autofillHints: [AutofillHints.newPassword],
// errorMode: ErrorMode.float,
// validationPlace: ValidationPlace.manual,
);
GetInputTextConfig repeatPassword = GetInputTextConfig(
onValidate: (val) => FormValidations.password(val),
label: AppStrings.repeat_password_label,
maxLength: 255,
isPassword: true,
// obscureController: password,
textCapitalization: TextCapitalization.none,
textInputAction: TextInputAction.send,
keyboardType: TextInputType.visiblePassword,
autofillHints: [AutofillHints.newPassword],
// errorMode: ErrorMode.fixed,
// validationPlace: ValidationPlace.manual,
);
List<GetInputTextConfig> allFields;
SignupFields() {
allFields = [name, lastname, email, phone, password, repeatPassword];
repeatPassword.obscureController = password;
}
bool validate() {
var error = '';
for (var i in allFields) {
if (!i.validate()) {
error = i.error;
break;
}
}
if (error.isNotEmpty) {
AppGetUtils.showError(error);
return false;
} else {
// validate passwords stand alone.
error = FormValidations.matchPasswords(
password.value, repeatPassword.value) ??
'';
if (error.isNotEmpty) {
repeatPassword.error = error;
AppGetUtils.showError(error);
return false;
}
}
return true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment