-
-
Save sylvesterasiedu/98b3fd1b0d79fae33137d5042b68e006 to your computer and use it in GitHub Desktop.
TextField concept for GetX (WIP) ...
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// 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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// 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, | |
), | |
); | |
}, | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// 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"); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// 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), | |
], | |
), | |
), | |
), | |
) | |
], | |
), | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// 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