Tcomb-form-native: Multi-Select

Created on 15 Mar 2016  Â·  12Comments  Â·  Source: gcanti/tcomb-form-native

How does one create a multi select field using tcomb-form-native?

Most helpful comment

@appbeijing I ended up creating a factory. Here's the code:

// MultiSelect.js
import React, { View, Text } from 'react-native'
import t from 'tcomb-form-native'
var Checkbox = require('react-native-checkbox');

// extend the base Component
class MultiSelect extends t.form.Select {

  constructor (props) {
    console.log(props);
    super(props);
    var locals = super.getLocals();
    var isChecked = {};
    if (locals.options != null) {
      locals.options.forEach(function(item) {
        isChecked[item.text] = false;
      });
    }

    if (this.props.value != null) {
      this.props.value.forEach(function(item) {
        isChecked[item] = true;
      });
    }
    this.state = {
      isChecked: isChecked
    }
  }

  getTransformer() {
    return MultiSelect.transformer();
  }

  // this is the only required method to implement
  getTemplate() {
    let self = this;
    return function (locals) {
      console.log(locals);

      var stylesheet = locals.stylesheet;
      var formGroupStyle = stylesheet.formGroup.normal;
      var controlLabelStyle = stylesheet.controlLabel.normal;
      var checkboxStyle = stylesheet.checkbox.normal;
      var helpBlockStyle = stylesheet.helpBlock.normal;
      var errorBlockStyle = stylesheet.errorBlock;

      if (locals.hasError) {
        formGroupStyle = stylesheet.formGroup.error;
        controlLabelStyle = stylesheet.controlLabel.error;
        checkboxStyle = stylesheet.checkbox.error;
        helpBlockStyle = stylesheet.helpBlock.error;
      }

      var label = locals.label ? <Text style={controlLabelStyle}>{locals.label}</Text> : null;
      var help = locals.help ? <Text style={helpBlockStyle}>{locals.help}</Text> : null;
      var error = locals.hasError && locals.error ? <Text style={errorBlockStyle}>{locals.error}</Text> : null;

      var viewArr = [];
      locals.options.forEach(function(item) {
        viewArr.push(<Checkbox
          ref={item.text}
          label={item.text}
          checked={self.state.isChecked[item.text]}
          onChange={(checked) => {self._onChange(item, locals, checked)}}
        />);
      });

      return (
        <View style={formGroupStyle}>
          {label}
          {viewArr}
          {help}
          {error}
        </View>
      )
    }
  }

  getOptions() {
    var options = this.props.options;
    var items = options.options ? options.options.slice() : this.getOptionsOfEnum(this.getEnum());
    if (options.order) {
      items.sort(this.getComparator(options.order));
    }
    return items;
  }

  _onChange(item, locals, checked) {
    var isChecked = this.state.isChecked;
    isChecked[item.text] = checked

    var changeArr = [];
    for (var key in isChecked) {
      if (isChecked[key] == true) {
        changeArr.push(key);
      }
    }
    locals.onChange(changeArr);

    this.setState({
      isChecked: isChecked
    });

    // Not recommended. A better way?
    //this.forceUpdate();

  }
}

MultiSelect.transformer = () => {
  return {
    format: value => Array.isArray(value) ? value : [],
    parse: value => value ? value : []
  };
};

export default MultiSelect

This is how I'm using it:

item.picklist.forEach(function(picklistItem) {
    picklistOptions = picklistOptions.concat({value: picklistItem.value, text: picklistItem.label});
});
form = form.extend({[item.name]: t.maybe(t.list(t.String)) });
options.fields[item.name] = {label: item.label, factory: MultiSelect, options: picklistOptions};

Hope this helps!

All 12 comments

Hello @mintotsai !!

You have to create your own factory to do that. Take this as an example https://github.com/APSL/react-native-floating-label/blob/master/FloatingLabel.js

Basically you would end with something like this (options are a list of possible selection options):

<View>
  {locals.options.forEach(option => (
    <TouchableOpacity onPress={() => this.setState({value: this.state.value.concat(option)})}>
      <Text>{option.name}</Text>
    </TouchableOpacity>
  ))}
</View>

Then, you have to decide which type should back your multi-selection. That's a pure implementation decision that you should leverage, I'll leave that to you ;)

can you show a simple but complete example about multi select field? thanks。

@appbeijing I ended up creating a factory. Here's the code:

// MultiSelect.js
import React, { View, Text } from 'react-native'
import t from 'tcomb-form-native'
var Checkbox = require('react-native-checkbox');

