mobxjs / mobx.dart

MobX for the Dart language. Hassle-free, reactive state-management for your Dart and Flutter apps.
https://mobx.netlify.app
MIT License
2.39k stars 311 forks source link

[Working solution included] Observable TextField #750

Open fzyzcjy opened 2 years ago

fzyzcjy commented 2 years ago

Currently, as is seen in the example, TextField's text is not observable.

My naive attempt is as follows. Not sure whether this helps!

import 'package:flutter/material.dart';
import 'package:lombok_annotation/lombok_annotation.dart';
import 'package:mobx/mobx.dart';

class SyncTextField extends StatefulWidget {
  final GetSet<String> gs;

  // forward
  final InputDecoration? decoration;
  final TextInputType? keyboardType;

  const SyncTextField({Key? key, required this.gs, this.decoration, this.keyboardType}) : super(key: key);

  @override
  _SyncTextFieldState createState() => _SyncTextFieldState();
}

class _SyncTextFieldState extends State<SyncTextField> {
  late TextEditingController _controller;
  late VoidCallback _disposer;

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController();
    _disposer = reaction<String>(
      (_) => widget.gs.getter(),
      (val) => _controller.value = _controller.value.copyWith(text: val),
      fireImmediately: true,
    );
  }

  @override
  void dispose() {
    _disposer();
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _controller,
      onChanged: widget.gs.setter,
      // forward
      decoration: widget.decoration,
      keyboardType: widget.keyboardType,
    );
  }
}
fzyzcjy commented 2 years ago

This is quite helpful, because in the MobX world, it would be great if everything is observable, and TextField is very frequently used :)

@pavanpodila If the prototype looks good I will PR for it.

pavanpodila commented 2 years ago

I think these kind of things should be kept in a separate package: mobx_extensions. We should do that and then over time based on maturity we can bring them into core

fzyzcjy commented 2 years ago

Thanks for the reply. I will create that package later when having time.

So, maybe the documentation of mobx can mention that extensions package? Since these are quite useful things.

craigdfoster commented 1 year ago

This would be very useful in a package. Along with other Observable form fields.

Was this ever made? I can't find mobx_extensions

fzyzcjy commented 1 year ago

No it was not, but I have been using it for a long time. Below is the latest code copy-pasted from my internal library, which is fairly simple.

P.S. In addition to SyncTextField, there is SyncIntTextField which handles integer input.

