gcanti / tcomb-form-native

Forms library for react-native
MIT License
3.14k stars 457 forks source link

Multi-select validation for enums? #382

Open compojoom opened 7 years ago

compojoom commented 7 years ago

I created a template using this component: https://github.com/ataomega/react-native-multiple-select-list I used enums to display the list of options and I'm able to select several options. However I'm faling at the validation. It seems that enums are just a single value, where in my case I'm getting an array of values => so tcomb is failing the form validation.

I thought of doing a refinement to check if the value is an array and then return true, but when I did:

var predicate = function (x) { return true };
var MultipleEnum = t.refinement(t.enums, predicate);

used like this

other_chemicals: MultipleEnum({
                c: 'pH, P, K, Mg',
                d: 'Bor',
                e: 'Humus'
            }),

I'm getting invalid value supplied to enums? image

Can I do a refinement for enums? Or what would be the correct type to use in order to create a multi-select component?

compojoom commented 7 years ago

I just solved this in the most stupid way possible. I would be happy if someone could point me to a better solution.

So I have my enums property and I made a transformer. Whenever the parsed value is an array I just get the first item. This way if I have more than 1 item in the array the validation still doesn't complain. I don't use the returned struct from .getValue(), but directly use my state to save the object, so I end up with the correct data, but that is really not a good solution. I would be happy if someone could shed some light on this. I've re-read the docs like 100 times and I just don't understand how one is supposed to do a multiple selection list.

phillbaker commented 6 years ago

Seems like this is a dupe of #131? (Also - that you actually want to use a t.list(t.String) and not a t.enum().)

jupiter23 commented 5 years ago

@compojoom , were you ever able to validate your values when more than one is selected? I am having the same issue.

const Choices = {
  'First': 'First choice',
  'Second': 'Second choice',
}

const EnumChoices = t.enums(Choices, 'Choices');

const Model = t.struct({
  choices: EnumChoices,
});

const options = {
  label: "The Choice Maker",
  auto: 'placeholders',
  fields: {
    choices: {
      label: 'Make a choice',
      choices: Choices,
      factory: MultiSelectExample,
    }
  }
};

In the MultiSelectExample component I have assigned as factory

class MultiSelectExample extends t.form.Select {

  constructor(props) {
    super(props)
    this.choices = props.options.choices
  }

  onSelectedItemsChange = selectedItems => {
    this.setState({ selectedItems })
    this.props.onChange(selectedItems);
  }

  getChoices() {
    let choices = []
    for (const choice in this.choices) {
      choices.push({id: choice, name: this.choices[choice]})
    }
    return choices
  }

  getTemplate() {
    let self = this;
    return function(locals) {
      return (
          <View>
            <MultiSelect 
               items={self.getChoices()} 
               /*  lots of props for react-native-multiple-select */
            />
          </View>
      );
    }
  }
}

Then the default component:

export default class App extends React.Component<Props, State> {

  constructor(props) {
    super(props);
    this.state = {
      options: options,
      value: {}
    };
  }

  handleSubmit = () => {
    const value = this._form.getValue();
    console.log('value: ', value);
  }

  onSubmit(event) {
    event.preventDefault();
    const value = this._form.getValue();
    console.log(value);
  }

  onChange(value, path) {
    console.log(value);
  }

  render() {
    return (
        <View style={styles.container}>
          <Form
              ref = {component => this._form = component}
              type={Model}
              options={options}
              value={this.state.value}
              onChange={this.onChange}
          />
          <TouchableHighlight style={styles.button} onPress={this.handleSubmit} underlayColor='#99d9f4'>
            <Text style={styles.buttonText}>Save</Text>
          </TouchableHighlight>
        </View>
    );
  }
}

When I only select one value, onChange() and onSubmit() give this.

[18:14:38] Object {
[18:14:38]   "choices": Array [
[18:14:38]     "First",
[18:14:38]   ],
[18:14:38] }
[18:14:39] value:  Struct {
[18:14:39]   "choices": "First",
[18:14:39] }

If I select two, I get null value for the form onSubmit.

[18:14:40] Object {
[18:14:40]   "choices": Array [
[18:14:40]     "First",
[18:14:40]     "Second",
[18:14:40]   ],
[18:14:40] }
[18:14:41] value:  null
compojoom commented 5 years ago

I "solved" it with this:

export const MultiSelectTransformer = {
  format: value => {
    return Array.isArray(value) ? value : []
  },
  parse: value => {
    // Just trick it here by returning the first value. Otherwise validation won't work
    // TODO: find better way to do this: https://github.com/gcanti/tcomb-form-native/issues/382
    return value ? value[0] : []
  }
}

I feel/know that this is not the correct solution, but I still fail to understand how to do it correctly...

jupiter23 commented 5 years ago

@compojoom seems like I was able to solve my issues by not extending from the t.form.Select component and by providing a t.list(t.String) comb to my Model instead of enum. So this seems to remove the need for validating enums.

import MultiSelect from 'react-native-multiple-select';
import t from 'tcomb-form-native';

const Form = t.form.Form;

const Choices = {
  'First': 'First choice',
  'Second': 'Second choice',
}

const Model = t.struct({
  choices: t.list(t.String),
});

class MultiSelectExample extends t.form.Component {

  constructor(props) {
    super(props)
    this.choices = props.options.choices
  }

  onSelectedItemsChange = selectedItems => {
    this.setState({ selectedItems })
    this.props.onChange(selectedItems);
  }

  getChoices() {
    let choices = []
    for (const choice in this.choices) {
      choices.push({id: choice, name: this.choices[choice]})
    }
    return choices
  }

  getTemplate() {
    let self = this;
    return function(locals) {
      return (
          <View>
            <Text>
              MultiSelect Sample
            </Text>
            <MultiSelect
                ...
                items={self.getChoices()}
                ref={(component) => { self.multiSelect = component }}
                onSelectedItemsChange={self.onSelectedItemsChange}
                selectedItems={self.state.selectedItems}
                ...
            />
            <View>
              {self.multiSelect && self.multiSelect.getSelectedItemsExt(self.state.selectedItems)}
            </View>
          </View>
      );
    }
  }
}

const options = {
  label: "The Choice Maker",
  auto: 'placeholders',
  fields: {
    choices: {
      label: 'Make a choice',
      choices: Choices,
      factory: MultiSelectExample,
    }
  }
};

type Props = {}
type State = {
  value: Object,
  options: Object
}

export default class App extends React.Component<Props, State> {

  constructor(props) {
    super(props);
    this.state = {
      options: options,
      value: {}
    };
  }

  handleSubmit = () => {
    const value = this._form.getValue();
    console.log('fn0rd! value: ', value);
  }

  onChange(value, path) {
    console.log(value);
  }

  render() {
    return (
        <View style={styles.container}>
          <Form
              ref = {component => this._form = component}
              type={Model}
              options={options}
              value={this.state.value}
              onChange={this.onChange}
          />
          <TouchableHighlight style={styles.button} onPress={this.handleSubmit} underlayColor='#99d9f4'>
            <Text style={styles.buttonText}>Save</Text>
          </TouchableHighlight>
        </View>
    );
  }
}