gcanti / tcomb-form

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

tcomb-form onChange event on components #201

Closed benmonro closed 8 years ago

benmonro commented 9 years ago

_feature request_

It would be nice is to get the tcomb context in an event handler on an individual component.

I did this in a demo by just adding an 'onChange' event to my options and then in the form on change I did the following:

onChange(value, path) {
    if (path) {
        var component = this.refs.form.getComponent(path);
        if (_.has(component, 'props.options.onChange')) {
            component.props.options.onChange.call(this, t, value);
        }
    }
}

//... then in the options (defined in a message or another file)
var options = {
   fields: {
       myField: function(t, value) {

         var options = t.update(this.state.options, {
             fields: {
                 myOtherField: {
                     isHidden: {'$set': value.myField === 'N'}
                 }
             }
         });
         this.setState({options: options, value: value});

      }
   }
}

Even though I was able to accomplish this myself, I was just thinking it would be nice if tcomb would automatically do this for me so that my form onChange wouldn't have to do this...

This way the component that was changed is responsible for handling it's events but still has access to tcomb in it's context. The use case for this is that we want to have dynamic elements in our form where when one value in the form changes, it will modify how another element is rendered, or if it's even rendered at all. However, we don't want this to be the responsibility of the form, but rather the component that changed. For example. Suppose you have a country Select box. If the user choose US, you might want it to change the options of a 'State' select box with all of the US states. But then if they choose Canada maybe it changes it to 'provinces' etc. This is just one example. We will have various use cases where form elements can be defined as 'dynamic' by the user so we really need to handle this in the component options and not at the form level.

gcanti commented 9 years ago

Personally I'd prefer handle dynamic forms in a centralized and declarative way rather than scatter the logic amongst the components.

For example, this is the general form of a dynamic form:

function getType(value) {
  //...return the type based on value
}

function getOptions(value) {
  //...return the options based on value
}

var App = React.createClass({

  getState(value) {
    return {
      value,
      type: getType(value),
      options: getOptions(value)
    };
  },

  getInitialState() {
    return this.getState({});
  },

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

  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={this.state.type}
          options={this.state.options}
          value={this.state.value}
          onChange={this.onChange}
        />
        <button type="submit" className="btn btn-primary">Save</button>
      </form>
    );
  }

});

Now in your use case

Suppose you have a country Select box. If the user choose US, you might want it to change the options of a 'State' select box with all of the US states. But then if they choose Canada maybe it changes it to 'provinces' etc

would be

const Country = t.enums({
  US: 'United States',
  CA: 'Canada'
});

const State = t.enums({
  AL: 'Alabama',
  AK: 'Alaska'
  // and so on...
});

const Province = t.enums({
  A: 'Province A',
  B: 'Province B'
  // and so on...
});

function getType(value) {
  var props = {
    country: Country
  };
  switch (value.country) {
    case 'US' :
      props.state = State;
      break;
    case 'CA' :
      props.province = Province;
      break;
  }
  return t.struct(props);
}

function getOptions(value) {
  return {};
}

I found this pattern neat and clear. What do you think?

gcanti commented 9 years ago

Remark. The above getType function is not optimised (it's just the more general form I can think of) but you can optimise based on the particular use case

const NoCountry = t.struct({
  country: Country
});

const US = NoCountry.extend({
  state: State
});

const CA = NoCountry.extend({
  province: Province
});

function getType(value) {
  switch (value.country) {
    case 'US' : return US;
    case 'CA' : return CA;
    default : return NoCountry;
  }
}
benmonro commented 9 years ago

well, the problem I have w/ that approach is that our forms are defined by the user and sent to us as a message. Each message will have the schema, options & value. So we really need to define the change handlers in the options for each component. All of the messages will be processed by the same form. So while 1 form might have Country/State another might have something completely different that behaves in a totally different way. So my goal is to create reusable change handlers, transformers, templates & components that can be used by providing a string in a message (which will then be converted to a require).