Skip to content

Control Value Accessors

Joan Pablo edited this page Oct 18, 2020 · 60 revisions

Reactive Forms introduces ControlValueAccessor as a mechanism to convert control's value data type to UI data type and vice versa. It acts as a brigde beetwen controls and UI reactive widgets.

For example supose you have the following definition:

final form = fb.group({
  'quantity': FormControl<int>(value: 0),
});

And you want to bind the quantity control to a ReactiveTextField:

ReactiveTextField(
  formControlName: 'quantity'
),

How is possible that the text field shows the number and also let you write digits as simple text and it updates the control with the correct int value? The answer is the ControlValueAccessor.

By default all reactive widgets use a Control Value Accessor to read and write values from the control.

When a control value accessor is not provided to a ReactiveTextField it selects the value accessor that best fits the control data type.

Reactive Forms includes some default control value accessor implementations:

  • DefaultValueAccessor (doesn't convert data types at all)
  • DoubleValueAccessor (convert between String and double)
  • IntValueAccessor (convert between String and int)
  • DateTimeValueAccessor (convert between String and DateTime)
  • TimeOfDayValueAccessor (convert between String and TimeOfDay)

Custom Value Accessor

The best way to understand a concept is to write some code. So lets implement a control value accessor that converts data from String to DateTime. We will also be able to customize the format of the DateTime when visualizing the value.

To implement a custom value accessor is as simple as create a class that extends ControlValueAccessor:

/// Defines a custom value accessor that converts from String to DateTime
/// and vice versa.
class DateTimeValueAccessor extends ControlValueAccessor<DateTime, String> {
  
}

and the override just two simple methods modelToViewValue and viewToModelValue:

/// Defines a custom value accessor that converts from String to DateTime
/// and vice versa.
class DateTimeValueAccessor extends ControlValueAccessor<DateTime, String> {
  @override
  String modelToViewValue(DateTime modelValue) {
    // TODO: converts from model data type to view data type
  }

  @override
  DateTime viewToModelValue(String viewValue) {
     // TODO: converts from view data type to model data type
  }
}

First lets implement modelToViewValue. This method receives the value from the model as a DateTime and returns the String representation of the date.

@override
String modelToViewValue(DateTime modelValue) {
  return DateFormat.yMMMd().format(modelValue);
}

Quiet simple. Isn't it? We have just used intl package to format the DateTime to a String.

But even in this simple code we have to solve first some issues before release it to production, for example:

  • What if the model value is null?
  • How can I provide the format of the String as an argument and not fixed format DateFormat.yMMMd() as in the above example?
  • What about the locale (en, es, fr) of the formated String?

Lets solve the first issue with a very simple condition: if the value is null then return an empty String:

@override
String modelToViewValue(DateTime modelValue) {
  return modelValue == null ? '' : DateFormat.yMMMd('en').format(modelValue);
}

Now lets provide DateTime format as an argument in constructor class.

class DateTimeValueAccessor extends ControlValueAccessor<DateTime, String> {
  final DateFormat format;

  /// Constructs an instance of the [DateTimeValueAccessor].
  /// 
  /// Can optionally provide the [format] argument.
  DateTimeValueAccessor({DateFormat format})
      : this.format = (format ?? DateFormat.yMMMd('en'));

  @override
  String modelToViewValue(DateTime modelValue) {
    return modelValue == null ? '' : this.format.format(modelValue);
  }

  //...
}

We have just add a constructor argument to provide a custom DateFormat. If argument is null then an instance of DateFormat.yMMMd('en') will be used as a default format.

Now lets implement the second and last method viewToModelValue. This method receives the value from the UI widget (for example from a ReactiveTextField) and then returns the value converted to a proper data type.

@override
DateTime viewToModelValue(String viewValue) {
  return (viewValue == null || viewValue.isEmpty)
      ? null
      : this.format.parse(viewValue);
}

If value is null or empty string then returns null, otherwise convert from String to DateTime.

The complete code of the class:

/// Defines a custom value accessor that converts from String to DateTime
/// and vice versa.
class DateTimeValueAccessor extends ControlValueAccessor<DateTime, String> {
  final DateFormat format;

  /// Constructs an instance of the [DateTimeValueAccessor]
  ///
  /// Can optionally provide [format] argument.
  DateTimeValueAccessor({DateFormat format})
      : this.format = (format ?? DateFormat.yMMMd('en'));

  @override
  String modelToViewValue(DateTime modelValue) {
    return modelValue == null ? '' : this.format.format(modelValue);
  }

  @override
  DateTime viewToModelValue(String viewValue) {
    return (viewValue == null || viewValue.isEmpty)
        ? null
        : this.format.parse(viewValue);
  }
}

So let's use this new control value accessor:

class SampleWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ReactiveFormBuilder(
      // defines a form with a FormControl<DateTime> control
      form: () => fb.group({'birthday': DateTime.now()}),
      builder: (context, form, child) {
        return ReactiveTextField(
          // binds to control
          formControlName: 'birthday',
          // provides a value accessor with a custom format
          valueAccessor: DateTimeValueAccessor(
            format: DateFormat.yMd(),
          ),
        );
      },
    );
  }
}
Clone this wiki locally