question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Cross-field validation not working

See original GitHub issue

I often need validation rules that access more than just a single field. Typical examples are:

  • ‘Password’ and ‘Repeat password’: The validation rule on ‘Repeat password’ should not only check that the field is not empty. It should also check that the value is identical to the value in ‘Password’.
  • ‘Start time’ and ‘End time’: The validation rule on ‘End time’ should not only check that ‘End time’ is a valid time. It should also check that ‘End time’ is greater than ‘Start time’.

The important thing here is that these validators should not just be re-evaluated when the value of their own field changes. They also have to be re-evaluated when the value of another field that’s used in the validator changes. For example, the validator on ‘Repeat password’ should run whenever ‘Password’ or ‘Repeat password’ is changed.

Luckily, MobX is great at tracking these kinds of dependencies, and it’s my understanding that you use MobX internally. So I expected that I could simply access the values of other fields within a validator. MobX would then automatically detect that the validator for ‘Repeat password’ depends on the value of ‘Password’ and re-evaluate it accordingly.

For some reason, this doesn’t seem to work. The validator on ‘Repeat password’ only gets re-evaluated when I change the value of ‘Repeat password’, not when I change the value of ‘Password’.

Below is my demo code. I’m sure I must have made some small error, but I can’t find it.

import React, { Component } from 'react';
import { FormState, FieldState } from 'formstate';
import { observer } from 'mobx-react';

@observer
class Input extends Component {
  render() {
    const { fieldState, label } = this.props;
    return (
      <div>
        <p>{label}</p>
        <p style={{ color: 'orange' }}>{fieldState.error}</p>
        <input
          type="text"
          value={fieldState.value}
          onChange={args => fieldState.onChange(args.target.value)}
        />
      </div>
    );
  }
}

@observer
export default class Usecase extends Component {
  constructor(props) {
    super(props);
    this.formState = new FormState({
      userName: new FieldState({ value: '' })
        .validators(value => !value && 'User name is required.'),
      password: new FieldState({ value: '' })
        .validators(value => !value && 'Password is required.'),
      repeatPassword: new FieldState({ value: '' })
        .validators(value => {
          if (!value) return 'Repeated password is required.';
          return (value !== this.formState.$.password.$) && 'Repeated password must match password.';
        })
    });
  }

  componentWillMount() {
    this.formState.validate();
  }

  render(){
    return (
      <form>
        <Input fieldState={this.formState.$.userName} label="User name" />
        <Input fieldState={this.formState.$.password} label="Password" />
        <Input fieldState={this.formState.$.repeatPassword} label="Repeat password" />
      </form>
    );
  }
}

Issue Analytics

  • State:closed
  • Created 7 years ago
  • Comments:7

github_iconTop GitHub Comments

1reaction
ghostcommented, May 29, 2018

I used inheritance to wrap up the functionality I’ve been playing with and to access the protected fields which make things work a little more correctly.

It’s still a horrible hack, but I think it’s finally to a point where I need to say “good enough” and move on.

export class AutorunFieldState<TFieldValue> extends FormState.FieldState<TFieldValue> {
    constructor(initValue: TFieldValue) {
        super(initValue);
    }

    @MobX.action
    public addAutorunValidator = <TForm extends FormState.ValidatableMapOrArray>(
        formState: FormState.FormState<TForm>,
        validator: (fieldValue: TFieldValue, form: FormState.FormState<TForm>) => string | null | undefined,
        initValue: TFieldValue
    ) => {
        //AJ: Add a validator which takes a single value, wrapping our form state aware validator.
        this._validators.push((fieldValue) => validator(fieldValue, formState));

        const autorunDisposer = MobX.autorun(() => {
            //AJ: Run this every time, even though we do nothing with the output.
            //AJ: MobX tracks the observables used in the autorun, and not just on the first run.
            //AJ: Apparently it updates the observables watch list on every run.
            validator(initValue, formState);

            if (this._autoValidationEnabled) {
                //AJ: If we need to validate, then queue a validation.
                //AJ: This provides context sensitive validation and debouncing.
                this.queueValidation();
            }
        }, {
            name: "ValidatorAutorun"
        });

        return autorunDisposer;
    };
};

private formState = new FormState.FormState({
    currentPassword: new FormState.FieldState("").disableAutoValidation().validators(
        Validators.createRequired("Current Password"),
        Validators.createMinLength("Current Password", 8)
    ),
    newPassword: new FormState.FieldState("").disableAutoValidation().validators(
        Validators.createRequired("New Password"),
        Validators.createMinLength("New Password", 8)
    ),
    confirmNewPassword: new AutorunFieldState("").disableAutoValidation().validators(
        Validators.createRequired("Confirm New Password"),
        Validators.createMinLength("Confirm New Password", 8)
    )
});

private confirmNewPasswordDisposeAutorun: MobX.IReactionDisposer | undefined;

public componentDidMount() {
    const autorunFieldState = this.formState.$.confirmNewPassword as AutorunFieldState<string>;

    this.confirmNewPasswordDisposeAutorun = autorunFieldState.addAutorunValidator(
        this.formState,
        (fieldValue, formState) => {
            return (fieldValue === formState.$.newPassword.value) ? "" : "Must match 'New Password'.";
        },
        ""
    );
}

public componentWillUnmount() {
    if(this.confirmNewPasswordDisposeAutorun)
        this.confirmNewPasswordDisposeAutorun();
}
0reactions
basaratcommented, May 30, 2018

form level validators only run on-demand (i.e. when the developer explicitly requests it const result = await this.formState.validate()😉

No. If you call formstate.compose, they also run when all the fields are valid see : https://formstate.github.io/demos/#cross-field-validation

image

Sorry for the late reply. Been a busy few days / weekend ❤️

Read more comments on GitHub >

github_iconTop Results From Across the Web

Angular: Cross field validation not working with setError()
The issue is what you called out - you need to remove the setErrors calls. Those are side effects of your group validator...
Read more >
Cross field validation does not work using "validators" key but ...
Minimal reproduction of the problem with instructions. Just play by switching from validators to validator as key to define custom validators.
Read more >
Cross-field validation not working - Oracle Communities
I am developing an APEX 5.1 application. I created a data entry form page with several data entry page items. Two of the...
Read more >
Cross Field Validation Using Angular Reactive Forms
In this blog post I would like to describe how you can add validation to multiple fields of your reactive forms in Angular...
Read more >
Angular: Cross-field validation for Reactive Forms - ITNEXT
Create a custom Form Validator. Let's start by creating a new file. I recommend that you create a new folder where you keep...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found