// extend the base Component
class MultiSelect extends t.form.Select {

  constructor (props) {
    console.log(props);
    super(props);
    var locals = super.getLocals();
    var isChecked = {};
    if (locals.options != null) {
      locals.options.forEach(function(item) {
        isChecked[item.text] = false;
      });
    }

    if (this.props.value != null) {
      this.props.value.forEach(function(item) {
        isChecked[item] = true;
      });
    }
    this.state = {
      isChecked: isChecked
    }
  }

  getTransformer() {
    return MultiSelect.transformer();
  }

  // this is the only required method to implement
  getTemplate() {
    let self = this;
    return function (locals) {
      console.log(locals);

      var stylesheet = locals.stylesheet;
      var formGroupStyle = stylesheet.formGroup.normal;
      var controlLabelStyle = stylesheet.controlLabel.normal;
      var checkboxStyle = stylesheet.checkbox.normal;
      var helpBlockStyle = stylesheet.helpBlock.normal;
      var errorBlockStyle = stylesheet.errorBlock;

      if (locals.hasError) {
        formGroupStyle = stylesheet.formGroup.error;
        controlLabelStyle = stylesheet.controlLabel.error;
        checkboxStyle = stylesheet.checkbox.error;
        helpBlockStyle = stylesheet.helpBlock.error;
      }

      var label = locals.label ? <Text style={controlLabelStyle}>{locals.label}</Text> : null;
      var help = locals.help ? <Text style={helpBlockStyle}>{locals.help}</Text> : null;
      var error = locals.hasError && locals.error ? <Text style={errorBlockStyle}>{locals.error}</Text> : null;

      var viewArr = [];
      locals.options.forEach(function(item) {
        viewArr.push(<Checkbox
          ref={item.text}
          label={item.text}
          checked={self.state.isChecked[item.text]}
          onChange={(checked) => {self._onChange(item, locals, checked)}}
        />);
      });

      return (
        <View style={formGroupStyle}>
          {label}
          {viewArr}
          {help}
          {error}
        </View>
      )
    }
  }

  getOptions() {
    var options = this.props.options;
    var items = options.options ? options.options.slice() : this.getOptionsOfEnum(this.getEnum());
    if (options.order) {
      items.sort(this.getComparator(options.order));
    }
    return items;
  }

  _onChange(item, locals, checked) {
    var isChecked = this.state.isChecked;
    isChecked[item.text] = checked

    var changeArr = [];
    for (var key in isChecked) {
      if (isChecked[key] == true) {
        changeArr.push(key);
      }
    }
    locals.onChange(changeArr);

    this.setState({
      isChecked: isChecked
    });

    // Not recommended. A better way?
    //this.forceUpdate();

  }
}

MultiSelect.transformer = () => {
  return {
    format: value => Array.isArray(value) ? value : [],
    parse: value => value ? value : []
  };
};

export default MultiSelect

This is how I'm using it:

item.picklist.forEach(function(picklistItem) {
    picklistOptions = picklistOptions.concat({value: picklistItem.value, text: picklistItem.label});
});
form = form.extend({[item.name]: t.maybe(t.list(t.String)) });
options.fields[item.name] = {label: item.label, factory: MultiSelect, options: picklistOptions};

Hope this helps!

@mintotsai Could you expain that what is item,form, picklistOptions?
or give a sample.

I'm not good at react-native.

where to place this factory? Could you explain how to write and use custom factory components?

@mintotsai can you elaborate on the way you present the data to the factory? How do you structure your list in this case?

in your function

getOptions() {
    var options = this.props.options;
    var items = options.options ? options.options.slice() : this.getOptionsOfEnum(this.getEnum());
    if (options.order) {
      items.sort(this.getComparator(options.order));
    }
    return items;
  }

you extract your data for the Multiple Select by either copying from the options, or with the getOptionsOfEnum() function.
Unfortunatelly you dont provide an example for the data structure when used in options.
I tried using the enum as for the simple selector, but the getOptionsOfEnum() function seems not to be defined. I receive the same error all along:

this.getOptionsOfEnum is not a function. (In 'this.getOptionsOfEnum(this.getEnum())', 'this.getOptionsOfEnum' is undefined)

@mintotsai 's code is not working for 0.47.x from my testing, so I updated as below:

