Skip to content

FormField

github-actions[bot] edited this page Dec 3, 2024 · 3 revisions
API / FormField<TValue, TValidationError> class

Represents a form field containing the minimum set of information required to describe a field in a form.

Extends Validatable<TValidationError>.

class FormField<TValue, TValidationError = string>
    extends Validatable<TValidationError>

Source reference: src/forms/FormField.ts:169.

Generic Parameters

  • TValue - The type of values the field contains.

  • TValidationError - The concrete type for representing validation errors (strings, enums, numbers etc.).

    Default value: string.

Description

The form fields are designed to have the absolute minimum a form would require and at the same time be easily extensible. It is highly encouraged that applications define their own forms and fields even if there are no extra features, just to make it easy to add them later on.

The initialization follows a config-style approach where an object that contains all the values is provided to the field constructor. This allows for a simple syntax where properties are initialized in a similar way to object initializers.

On top of this, extending the field with this approach is easy. Extend the base config interface with extra properties that are need, pass the same object to the base constructor and extract the newly added ones afterwards.

Constructors

Properties

  • initialValue
  • name
  • readonly validation - Gets the validation configuration for the current field.
  • value
  • inherited error - Gets or sets the error message when the object is invalid.
  • inherited isInvalid - A flag indicating whether the object is invalid.
  • inherited isValid - A flag indicating whether the object is valid.
  • inherited propertiesChanged - An event that is raised when one or more properties may have changed.

Methods

  • reset - Resets the field. Only the validation configuration is reset, the field retains its current value.
  • protected onShouldTriggerValidation - Invoked when the current instance's properties change, this is a plugin method to help reduce

Inheritance Hierarchy

Guidance: Adding Features to a Field

One of the common features that a field may use is to track whether it was touched, i.e. if the input it is bound to ever came into focus. This is not something provided implicity by the base implementation, however adding this is easy.

class ExtendedFormField<TValue> extends FormField<TValue> {
  private _isTouched: boolean = false;

  public get isTouched(): boolean {
    return this._isTouched;
  }

  public set isTouched(value: boolean) {
    if (this._isTouched !== value) {
        this._isTouched = value;
        this.notifyPropertiesChanged('isTouched');
    }
  }
}

Form fields follow a config-style approach when being initialized, this allows to pass property values through the constructor instead of having to set each after the instance is created. Additionally, required and optional properties can be clearly specified.

Following on the example, an isTouched initial value can be provided by extending the base config and requesting it in the constructor.

interface IExtendedFieldConfig<TValue> extends IFormFieldConfig<TValue> {
  readonly isTouched?: boolean;
}

class ExtendedFormField<TValue> extends FormField<TValue> {
  public constructor({ isTouched = false, ...baseConfig }: IExtendedFieldConfig<TValue>) {
    super(baseConfig);

    this._isTouched = isTouched;
  }

  // ...
}

Changes to the field may trigger validation, by default only changes to the FormField.value does this, to change the behavior see FormField.onShouldTriggerValidation.

Guidance: Data Binding

Generally, binding in MVVM comes along with binding expressions, however this is not really necessary.

Binding, or data binding, refers to linking a view and a view model so one updates based on how the other changes. Usually, the view is the destination and the view model is the source, this gives us a few types of binding.

  • One-time: read the value when the component renders.
  • One-way: same as one-time, but whenever the value changes the component re-renders.
  • Two-way: applies mostly to inputs, when the field value changes the input reflects this, and when the input value changes, the field value is also updated. The two are permanently in sync.
  • One-way to source: whenever the value on the view changes, the view model is updated, but the view does not update when the view model changes.

Binding expressions are nice, but not really required. They may add additional complexity to the UI and can be limiting. While it is great to be descriptive about this, being able to do this more or less manually through event handling can provide more options. One should not exclude the other.

The most common types of binding are the one-way and two-way ones. On pages where we only show data, we usually use one-way binding because there's no interaction with it, we simply display it when it is ready. We can do this using the useViewModel hook alone.

Forms, on the other hand, use two-way binding. We want to keep our inputs in sync with whatever is going on with the fields. If changing one field clears another, we want to see this on the UI.

Unavoidably, we end up creating components for specific types of inputs to avoid repetitive code as well as ensure they all behave in the same way. Binding is generally handled inside these components using DOM event handlers/callbacks.

This is the most basic form of two-way binding, there's full control over it. The value coming from the field can be transformed in the component. Similarly, when the input changes its value can be converted back to something the form field may understand. There is full control over how the two link to one another as well as having the ability to create reusable components for specific types of inputs.

interface ITextInputProps {
  readonly field: FormField<string>;
}

function TextInput({ field }: ITextInputProps): JSX.Element {
  useViewModel(field);

  const onInputChangedCallback = useCallback(
    (event: ChangeEvent<HTMLInputElement>) => {
      field.value = event.target.value;
    },
    [field]
  );

  return (
    <input
      value={field.value}
      onChange={onInputChangedCallback} />
  );
}

See also

Clone this wiki locally