-
Notifications
You must be signed in to change notification settings - Fork 56
injected_text_editing_api
TextEditingController
is yet another controller that has a dedicated Injected state that will make it easier for us:
- Create and automatically dispose of a
TextEditingController
, - Associate the
TextEditingController
with aTextField
orTextFormField
, - Change the value of the text and cache it,
- Easily work with a collection of TextFields (Form).
- Manage
FocusNote
, to dynamically change the focus to the next field to edit.
To deal with form input fields other than TextField use InjectedFormField
-
Creation of the
InjectedTextEditing
state - Working with forms
- TextField validation and NodeFocus
- Add isReadOnly and isEnable to
InjectedTextEditing
, andOnFormBuilder
Let's take the case of two TextField
s: one for the email and the other for the password.
final email = RM.injectTextEditing():
final password = RM.injectTextEditing(
text: '',
selection: TextSelection.collapsed(offset: -1),
composing: TextRange.empty,
validators: [
(String? value) {
if (value!.length < 6) {
return "Password must have at least 6 characters";
}
return null;
},
],
validateOnTyping: true,
validateOnLoseFocus: false,
isReadOnly: false,
isEnabled: true,
autoDispose: true,
onTextEditing :(password){
//fired when ever input text or selection changes
}
);
-
text
,selection
andcomposing
used as in Flutter. -
The
validators
is a list of validation rules the field should pass.validators
exposes the text that the user entered. If any of the validation callbacks return a non empty strings, the filed is considered non valid. For the field to be valid all validators must return null. example:final _email = RM.injectTextEditing( validators: [ (value) { //Frontend validation if (!Validators.isValidEmail(value)) { return 'Enter a valid email'; } }, ] );
The validations performed here are frontend validation. To do backend validation you must use
InjectedForm
. -
validateOnTyping
: Whether to validate the input while the user is typing. The default value depends on whether the linked [TextField] is inside or outside [OnFormBuilder]:- If outside: it default to true if
validateOnLoseFocus
is false. - If inside: it defaults to false if [InjectedForm.autovalidateMode] is [AutovalidateMode.disabled], otherwise it defaults to true.
If
validateOnTyping
is set to false, the text is not validate on typing. The text can be validate manually by invoking [InjectedTextEditing.validate]. - If outside: it default to true if
-
validateOnLoseFocus
Whether to validate the input just after the user finishes typing and the field loses focus. It defaults to true if the linked [TextField] is inside [OnFormBuilder] and defaults to false if it is outside. Once the [TextField] loses focus and if it fails to validate, the field will auto validate on typing the next time the user starts typing.For
validateOnLoseFocus
to work you have to set the [TextField]'s [FocusNode]to use [InjectedTextEditing.focusNode] Example:final email = RM.injectTextEditing(); // In the widget tree TextField( controller: email.controller, focusNode: email.focusNode, //It is auto disposed of. ),
-
The
InjectedTextEditing
is automatically deleted when it is no longer used. If you want to keep the data and use it in anotherTextField
, setautoDispose
to false. -
isReadOnly
: If true the [TextField] is clickable and selectable but not editable. Later on, you can set it using [InjectedTextEditing.isReadOnly].All input fields are set to be read only if they are inside a [OnFormBuilder] and the form is waiting for submission to resolve.
-
isEnabled
: If false the [TextField] is disabled. Later on, you can set it using [InjectedTextEditing.isEnable]. You can enable or disable all input fields inside [OnFormBuilder] using [InjectedForm.isEnabled] setter.For
isEnabled
to work you have to set the [TextField]'s enable property to use [InjectedTextEditing.isEnabled]Example:
final email = RM.injectTextEditing(): // In the widget tree TextField( controller: email.controller, enabled: email.isEnabled, ),
//Basically, define only the controller
//No need to define onChange nor onSave callbacks
TextField(
controller: email.controller,
),
//Or for full features
TextField(
controller: email.controller,
focusNode: email.focusNode, //It is auto disposed of.
decoration: InputDecoration(
errorText: email.error, //To display the error message.
),
onSubmitted: (_) {
//Focus on the password TextField after submission
password.focusNode.requestFocus();
},
),
_submit() {
if (email.isValid && password.isValid) {
print('Authenticate with ${email.text} and ${password.text}');
}
}
- You can reset any
TextField
to its initial text using:
email.reset();
password.reset();
If the application you are working on contains dozens of TextFields, it becomes tedious to process each field individually. Form
helps us collect many TextFields and manage them as a unit.
final form = RM.injectForm(
//optional parameters
autovalidateMode: AutovalidateMode.disable,
autoFocusOnFirstError: true,
submit: () async {
//This is the default submission logic,
//It may be override when calling form.submit( () async { });
//It may contains server validation.
await serverError = authRepository.signInWithEmailAndPassword(
email: email.text,
password: password.text,
);
//after server validation
if(serverError == 'Invalid-Email'){
email.error = 'Invalid email';
}
if(serverError == 'Weak-Password'){
email.error = 'Password must have more the 6 characters';
}
},
submissionSideEffects: SideEffects(..), // for side effects
onSubmitting: () {
// called while waiting for form submission,
},
onSubmitted: () {
// called after form is successfully submitted
// For example navigation to user page
}
);
-
InjectedForm
has one optional parameter. “autovalidateMode” is of type “AutovalidateMode”. As in Flutter, It can take one of three enumeration values: -AutovalidateMode.disable
: The form is validated manually by callingform.validate()
-AutovalidateMode.always
: The form is always validated -AutovalidateMode.onUserInteraction
: The form is not validated until the user has started typing. -
autoFocusOnFirstError
if set to true (default), after form validation the first non valid field is get focus. -
submit
contains the user logic for form submission. It will be invoked whe theform.submit()
si called without argument. (See from submission below) - For side effects you have
onSubmitting
andonSubmitted
.
In the user interface, we put the TextField
s that we want to associate with the form inside the OnFormBuilder
widget:
OnFormBuilder(
listenTo: form,
builder: () => Column(
children: <Widget>[
TextField(
focusNode: email.focusNode,
controller: email.controller,
decoration: InputDecoration(
errorText: email.error,
),
onSubmitted: (_) {
//request the password node
password.focusNode.requestFocus();
},
),
TextField(
focusNode: password.focusNode,
controller: password.controller,
decoration: new InputDecoration(
errorText: password.error,
),
onSubmitted: (_) {
//request the submit button node
form.submitFocusNode.requestFocus();
},
),
OnFormSubmissionBuilder(
listenTo: form
onSubmitting: () => CircularProgressIndicator(),
child : ElevatedButton(
focusNode: form.submitFocusNode,
onPressed: (){
form.submit();
},
child: Text('Submit'),
),
),
],
),
),
- We only use
TextField
widgets, no need to useTextFormField
- To validate all fields, use:
form.validate()
- To reset all fields to their initial values, use:
form.reset()
- To check that all fields are valid, use
form.isValid
- Each
InjectedTextEditing
has an associatedFocusNode
. - The
InjectedForm
is associated with a FocusNote to be used in the submit button. - All
TextEditingControllers
andFocusNotes
are automatically disposed of when they are no longer in use. -
OnFormSubmissionBuilder
widget is used to listen to form submission. (See below) -
OnSubmissionBuilder
widget is used to listen to form submission. (See below)
To submit a form, you can use the submit
method :
//If called without argument, it will use the default submit callback defined while initializing the InjectForm
form.submit();
//Some time is is more convenient to define the submission logic inside the widget tree.
form.submit(() async {
//submission logic,
//It may contains server validation.
authRepository.signInWithEmailAndPassword(
email: email.text,
password: password.text,
);
//after server response
if(serverError == 'Invalid-Email'){
email.error = 'Invalid email';
}
if(serverError == 'Weak-Password'){
email.error = 'Password must have more the 6 characters';
}
});
Here is an example take form This auth example
In the user repository,We sing in using FireBaseAuth
:
Future<User?> _signInWithEmailAndPassword(
String email,
String password,
) async {
try {
final firebase.UserCredential authResult =
await _firebaseAuth.signInWithEmailAndPassword(
email: email,
password: password,
);
return _fromFireBaseUserToUser(authResult.user);
} catch (e) {
if (e is firebase.FirebaseAuthException) {
switch (e.code) {
case 'invalid-email':
throw EmailException('Email address is not valid');
case 'user-disabled':
throw EmailException(
'User corresponding to the given email has been disabled');
case 'user-not-found':
throw EmailException(
'There is no user corresponding to the given email');
case 'wrong-password':
throw PasswordException('Password is invalid for the given email');
default:
throw SignInException(
title: 'Create use with email and password',
code: e.code,
message: e.message,
);
}
}
rethrow;
}
}
In the UI :
final _email = RM.injectTextEditing(
validators: [(String? val) {
//Frontend validation
if (!Validators.isValidEmail(val!)) {
return 'Enter a valid email';
}
}],
);
final _password = RM.injectTextEditing(
validators: [
(String? val) {
if (!Validators.isValidPassword(val!)) {
return 'Enter a valid password';
}
}],
validateOnTyping: true,
);
final _confirmationPassword = RM.injectTextEditing(
validators: [
(String? val) {
if (_password.text != val) {
return 'Passwords do not match';
}
}],
validateOnTyping: true,
);
final _form = RM.injectForm();
class SignInRegisterFormPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(10),
child: FormWidget(),
),
);
}
}
class FormWidget extends StatelessWidget {
final _isRegister = false.inj();
@override
Widget build(BuildContext context) {
return OnFormBuilder(
listenTo: form,
builder: () => Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextField(
controller: _email.controller,
focusNode: _email.focusNode,
decoration: InputDecoration(
icon: Icon(Icons.email),
labelText: 'Email',
errorText: _email.error,
),
keyboardType: TextInputType.emailAddress,
autocorrect: false,
onSubmitted: (_) {
_password.focusNode.requestFocus();
},
),
TextField(
controller: _password.controller,
focusNode: _password.focusNode,
decoration: InputDecoration(
icon: Icon(Icons.lock),
labelText: 'Password',
errorText: _password.error,
),
obscureText: true,
autocorrect: false,
onSubmitted: (_) {
if (_isRegister.state) {
_confirmationPassword.focusNode.requestFocus();
} else {
_form.submitFocusNode.requestFocus();
}
},
),
OnReactive(
() => Column(
children: [
_isRegister.state
? TextField(
controller: _confirmationPassword.controller,
focusNode: _confirmationPassword.focusNode,
decoration: InputDecoration(
icon: Icon(Icons.lock),
labelText: 'Confirm Password',
errorText: _confirmationPassword.error,
),
obscureText: true,
autocorrect: false,
onSubmitted: (_) {
_form.submitFocusNode.requestFocus();
},
)
: Container(),
const SizedBox(height: 10),
Row(
children: <Widget>[
Checkbox(
value: _isRegister.state,
onChanged: (value) {
_isRegister.state = value!;
},
),
Text(' I do not have an account')
],
),
OnFormSubmissionBuilder(
listenTo: form,
onSubmitting: () =>
Center(child: CircularProgressIndicator()),
child: ElevatedButton(
focusNode: _form.submitFocusNode,
child: _isRegister.state
? Text('Register')
: Text('Sign in'),
onPressed: () {
_form.submit(
() async {
if (_isRegister.state) {
await user.auth.signUp(
(_) => UserParam(
signUp: SignUp.withEmailAndPassword,
email: _email.state,
password: _password.state,
),
);
} else {
await user.auth.signIn(
(_) => UserParam(
signIn: SignIn.withEmailAndPassword,
email: _email.state,
password: _password.state,
),
);
//Server validation
if (user.error is EmailException) {
_email.error = user.error.message;
}
if (user.error is PasswordException) {
_password.error = user.error.message;
}
}
},
);
},
),
),
],
),
],
),
);
}
}
If a TextField is put inside OnFormBuilder
the validation logic defaults to the following:
- By default form validation mode is
AutovalidateMode.disable
. - The
validateOnLoseFocus
is set to true andvalidateOnTyping
is set to false. That is, the input text will not be validated until the field first loses focus. - After a field loses focus, and if the input text is not valid, the
validateOnTyping
is set to true so that the field is validated on the fly. - When the form is validated by calling f
orm.validate
orform.submit
, and if validation fails, the first non-valid field will get focus.
In some scenarios, we need to disable an input field or just make it read only.
Now InjectedTextEditing
and InjectedFormField
have:
-
isEnabled
: If set to false, it will make the field non-selectable; non-focusable, non-editable. -
isReadOnly
: If set to true, the field is selectable, focusable but not editable.
Later on, you can change value of the isEnabled
, isReadOnly
properties:
final myText = RM.injectedTextEditing(
isEnabled: false,
isReadOnly: true,
);
final myCheckBox = RM.injectedFormField(
isEnabled: false,
isReadOnly: true,
);
// In the widget tree
Column(
children: [
// For TextFields you have to explicitly define enabled and readOnly parameters.
TextField(
controller: myText.controller,
enabled: myText.isEnabled,
readOnly: myText.isReadOnly,
),
//In contrast in OnFormFieldBuilder enabled and readOnly are implicitly assigned.
OnFormFieldBuilder<bool>(
listenTo: myCheckBox,
builder: (value, onChanged){
return CheckBoxListTile(
value: value,
onChanged: onChanged,
title: Text('Accept me'),
);
}
)
]
)
// To toggle isEnabled and isReadOnly:
myText.isEnabled = true;
myText.isReadOnly = false;
myCheckBox.isEnabled = true;
myCheckBox.isReadOnly = false;
In case we have many input fields we can enable or disable each field individually but this my be very tedious. InjectedForm
and OnFormBuilder
are here to help us.
Let's imagine a case where we want to disable all input fields (or just make them read only) while the form is submitting.
final isEnabledRM = true.inj();
final formRM = RM.injectForm(
submissionSideEffects: SideEffects.onOrElse(
onWaiting: ()=> isEnabledRM = false,
orElse: (_)=> isEnabledRM = true,
submit: () => repository.submitForm( ... ),
),
);
// In the widget tree
OnFormBuilder(
listenTo: formRM,
// Adding this all child input's enabled and readOnly properties are controlled.
isEnabledRM: isEnabledRM,
// // Similar if you want to make it readOnly
//isReadOnlyRM: isReadOnlyRM,
builder: () => Column(
children: [
TextField(
controller: myText.controller,
enabled: myText.isEnabled,
readOnly: myText.isReadOnly,
),
OnFormFieldBuilder<bool>(
listenTo: myCheckBox,
builder: (value, onChanged){
return CheckBoxListTile(
value: value,
onChanged: onChanged,
title: Text('Accept me'),
);
}
)
]
),
)
As you can use many nested OnFormFieldBuilder
for the same formRM
you can disable (or make readOnly) only a part of the field inputs.For example, sometimes there is a checkbox as an agreement to continue to the next section, before that all/some fields on the next section are disabled.