I'm going with MobX on my new project which involves a lot of Forms (hundreds) and Fields (10 to 50)
At core MobX solve a lot of concerns but it's not easy to copy the Form example at scale when you have a lot of fields.
There are two stores to handle, values and errors also you should not forget to init disposers.
There's an ObservableList that handle specific modifications and react accordingly.
I'd like to have an ObservableFormField that would handle Fields.
So much handwritten code is really error prone when working on real business forms entries
Thank you for your feedback !
I'm not sure how to architect all this but I'll be happy to find logic per field at one place only.
@observable
ObservableFormField<String> name = ObservableFormField<String>(
name:'firstname',
validator : ...
error : errorBuilder<String>(..) => ...
)
@action
setName(String value) => name.value = value
Hi Robert, you have raised a very valid concern and I am totally with you on the complexity of managing this. Your suggestion of ObservableFormField seems like a good approach. I think we should make a package called mobx_forms and have this be part of that?
The sample API could look like so. Note that the validation itself is outside of the scope of mobx_forms
import 'package:mobx/mobx.dart';
part 'observable_form_field.g.dart';
class ObservableFormField<T> = _ObservableFormField<T> with _$ObservableFormField;
class _ObservableFormField<T> with Store {
_ObservableFormField({this.name, this.label, this.value});
final String name;
final String title;
// Most important error
@computed
String get error => errors == null ? null : errors[0];
// All errors, if more than one
@observable
List<String> errors;
@observable
T value;
@computed
bool get isValid => errors == null;
}
This is just a start and we can iterate together to create this package ?
Hi Pavan,
A full featured but complementary mobx_forms that rely partly on validation lib should have a lot of success ! I can already see how to leverage the capacities of MobX to make complexe business rules more transparent.
I'll provide you with all the support I can to setup this package !
Awesome. Why don't we start with the use cases you are tackling ? Should I make a branch on this repo for mobx_forms and invite you for collaboration ?
Currently I'm working on a POC and creating the use cases for the developers to rely on.
Goals is to cover basic types fields, errors management, local & remote validations, sub-forms reactions, etc
This could be used as the main example for flutter_mobx_form
I'll join you on this new exciting branch.
Nice. Let me make and invite you
We might have to include some Observer components too, so I think I should call this flutter_mobx_forms ? It will have the MobX stores + Observer widgets
flutter_mobx_forms is the perfectly searchable pub name
error should probably be computed instead:
abstract class StringField with Store {
StringField(this._validator);
final String Function(String value) _validator;
@observable
String value;
@computed
String get error => _validator?.call(value);
@computed
bool get isValid => error == null;
}
Which allows:
StringFIeld((value) {
if (value.isEmpty) return "Missing field but required";
return null;
});
Agree @rrousselGit. I was also thinking there could be multiple errors for some fields
We will have to decide if we take a hard dependency on validator or make it a callback function that we invoke. That way we can plugin any custom validation logic
Indeed but validator could return an iterable instead:
StringField((value) sync* {
if (value.isEmpty) yield "Missing field but required";
});
assuming we change the StringField error to:
@computed
List<String> get error => _validator?.call(value)?.toList() ?? const [];
We will have to decide if we take a hard dependency on validator or make it a callback function that we invoke. That way we can plugin any custom validation logic
What do you mean by hard-dependency here?
I meant the validator package but this could just be a validator instance...if you were thinking along those lines
Oh btw: say hello to flutter_mobx_forms...the most underrated package as of now :-)
I've put in three of us as authors. Robert if you share your email, I'll update
I meant the validator package
I meant the validator package
Ya, looks defunct now? I prefer making validation just be a callback function that we invoke at specific times. Makes it more pluggable.
I use Validator currently but yes it's dead so I've copied it and updated it as local sources, more devs will hit the same limitations.
So I see two exits :
mobx form users will have add it to their dependenciesmobx form umbrellaI've a natural preference to have it under our umbrella so I just import one package that covers nearly all my needs, that reassure me it is alive and evolving with library.
We just provide what's already in the original lib and offer a basic way to define extended validators.
What's your feelings about this potential integration ?
// Thanks @pavanpodila for adding my name ! mail : felker D0T robert @ Gmail D0T com
Also I try to tinker with what I would use as an end-user :
@form()
class ConsumerForm ? { ...}
So that users can have access to generated methods like
final configuredForm = ConsumerForm(
name: "ConfigurableFormForConsumer", // Helper debug message like for Observer, [FormName-Fieldname] log,
uri: 'http://myserver.dev/form // Optional ?
) // Nothing to do we have the URI and normalised serialisation.
final configurableForm = ConsumerForm(
send: (fields) {
ChopperService.call some HTTP methods
}
)
In the widget
form.fieldsOnError.map<Widget>((field) => Text(
"${field.name} ${formatErrors(field)}"
, style|color|red);
RaisedButton(
enabled: form.valid // auto generated based on inner FormFields entries
onTap: form.send( ... options)
)
RaisedButton(
child: Text("Reset"),
onTap: form.reset()
)
I like the @form and possibly the @form_field annotation approach. We will have to build the core classes to support this and then simplify the API with annotations.
Let's start working on this branch: https://github.com/mobxjs/mobx.dart/pull/263
Also we can provide a basic set of validations as part of the package. If the validations grow beyond the scope of the package, we can move them out to its own top-level package later
Here are some ideas on top of the last commits:
What do you think of this form of errors declaration, I like that it's more easy to stack and I don't have to handle the array of errors. ErrorContext handle method wrapping a Set.
Also trying Error as Object instead of just a String so complementary infos about the error can be added.
Think about password security level or response where a String it not enough and more should be formatted for the user to understand.
Constraints Class with static fields for basic fields checks.
FormField one = FormField<String>(
name: 'Firstname',
label: 'Your Firstname',
value: 'Robbie',
validator: (value, errors) {
if(value==null) errors.message(Don't hide yourself");
if(Constraints.isNumeric(value))
errors.error(ConstraintError("Your not a number !!!", {'myDetail': 0}))
}
)
@observable
final errorsContext = ErrorContext();
...
@action
void _syncValidate() {
...
validator(value, errorsContext.reset());
...
// Maybe make this class Iterable for call on map in the Widget build()
// Observable too
class ErrorContext {
Set<ConstraintError> errors = {};
ErrorContext reset() {
errors.clear();
return this;
}
ConstraintError message(String message) => errors.add(ConstraintError(message));
ConstraintError error(ConstraintError error) => errors.add(error);
}
I like the ErrorContext approach. Makes the errors more structured. Will incorporate this.
Pushed new changes as per this discussion to the PR #263
There's already a ticket open but here's the manifestation in Form module :
Declaring this observable FieldState
fieldState.value will get you Dynamic not String because Built package remove the type in .g.dart
@observable
FieldState firstname = FieldState<String>(
...
firstname.value <= dynamic not String
Still working on the setup I got this under web deployment, working well under mobile.
I don't know how to put a breakpoint to find the cause.
Invalid argument: Maximum call stack size exceeded
Invalid argument: Maximum call stack size exceeded
User-created ancestor of the error-causing widget was:
TabBarView org-dartlang-app:///packages/hrpath_forms/forms/subscription/subscription.route.dart:34:17
When the exception was thrown, this was the stack:
package:mobx/src/core/context.dart 66:38 get config
package:mobx/src/core/context.dart 183:22 <fn>
package:mobx/src/core/context.dart 201:14 enforceWritePolicy
package:mobx/src/core/context.dart 210:7 conditionallyRunInAction
package:hrpath_forms/forms/mobx/field_state.g.dart 52:33 set [_isValidating]
Also I modified the current implementation with
@observable
bool _isValidating = false;
Previously it was set to null by default.
Ya, looks defunct now? I prefer making validation just be a callback function that we invoke at specific times. Makes it more pluggable.
https://pub.dev/documentation/validators/latest/validators/validators-library.html
This one is not dead so we open the discussion are we still integrating our own or are we working with this one ?
We can use that and get started. The only thing to note is that validation should be pluggable via a callback function interface. That way we can add/replace/chain more in the future.
I've created some wrapper around FieldState, something like
class TextFieldState extends AbstractFormFieldFieldState<String>
Idea is to add to the lib Widget ready to use and bind to different types of data.
Text, Number, Mail, Date, etc
TextFieldState(
state: civilState.address.street,
)
Also @aloisdeniel is also giving a hand on the project so we have something clean
How about a single TextFieldState with toStringBuilder and fromStringBuilder to encode/decode in string? Or am I missing something ?
Or maybe I'm missing a thing ? here's some code
class TextFieldState extends AbstractFormFieldFieldState<String> {
final bool autofocus;
final bool autovalidate;
final bool enabled;
final int maxLength;
const TextFieldState({
@required FieldState<String> state,
this.autofocus = false,
this.autovalidate = true,
this.enabled = true,
this.maxLength,
}) : super(state);
@override
Widget build(BuildContext context) => TextFormField(
autofocus: autofocus,
autovalidate: autovalidate,
enabled: enabled,
maxLength: maxLength,
decoration: InputDecoration(
labelText: state.label,
hintText: state.hint,
),
validator: (_) =>
state.isValid ? null : state.errorContext.errors.first.message,
initialValue: state.value,
onChanged: (v) {
state.value = v;
state.validate();
},
);
}
We configure nearly everything in the FieldState and pass it as a parameter to have our widget configured. It's a little rough but you get the idea how we wrap data from state.
Later maybe we'll add annotation to FieldState like
@max(10)
@min(5)
@observable
FieldState<String> street = FieldState<String>(label: 'Voie');
And those will configure the Widget.
Got it. It really looks cool, esp. the annotations for constraints! Would love to have this part of the project, so we all can benefit 馃
Annotations are not here for now but I use this Thread to push and validate ideas like this with the community.
Commit asap we have so stabilised basics features :)
Awesome! Declarative, annotation driven form development would make it so nice! If possible we can even hide the MobX machinery :-)
Can you help with this code please
The value is well calculated the first time but even is _calculateAge() is called on update,
the field is not displayed.
@computed
@observable
FieldState<String> get age =>
FieldState<String>(label: 'Age', value: _calculateAge());
String _calculateAge() {
if (birthDate.value == null) return null;
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
var calculatedAge = today.year - this.birthDate.value.year;
if (birthDate.value
.isAfter(DateTime(today.year - calculatedAge, today.month, today.day)))
calculatedAge--;
return "$calculatedAge ans";
}
Widget never rebuild.
return Observer(
builder: (ctx) {
final civilState = Provider.of<UserForm>(context).civilState;
return Container(
...
child: Observer(
builder: (context) => TextFieldState(
enabled: false,
state: civilState.age,
),
),
...
)
Can it be an interference between Provider & MobX ?
I've tried with one observer or many none trigger.
Thank you !
final civilState = Provider.of<UserForm>(context).civilState;
You accessed .civitState outside of Observer
So Mobx is unable to watch the field
This too is not responding what am I missing ?
return Observer(
builder: (ctx) {
final civilState = Provider.of<UserForm>(context).civilState;
return Container(
...
TextFieldState(
enabled: false,
state: civilState.age,
),
...
I think you need to remove the @observable annotation on age. It might be interfering with the @computed
Still no luck :/
Computed is called & no widget update
change tab and go back widget updated
When you switch tabs, there has to be something that is changing inside _calculateAge. That would be a trigger for the @computed. For me, it's not clear if _calculateAge is actually causing any changes that will affect age. Are there any observables consumed inside that function?
@pavanpodila I add you to the project so you can check we're using MobX Form the way is should ?!
Generated is triggered, value is calculated, state value is OK but the field inside Observer does not update.
Changing tab and going back does, they both trigger the same events in the same orders in the debug.
Sure, you can add me :-). Will take me towards end of this week to dig deeper. Have a project deadline on Thu/Fri.
Any update on this? I see PR, but it's not merged for months.
Sorry, no progress on this as this is not something I need immediately. Happy to look at a PR though :-)
Most helpful comment
Oh btw: say hello to flutter_mobx_forms...the most underrated package as of now :-)
https://pub.dev/packages/flutter_mobx_forms