gcanti / tcomb-form

Forms library for react
https://gcanti.github.io/tcomb-form
MIT License
1.16k stars 136 forks source link

subtype validation #233

Closed mvlach closed 8 years ago

mvlach commented 8 years ago

Hi, I woud like to validate a struct.

http://jsbin.com/lejijawome/edit?js,console,output

The Passwords are are not evaluated correctly. Why the refinement method is not called ? I tried to set the Passwords as optional, but the label displays "Optional", but it still needs a value.

I would like to use this scenario:

  1. everytime is called the refinement validation method (samePassword), the error message is displayed. The inputs are with hasError class. (at this time I have to wait the user inputs some value - why?)
  2. I would like to display error message at the second input field - not the struct error (it is possible ?)

@gcanti: can you please create one advanced validation example ? From real life ?

Let's imagine:

Thanks Mila

gcanti commented 8 years ago

Hi @mvlach,

Why the refinement method is not called

maybe structs are not (yet) supported:

var Person = t.struct({
  name: t.Str,
  surname: t.Str,
  email: Email,
  passwords: t.maybe(Type) // <= not supported
});

can you please create one advanced validation example

First version (without point 2.):

var Email = t.refinement(t.String, function (s) {
  return /@/.test(s);
});

var Password = t.refinement(t.String, function (s) {
  return s.length >= 2;
});

function samePasswords(x) {
  return x.newPassword === x.confirmPassword;
}

var Type = t.subtype(t.struct({
  name: t.String,
  surname: t.String,
  email: Email,
  newPassword: Password,
  confirmPassword: Password
}), samePasswords);

var options = {
  error: 'Passwords must match',
  fields: {
    email: {
      error: 'Invalid email'
    },
    newPassword: {
      type: 'password',
      error: 'Invalid password, enter at least 2 chars'
    },
    confirmPassword: {
      type: 'password',
      error: 'Invalid password, enter at least 2 chars'
    }
  }
};

var App = React.createClass({

  onSubmit(evt) {
    evt.preventDefault();
    var value = this.refs.form.getValue();
    if (value) {
      console.log(value);
    }
  },

  render() {
    return (
      <form onSubmit={this.onSubmit}>
        <t.form.Form
          ref="form"
          type={Type}
          options={options}
        />
        <div className="form-group">
          <button type="submit" className="btn btn-primary">Save</button>
        </div>
      </form>
    );
  }

});

Second version (with point 2.):

var Email = t.refinement(t.String, function (s) {
  return /@/.test(s);
});

var Password = t.refinement(t.String, function (s) {
  return s.length >= 2;
});

function samePasswords(x) {
  return x.newPassword === x.confirmPassword;
}

var Type = t.subtype(t.struct({
  name: t.String,
  surname: t.String,
  email: Email,
  newPassword: Password,
  confirmPassword: Password
}), samePasswords);

var defaultOptions = {
  fields: {
    email: {
      error: 'Invalid email'
    },
    newPassword: {
      type: 'password',
      error: 'Invalid password, enter at least 2 chars'
    },
    confirmPassword: {
      type: 'password',
      error: 'Invalid password, enter at least 2 chars'
    }
  }
};

var App = React.createClass({

  getInitialState() {
    return {
      value: {},
      options: defaultOptions
    };
  },

  onSubmit(evt) {
    evt.preventDefault();
    this.setState({options: defaultOptions});
    var value = this.refs.form.getValue();
    if (value) {
      console.log(value);
    }
    else {
      if (this.state.value.confirmPassword && !samePasswords(this.state.value)) {
        this.setState({options: t.update(this.state.options, {
          fields: {
            confirmPassword: {
              hasError: { $set: true },
              error: { $set: 'Password must match' }
            }
          }
        })});
      }
    }
  },

  onChange(value) {
    this.setState({value});
  },

  render() {
    return (
      <form onSubmit={this.onSubmit}>
        <t.form.Form
          ref="form"
          type={Type}
          options={this.state.options}
          value={this.state.value}
          onChange={this.onChange}
        />
        <div className="form-group">
          <button type="submit" className="btn btn-primary">Save</button>
        </div>
      </form>
    );
  }

});
mvlach commented 8 years ago

@gcanti thanks you for the fast answer. I'll try this solutions.

The seconds does what I need but I think that this validation should be at the domain...

Have a nice day...

M.

gcanti commented 8 years ago

The seconds does what I need but I think that this validation should be at the domain...

Maybe that's a matter of some philosophical debate: we are talking about error messages here and in my opinion they belongs to the layout / display world (hence they go in the option prop).

The domain models:

var Email = t.refinement(t.String, function (s) {
  return /@/.test(s);
});

var Password = t.refinement(t.String, function (s) {
  return s.length >= 2;
});

function samePasswords(x) {
  return x.newPassword === x.confirmPassword;
}