// MultiSelect.js
import React from 'react';
import { View, Text } from 'react-native';
import t from 'tcomb-form-native';
import Checkbox from 'react-native-checkbox';
import autobind from 'autobind-decorator';
import _ from 'lodash';

class TcombMultiSelect extends t.form.Select {
  constructor(props) {
    super(props);
    const locals = super.getLocals();
    const isChecked = {};
    const { options, value } = locals;

   options &&
      options.map(item => (isChecked[item.text] = !!_.find(value, _value => _value === item.text)));

    this.state = {
      isChecked,
    };
  }

  getTransformer() {
    return TcombMultiSelect.transformer();
  }

  getTemplate() {
    return (locals) => {
      const stylesheet = locals.stylesheet;
      let formGroupStyle = stylesheet.formGroup.normal;
      let controlLabelStyle = stylesheet.controlLabel.normal;
      let checkboxStyle = stylesheet.checkbox.normal;
      let helpBlockStyle = stylesheet.helpBlock.normal;
      const errorBlockStyle = stylesheet.errorBlock;

      if (locals.hasError) {
        formGroupStyle = stylesheet.formGroup.error;
        controlLabelStyle = stylesheet.controlLabel.error;
        checkboxStyle = stylesheet.checkbox.error;
        helpBlockStyle = stylesheet.helpBlock.error;
      }

      const label = locals.label ? <Text style={controlLabelStyle}>{locals.label}</Text> : null;
      const help = locals.help ? <Text style={helpBlockStyle}>{locals.help}</Text> : null;
      const error =
        locals.hasError && locals.error ? (
          <Text style={errorBlockStyle}>{locals.error}</Text>
        ) : null;

      const viewArr = [];

      locals.options.map((item, index) =>
        viewArr.push(<Checkbox
            key={index}
            ref={item.text}
            label={item.text}
            checked={this.state.isChecked[item.text]}
            onChange={(checked) => {
              this._onChange(item, locals, !checked);
            }}
          />, ), );

      return (
        <View style={formGroupStyle}>
          {label}
          {viewArr}
          {help}
          {error}
        </View>
      );
    };
  }

  getOptions() {
    const { options } = this.props;
    const items = options.options ? options.options.slice() : this.getOptionsOfEnum(this.getEnum());
    if (options.order) {
      items.sort(this.getComparator(options.order));
    }
    return items;
  }

  @autobind
  _onChange(item, locals, checked) {
    const { isChecked } = this.state;
    isChecked[item.text] = checked;
    const changeArr = [];

    _.forIn(isChecked, (isCheckedItem, key) => {
      isCheckedItem && changeArr.push(key);
    });

    locals.onChange(changeArr);

    this.setState({
      isChecked,
    });
  }
}

TcombMultiSelect.transformer = () => ({
  format: value => (Array.isArray(value) ? value : []),
  parse: value => value || [],
});

export default TcombMultiSelect;

Thank you for your help @yai111

unfortunatelly this part is already causing an error. I am getting a RCTFatal error on the @autobind line.

  @autobind
  _onChange(item, locals, checked) {
    const { isChecked } = this.state;
    isChecked[item.text] = checked;
    const changeArr = [];

    _.forIn(isChecked, (isCheckedItem) => {
      isCheckedItem && changeArr.push(isCheckedItem);
    });

    locals.onChange(changeArr);

    this.setState({
      isChecked,
    });

    // Not recommended. A better way?
    // this.forceUpdate();
  }

screenshot

@Maksym-Vasyukov Probably you are missing babel-plugin-transform-decorators-legacy dependency and

{
  "presets": ["react-native"],
  "plugins": ["transform-decorators-legacy"]
}

in .babelrc

or you can remove the @ autobind decorator, add

this. _onChange = this. _onChange.bind(this); 

in constructor

@yai111 great, fixed the building problem with the constructor solution.
Now I am stuck at the same problem as six days ago with the former solution.

😄

@Maksym-Vasyukov

const fields = [
  t.struct({
    registrationLocation: t.list(t.String),
  }),
...

const options = [
  {
    fields: {
      registrationLocation: {
        label: 'Registration location *',
        factory: TcombMultiSelect,
        options: [
          { value: 'blabla..', text: 'blabla' },
          { value: 'blabla', text: 'blabla' },
        ],
      },
....

@yai333 I have used your code for multi select options. The field is mandatory like i.e. not t.maybe but t.list so it should give error when form.getvalue is called. Its not giving error. Any idea how I can override the validate method ? Can you please assist ?

Was this page helpful?
0 / 5 - 0 ratings