gcanti / tcomb-form

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

Select with number values? #187

Closed mohanzhang closed 9 years ago

mohanzhang commented 9 years ago

Is it possible to have a select field with number values? Suppose I have something like this:

export const Foo = t.struct({
  userId: t.Num,
});

I provide an option to the form like this:

userId: {
  factory: t.form.Select,
  options: _.map(collection, (x) => { return { value: x.id, text: x.name } }),
}

Everything works fine until validation, at which point I get the following (validationResult.firstError()): "Invalid value "3" supplied to /userId: Number"

This was surprising to me because I thought I'd read somewhere that tcomb will automatically cast numbers to Number?

EDIT: Oops, I just realized that it probably is the case that tcomb converts js numbers to tcomb's Num, but in my case, the value is a string. So really, I am not surprised :)

I tried using a transformer approach whereby I parse the value into a string. This works in my example, but not in another case where I use t.maybe(t.Num). It seems that null cannot reduce to a Number (ok, makes sense), but that's what the maybe is for?

gcanti commented 9 years ago

Hi @mohanzhang,

Is it possible to have a select field with number values?

Yes but it's a little verbose:

export const Foo = t.struct({
  userId: t.Num
});

userId: {
  factory: t.form.Select,
  transformer: { // handles numbers and Nil
    format: (value) => t.Nil.is(value) ? '' : String(value),
    parse: (value) => value === '' ? null : parseInt(value, 10)
  },
  options: _.map(collection, (x) => ({ value: x.id, text: x.name }))
}

Another option:

const collection = [
  {id: 1, name: 'name 1'},
  {id: 2, name: 'name 2'}
];

const UserID = t.enums(_.transform(collection, (result, x) => {
  result[x.id] = x.name;
  return result;
}, {}), 'UserID');

const Foo = t.struct({
  userId: UserID
});

const App = React.createClass({

  onSubmit(evt) {
    evt.preventDefault();
    var value = this.refs.form.getValue();
    if (value) {
      // here convert userId to a number...
    }
  },

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

});

p.s.

tcomb converts js numbers to tcomb's Num

Actually tcomb doesn't convert anything: tcomb types are just identity functions containing an assert to ensure the correct type:

var n1 = t.Num(2); // => n1 = 2
var n2 = t.Num('a'); // => throws

The only exceptions are the structs which don't merely return the input (but an instance of a "class")

var Foo = t.struct({
  userId: t.Num
});

var foo = Foo({userId: 1}); // => foo instanceof Foo === true
mohanzhang commented 9 years ago

Ah fantastic! Thank you so much @gcanti! I've noticed searching throughout the issues that your answers are always very thorough. And of course thank you for bringing some semblance of type safety to the js world ;)