```dart import 'dart:math'; import 'dart:ui' as ui; import 'package:convenient_test_common/src/common_flutter/get_set.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; class SyncTextField extends StatefulWidget { final GetSet gs; // forward final FocusNode? focusNode; final InputDecoration? decoration; final TextInputType? keyboardType; final TextInputAction? textInputAction; final TextCapitalization textCapitalization; final TextStyle? style; final StrutStyle? strutStyle; final TextAlign textAlign; final TextAlignVertical? textAlignVertical; final TextDirection? textDirection; final bool autofocus; final String obscuringCharacter; final bool obscureText; final bool autocorrect; final bool enableSuggestions; final int? maxLines; final int? minLines; final bool expands; final bool readOnly; final bool? showCursor; final int? maxLength; final MaxLengthEnforcement? maxLengthEnforcement; final ValueChanged? onChanged; final VoidCallback? onEditingComplete; final ValueChanged? onSubmitted; final AppPrivateCommandCallback? onAppPrivateCommand; final List? inputFormatters; final bool? enabled; final double cursorWidth; final double? cursorHeight; final Radius? cursorRadius; final Color? cursorColor; final ui.BoxHeightStyle selectionHeightStyle; final ui.BoxWidthStyle selectionWidthStyle; final Brightness? keyboardAppearance; final EdgeInsets scrollPadding; final bool enableInteractiveSelection; final TextSelectionControls? selectionControls; final DragStartBehavior dragStartBehavior; final GestureTapCallback? onTap; final MouseCursor? mouseCursor; final InputCounterWidgetBuilder? buildCounter; final ScrollPhysics? scrollPhysics; final ScrollController? scrollController; final Iterable? autofillHints; final Clip clipBehavior; final String? restorationId; final bool enableIMEPersonalizedLearning; const SyncTextField({ super.key, required this.gs, // forward this.focusNode, this.decoration = const InputDecoration(), this.keyboardType, this.textInputAction, this.textCapitalization = TextCapitalization.none, this.style, this.strutStyle, this.textAlign = TextAlign.start, this.textAlignVertical, this.textDirection, this.readOnly = false, this.showCursor, this.autofocus = false, this.obscuringCharacter = '•', this.obscureText = false, this.autocorrect = true, this.enableSuggestions = true, this.maxLines = 1, this.minLines, this.expands = false, this.maxLength, this.maxLengthEnforcement, this.onChanged, this.onEditingComplete, this.onSubmitted, this.onAppPrivateCommand, this.inputFormatters, this.enabled, this.cursorWidth = 2.0, this.cursorHeight, this.cursorRadius, this.cursorColor, this.selectionHeightStyle = ui.BoxHeightStyle.tight, this.selectionWidthStyle = ui.BoxWidthStyle.tight, this.keyboardAppearance, this.scrollPadding = const EdgeInsets.all(20.0), this.dragStartBehavior = DragStartBehavior.start, this.enableInteractiveSelection = true, this.selectionControls, this.onTap, this.mouseCursor, this.buildCounter, this.scrollController, this.scrollPhysics, this.autofillHints = const [], this.clipBehavior = Clip.hardEdge, this.restorationId, this.enableIMEPersonalizedLearning = true, }); @override _SyncTextFieldState createState() => _SyncTextFieldState(); } class _SyncTextFieldState extends State { late TextEditingController _controller; late VoidCallback _disposer; @override void initState() { super.initState(); _controller = TextEditingController(text: widget.gs.getter()); _disposer = reaction((_) => widget.gs.getter(), _handleGsChange).call; } @override void dispose() { _disposer(); _controller.dispose(); super.dispose(); } void _handleGsChange(String newText) { int _clampOffset(int raw) => min(raw, newText.length); final oldValue = _controller.value; _controller.value = oldValue.copyWith( text: newText, selection: oldValue.selection.copyWith( baseOffset: _clampOffset(oldValue.selection.baseOffset), extentOffset: _clampOffset(oldValue.selection.extentOffset), ), composing: TextRange( start: _clampOffset(oldValue.composing.start), end: _clampOffset(oldValue.composing.end), ), ); } @override Widget build(BuildContext context) { return TextField( controller: _controller, onChanged: (v) { widget.gs.setter(v); widget.onChanged?.call(v); }, // forward focusNode: widget.focusNode, decoration: widget.decoration, keyboardType: widget.keyboardType, textInputAction: widget.textInputAction, textCapitalization: widget.textCapitalization, style: widget.style, strutStyle: widget.strutStyle, textAlign: widget.textAlign, textAlignVertical: widget.textAlignVertical, textDirection: widget.textDirection, readOnly: widget.readOnly, showCursor: widget.showCursor, autofocus: widget.autofocus, obscuringCharacter: widget.obscuringCharacter, obscureText: widget.obscureText, autocorrect: widget.autocorrect, enableSuggestions: widget.enableSuggestions, maxLines: widget.maxLines, minLines: widget.minLines, expands: widget.expands, maxLength: widget.maxLength, maxLengthEnforcement: widget.maxLengthEnforcement, onEditingComplete: widget.onEditingComplete, onSubmitted: widget.onSubmitted, onAppPrivateCommand: widget.onAppPrivateCommand, inputFormatters: widget.inputFormatters, enabled: widget.enabled, cursorWidth: widget.cursorWidth, cursorHeight: widget.cursorHeight, cursorRadius: widget.cursorRadius, cursorColor: widget.cursorColor, selectionHeightStyle: widget.selectionHeightStyle, selectionWidthStyle: widget.selectionWidthStyle, keyboardAppearance: widget.keyboardAppearance, scrollPadding: widget.scrollPadding, dragStartBehavior: widget.dragStartBehavior, enableInteractiveSelection: widget.enableInteractiveSelection, selectionControls: widget.selectionControls, onTap: widget.onTap, mouseCursor: widget.mouseCursor, buildCounter: widget.buildCounter, scrollController: widget.scrollController, scrollPhysics: widget.scrollPhysics, autofillHints: widget.autofillHints, clipBehavior: widget.clipBehavior, restorationId: widget.restorationId, enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, ); } } class SyncIntTextField extends StatelessWidget { final GetSet gs; // forward final UiElementLid? uiId; final InputDecoration? decoration; // ignore: unnecessary-nullable const SyncIntTextField({ super.key, required this.gs, required this.uiId, this.decoration, }); @override Widget build(BuildContext context) { return SyncTextField( gs: _gsIntToGsString(gs), keyboardType: TextInputType.number, // forward uiId: uiId, decoration: decoration, ); } } GetSet _gsIntToGsString(GetSet gsInt) => GetSet( getter: () => gsInt.getter().toString(), setter: (s) { final i = int.tryParse(s); if (i != null) gsInt.setter(i); }); ```
MaoHolden commented 12 months ago

@fzyzcjy this seems super interesting, looks like this will allow me to send the mobx observable to the TextField right? How do you use the GetSet class? It looks like you use this class to wrap mobx observables and the send the GetSet to the TextField right?

Would you be so kind to show me how to use this using a Mobx Store?

Thanks

fzyzcjy commented 12 months ago

looks like this will allow me to send the mobx observable to the TextField right?

Yes, automatically synchronize textfield with observable.

GetSet

class GetSet<T> {
  T Function() getter;
  void Function(T value) setter;

  GetSet({required this.getter, required this.setter});

  GetSet.gs(this.getter, this.setter);
}

Usage:

class YourStore { @observable String a; }

SyncTextField(
  gs: GetSet(getter: () => yourStore.a, setter: (value) => yourStore.a = value),
)
MaoHolden commented 11 months ago

@fzyzcjy thank you Yo! I'm using it already in my app. works great!

Not sure exactly how it works but I'll figure it out.

fzyzcjy commented 11 months ago

You are welcome!