var Type = t.subtype(t.struct({
  name: t.String,
  surname: t.String,
  email: Email,
  newPassword: Password,
  confirmPassword: Password
}), samePasswords);

already contain the whole information for validation purposes:

var value = {
  name: 'Giulio',
  surname: 'Canti',
  email: 'user@domain.com',
  newPassword: 'aaaaaa',
  confirmPassword: 'bbbbbb'
};

console.log(t.validate(value, Type).isValid()); // => false

Having said this, if you like to attach the validation messages to your types there's a third option, exploiting the getValidationErrorMessage (it will be automatically used as error option):

// again without point 2. for simplicity

t.String.getValidationErrorMessage = function () {
  return 'Required field';
};

var Email = t.refinement(t.String, function (s) {
  return /@/.test(s);
});

Email.getValidationErrorMessage = function () {
  return 'Invalid email';
};

var Password = t.refinement(t.String, function (s) {
  return s.length >= 2;
});

Password.getValidationErrorMessage = function () {
  return 'Invalid password, enter at least 2 chars';
};

function samePasswords(x) {
  return x.newPassword === x.confirmPassword;
}

var Type = t.subtype(t.struct({
  name: t.String,
  surname: t.String,
  email: Email,
  newPassword: Password,
  confirmPassword: Password
}), samePasswords);

Type.getValidationErrorMessage = function (value) {
  if (!samePasswords(value)) {
    return 'Password must match';
  }
};

var options = {
  fields: {
    newPassword: {
      type: 'password'
    },
    confirmPassword: {
      type: 'password'
    }
  }
};

var App = React.createClass({

  getInitialState() {
    return {value: {}};
  },

  onSubmit(evt) {
    evt.preventDefault();
    var value = this.refs.form.getValue();
    if (value) {
      console.log(value);
    }
  },

  onChange(value) {
    this.setState({value});
  },

  render() {
    return (
      <form onSubmit={this.onSubmit}>
        <t.form.Form
          ref="form"
          type={Type}
          options={options}
          value={this.state.value}
          onChange={this.onChange}
        />
        <div className="form-group">
          <button type="submit" className="btn btn-primary">Save</button>
        </div>
      </form>
    );
  }

});
gcanti commented 8 years ago

maybe structs are not (yet) supported:

Experimental support as per

enguerranws commented 6 years ago

Hi @gcanti,

I'm trying to do the exact same thing (double password field that must match). Using t.subtype(), the form doesn't show up anymore.

Is t.subtype() still supported? What's the best way to achieve that?

gcanti commented 6 years ago

@enguerranws yes is still supported as t.subtype is an (old) alias of t.refinement

This works for me

const Email = t.refinement(t.String, (s) => {
  return /@/.test(s)
})

const Password = t.refinement(t.String, (s) => {
  return s.length >= 2
})

function samePasswords(x) {
  return x.newPassword === x.confirmPassword
}

const Type = t.refinement(t.struct({
  name: t.String,
  surname: t.String,
  email: Email,
  newPassword: Password,
  confirmPassword: Password
}), samePasswords)

const defaultOptions = {
  fields: {
    email: {
      error: 'Invalid email'
    },
    newPassword: {
      type: 'password',
      error: 'Invalid password, enter at least 2 chars'
    },
    confirmPassword: {
      type: 'password',
      error: 'Invalid password, enter at least 2 chars'
    }
  }
}

class App extends React.Component {

  state = {
    value: {},
    options: defaultOptions
  }

  onSubmit = (evt) => {
    evt.preventDefault()
    this.setState({options: defaultOptions})
    const value = this.refs.form.getValue()
    if (value) {
      console.log(value)
    } else {
      const { newPassword, confirmPassword } = this.state.value
      if (newPassword && confirmPassword && !samePasswords(this.state.value)) {
        this.setState({options: t.update(this.state.options, {
          fields: {
            confirmPassword: {
              hasError: { $set: true },
              error: { $set: 'Password must match' }
            }
          }
        })})
      }
    }
  }

  onChange = (value) => {
    this.setState({value})
  }

  render() {
    return (
      <form onSubmit={this.onSubmit}>
        <t.form.Form
          ref="form"
          type={Type}
          options={this.state.options}
          value={this.state.value}
          onChange={this.onChange}
        />
        <div className="form-group">
          <button type="submit" className="btn btn-primary">Save</button>
        </div>
      </form>
    )
  }

}
enguerranws commented 6 years ago

Well, in my case, it just doesn't show the form. No errors given.

My form model looks like:

const formType = t.refinement(t.struct({
      email: Email,
      password: Password,
      password2: Password
    }), samePasswords);

Where samePasswords and Password the exact same functions as the example above.

However, if I simply do:

const formType = t.struct({
      email: Email,
      password: Password,
      password2: Password
    });

It works has expected. But no password matching validation. What do I miss?