gcanti / tcomb-form

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

Transformer now seems to be executing on every change? #251

Closed mohanzhang closed 8 years ago

mohanzhang commented 8 years ago

I recently upgraded from 0.6.3 to 0.6.10 and noticed that the behavior of transformers seems to have changed. It used to be that they applied to the field when I loaded the form (format) and retrieved the value of the form (parse). But now it seems that the transformers are applying every time I type in the text field.

The particular use-case I had been relying on the old behavior for was a field where you could enter a dollar amount, and the form would convert it to cents before submitting the value to the backend. So if you entered 100.50, it would parse the field as 10050.

Now what seems to be happening is that as soon as I type anything, the format function is called (which divides the backend value by 100 to put it back into dollars - I am aware of floating point errors and yes, this is a terrible design decision, but humor me here :disappointed: ), so if I type "1", it immediately becomes "0.01". It gets even funnier if you keep typing, because the number keeps getting divided by 100 and eventually floating point errors make the number look hilarious.

Any idea what's going on here? Or even if you can think of which commit might have changed this behavior, that would be very useful too. Thanks!

gcanti commented 8 years ago

Hi @mohanzhang,

Or even if you can think of which commit might have changed this behavior

I guess this one: "de-optimise structs / lists onChange, fix #235" (https://github.com/gcanti/tcomb-form/commit/e674ef2c3120ac6a286f470434533f89136f81e3)

I think you can fix your issue making your custom transformer.format idempotent (as it should be https://github.com/gcanti/tcomb-form/blob/master/GUIDE.md#transformers), this is my quick test:

const Type = t.struct({
  amount: t.Number
});

const defaultNumberTransformer = t.form.Textbox.numberTransformer;

const options = {
  fields: {
    amount: {
      transformer: {
        format: (n) => (
          t.Nil.is(n) || t.String.is(n) ? n : // idempotency!
          (n / 100).toFixed(2) // get bucks
        ),
        parse: (s) => {
          const n = defaultNumberTransformer.parse(s);
          return t.Number.is(n) ? n * 100 : n; // get cents
        }
      }
    }
  }
};

const value = {amount: 120};

const 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}
          value={value}
        />
        <div className="form-group">
          <button type="submit" className="btn btn-primary">Save</button>
        </div>
      </form>
    );
  }

});

Is it helpful?

mohanzhang commented 8 years ago

@gcanti Thanks Giulio - idempotency was indeed the key. As always, the completeness and helpfulness of your answers never cease to amaze me :) Cheers!

awhillas commented 8 years ago

Yeah, but idempotency isn't really the issue here. What you mean is that even if you pass it garbage then it should just return the garbage. You don't garintie anything about the input is what you mean my "idempotent".

I have a problem in which the "parse" function never seems to get called. Here is my transformer:

const tagsTransformer = {

  format: (values) => {  // server representation -> form values
    if(Array.isArray(values)){
      let taxonomys = {};
      values.map((tag) => {
        if (tag.taxonomy in taxonomys)
          taxonomys[tag.taxonomy].push(tag.id);
        else
          taxonomys[tag.taxonomy] = [tag.id];
      });
      return taxonomys;
    }
    else {
      return values;  // it must be idempotent aka ignore junk
    }
  },

  parse: (values) => {  // form values -> server representation
    // array of IDs
    console.log("parse", values);
    return Object.keys(values).reduce((p, c) => p.concat(values[c]), []);
  }
};

And here is the console log, note that parse never gets called screen shot 2016-06-10 at 10 39 28 am when is parse supposed to get called?

gcanti commented 8 years ago

Actually what I mean by "Idempotence" is... "Idempotence"

So in this case:

type Transformer<A, B> = {
  format: (a: A | B) => B,
  parse: (b: B) => A
};

where: