gcanti / tcomb-form

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

Dynamic enums data (for select boxes etc) #131

Closed jfeltesse closed 9 years ago

jfeltesse commented 9 years ago

Hi,

React & tcomb beginner here so bear with me please.

Going through the doc and issues here I couldn't find how to dynamically build enums in a form. My use case being that I want to make a contact form where the user chooses its title (Mr, Ms etc.). These are values defined in a remote DB's table, hence the dynamic loading.

The doc has this sort of example (trimmed):

var Gender = t.enums({
  M: 'Male',
  F: 'Female'
});

var Person = t.struct({
  name: t.Str,
  gender: Gender
});

but it's static.

I suppose I could modify the Person struct (to follow that example) in the constructor function of the component (using ES6, getInitialState is gone) but the doc is not very clear about how to dynamically modify a struct.

gcanti commented 9 years ago

Hi! See the issues labelled with "dynamic forms":

https://github.com/gcanti/tcomb-form/issues?utf8=✓&q=is%3Aclosed%20label%3A%22Topic%3A%20Dynamic%20forms%22%20

In particular for dynamic selects:

https://github.com/gcanti/tcomb-form/issues/78

You can even modify the type on the fly:

https://github.com/gcanti/tcomb-form/issues/86

jfeltesse commented 9 years ago

Thanks for the pointers.

I got it to work but for the life of me I can't get it to play nice with existing options like config and i18n.

Basically, the config and i18n options work but only for that field, after that they're gone OR they work but the dropdown is not populated.

import React from 'react';
import Tcomb from 'tcomb-form';
let Form = Tcomb.form.Form;

let Contact = Tcomb.struct({
  civilite: Tcomb.maybe(Tcomb.enums({})),
  nom: Tcomb.Str,
  prenom: Tcomb.maybe(Tcomb.Str)
});

let options = {
  i18n: {
    add: 'Nouveau',
    down: '↓',
    optional: ' (facultatif)',
    remove: 'Supprimer',
    up: '↑'
  },
  config: {
    horizontal: {
      sm: [2, 10]
    }
  }
}

class ContactForm extends React.Component {

  constructor(props) {
    super(props);
    this.state = { };
  }

  componentDidMount() {
    fetchFormDropdowns().then((dropdowns) => {
      options.fields = {
        civilite: {
          factory: Tcomb.form.Select,
          options: dropdowns.civilites
        }
      };
      this.setState({ options: options });
    });
  }

  render() {
    return (
      <div className="contact_form">
        <h1>Nouveau contact</h1>
        <Form ref="form" type={Contact} options={this.state.options} />
      </div>
    );
  }
}

makes such a form (with the select dropdown populated properly):

screen shot 2015-05-16 at 14 12 37

As you can see the layout and i18n settings work on the first field but are "forgotten" for the others.

Setting the initial state like this.state = { options: options }; in the constructor makes a form like the screenshot below but in that case the select dropdown is not populated...

screen shot 2015-05-16 at 14 15 01

jfeltesse commented 9 years ago

Ah!

Obviously moments after I wrote this reply I notice issue #128 which deals with that case.

For reference to the people stumbling here, I got it to work with the following:

let options = {
 // options from above plus
  fields: {
    civilite: { }
  }
}

class ContactForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = { options: options };
  }

  componentDidMount() {
    fetchFormDropdowns().then((dropdowns) => {
      let newOptions = Tcomb.update(this.state.options, {
        fields: {
          civilite: {
            options: { '$set': dropdowns.civilites }
          }
        }
      });
      this.setState({ options: newOptions });
    });
  }

  // rest of the code above
}

Thanks for the help!!

gcanti commented 9 years ago

Thanks for the help!!

De rien.

Just a few remarks:

1) you can set the i18n option globally in order to be DRY (I guess you want the french localization in all your forms):

// this will be used for all the forms
// you can still override this global setting in a local form options
// if you need
Form.i18n = {
  add: 'Nouveau',
  down: '↓',
  optional: ' (facultatif)',
  remove: 'Supprimer',
  up: '↑'
};

...

let options = {
  config: {
    horizontal: {
      sm: [2, 10]
    }
  }
};

// instead of

let options = {
  i18n: {
    add: 'Nouveau',
    down: '↓',
    optional: ' (facultatif)',
    remove: 'Supprimer',
    up: '↑'
  },
  config: {
    horizontal: {
      sm: [2, 10]
    }
  }
}

2) add the bootstrap form-horizontal className to your wrapping div in order to get a nice layout:

render() {
  return (
    <div className="contact_form form-horizontal"> // <-- here
      <h1>Nouveau contact</h1>
      <Form ref="form" type={Contact} options={this.state.options} />
    </div>
  );
}

3) Tcomb.form.Select is the default for enums:

civilite: {
  factory: Tcomb.form.Select, // <-- you can safely remove this
  options: dropdowns.civilites
}