mobx.dart icon indicating copy to clipboard operation
mobx.dart copied to clipboard

[Working solution included] Observable TextField

Open fzyzcjy opened this issue 4 years ago • 9 comments

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 avatar Feb 24 '22 02:02 fzyzcjy

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.

fzyzcjy avatar Mar 18 '22 12:03 fzyzcjy

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

pavanpodila avatar Mar 19 '22 03:03 pavanpodila

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.

fzyzcjy avatar Mar 19 '22 03:03 fzyzcjy

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

Was this ever made? I can't find mobx_extensions

craigdfoster avatar Aug 25 '23 11:08 craigdfoster

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.

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<String> 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<String>? onChanged;
  final VoidCallback? onEditingComplete;
  final ValueChanged<String>? onSubmitted;
  final AppPrivateCommandCallback? onAppPrivateCommand;
  final List<TextInputFormatter>? 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<String>? 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 <String>[],
    this.clipBehavior = Clip.hardEdge,
    this.restorationId,
    this.enableIMEPersonalizedLearning = true,
  });

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

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

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController(text: widget.gs.getter());
    _disposer = reaction<String>((_) => 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<int> 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<String> _gsIntToGsString(GetSet<int> gsInt) => GetSet(
    getter: () => gsInt.getter().toString(),
    setter: (s) {
      final i = int.tryParse(s);
      if (i != null) gsInt.setter(i);
    });

fzyzcjy avatar Aug 25 '23 12:08 fzyzcjy

@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

MaoHolden avatar Sep 23 '23 17:09 MaoHolden

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),
)

fzyzcjy avatar Sep 23 '23 23:09 fzyzcjy

@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.

MaoHolden avatar Sep 30 '23 17:09 MaoHolden

You are welcome!

fzyzcjy avatar Oct 01 '23 02:10 fzyzcjy