Tcomb-form-native: show PickerIOS as textfield (or something else compact) when not choosing

Created on 19 Oct 2015  Â·  46Comments  Â·  Source: gcanti/tcomb-form-native

would be nice for it to open on click, close on choice made

Most helpful comment

Ok for future reference I got this working using this

AccordionPicker.js

  import React, { View, Text } from 'react-native'
  import t from 'tcomb-form-native'
  import Accordion from 'react-native-collapsible/Accordion'
  import moment from 'moment';

  const Date = t.form.DatePicker;

  class AccordionPicker extends Date {
    _renderHeader (locals) {

      let curSelectedDate = moment(locals.value).format('MM-DD-YYYY');

      return (
        <View style={[styles.header, styles.textContainer]}>
          <Text style={[locals.stylesheet.controlLabel.normal, styles.label]}>
            {locals.label}
          </Text>
          <Text style={[styles.value]}>
           {curSelectedDate}
         </Text>
        </View>
      )
    }

    _renderContent (locals) {
      return (
        <View>
          {t.form.Form.templates.datepicker({...locals, date: locals.value, mode:'date'})}
        </View>
      )
    }

    getTemplate () {
      var self = this
      return function (locals) {
        // console.log("locals: "+JSON.stringify(locals));
        return (
          <Accordion
            style={styles.container}
            sections={['Date']}
            renderHeader={self._renderHeader.bind(self, locals)}
            renderContent={self._renderContent.bind(self, locals)}
          />
        )
      }
    }
  }

  const styles = React.StyleSheet.create({
    container: {
      flex: 1,
      marginBottom: 10,
      // backgroundColor: '#f9f9f9',
    },
    textContainer: {
      flexDirection: 'column',
      // paddingLeft: 5,
      // paddingRight: 5,
    },
    label:{
      flex: 1,
      fontSize: 16,
      // backgroundColor: 'red'
    },
    value: {
      flex: 1,
      // backgroundColor:'red',
      fontSize: 16,
    },
    header: {
      flex: 1,
      height: 44,
      // backgroundColor: '#f9f9f9',
    },
  })

  export default AccordionPicker

this render() within my form Form.js

with a this.props.value initial value of new Date()

<Form ref="form"
    type={dateZipcodeForm}
    options={options}
    value={this.props.value}
    onChange={this.props.onChange}
  />

Special thanks to my brother @alvaromb and @gcanti for being patient with me.

All 46 comments

I have made a component that does exactly that, but uses a third-party library to collapse the picker. I can share it with you if you want, but I would need some more time to being able to include it under this library.

I would need some more time to being able to include it under this library

@alvaromb instead of including, the easiest way to extend the library is to wrap the third party component in a template in your own code:

// third party component
class MyDatePicker {

  render() {
    return <Text>Date picker...{String(this.props.value)}</Text>;
  }

}

...

// your code
function myTemplate(locals) {
  return <MyDatePicker value={locals.value} onChange={locals.onChange} ...other props />;
}

var Type = t.struct({
  birthDate: t.Date
});

var options = {
  fields: {
    birthDate: {
      template: myTemplate
    }
  }
};

Yes @gcanti, this is what I did. What I was thinking about was to bring a set of included templates into the library to let the user choose whatever they want. Something like a group of third party components.

This component provides a collapsible picker element using the react-native-collapsible component:

import React, { View, Text } from 'react-native'
import t from 'tcomb-form-native'
import Accordion from 'react-native-collapsible/Accordion'

const Select = t.form.Select

class AccordionPicker extends Select {
  _renderHeader (locals) {
    const valueText = locals.options.find(element => {
      return element.value === locals.value
    })
    let controlLabelStyle = locals.stylesheet.controlLabel.normal
    if (locals.hasError) {
      controlLabelStyle = locals.stylesheet.controlLabel.error
    }
    return (
      <View style={[styles.header, styles.textContainer]}>
        <Text style={[styles.label, controlLabelStyle]}>
          {locals.label}
        </Text>
        <Text style={[locals.stylesheet.controlLabel.normal, styles.value]}>
          {valueText.text}
        </Text>
      </View>
    )
  }

  _renderContent (locals) {
    return (
      <View>
        {t.form.Form.templates.select({...locals, label: null})}
      </View>
    )
  }

  getTemplate () {
    var self = this
    return function (locals) {
      return (
        <Accordion
          style={styles.container}
          sections={['Select']}
          renderHeader={self._renderHeader.bind(self, locals)}
          renderContent={self._renderContent.bind(self, locals)}
        />
      )
    }
  }
}

const styles = React.StyleSheet.create({
  container: {
    flex: 1,
    marginBottom: 10,
    backgroundColor: '#f9f9f9',
  },
  textContainer: {
    flexDirection: 'row',
    paddingLeft: 5,
    paddingRight: 5,
  },
  label: {
    fontSize: 16,
    alignSelf: 'center'
  },
  value: {
    flex: 1,
    fontSize: 16,
    textAlign: 'right',
    alignSelf: 'center',
    color: '#666'
  },
  header: {
    flex: 1,
    height: 44,
    backgroundColor: '#f9f9f9',
  },
})

export default AccordionPicker

A custom factory, nice!

@alvaromb thx, will give it a try!

Closing this. Let me know if you need something else.

@alvaromb : Thanks for the cool Components. Sorry I am newbie at tcomb form. How did to use your component? like this?

var options = {
  fields: {
    birthDate: {
      template: AccordionPicker
    }
  }
};

please give the example how to use it. Thanks

Yes, that's how you define a custom template, but in my example I'm using a custom factory, so it would be:

const options = {
  fields: {
    birthDate: {
      factory: AccordionPicker,
    },
  },
}

cool @alvaromb! working like a charm. You save my day. thanks

@alvaromb : I had testing that your solution works on t.enums but not working on t.Date. could you tell me why? I am using latest react-native @0.22.2

@alvaromb I'm very interested in that as well..!

I confirm that I can't get it to work with a t.Date but I could get it working with t.enums.. I don't really understand the code to change it myself though

Try to extend from t.form.Date instead of t.form.Select. You're trying to render a date component with a UIPicker instead of a UIDatePicker.

Enviado desde mi iPhone

El 31 mar 2016, a las 22:39, Ioannis Kokkinidis [email protected] escribió:

I confirm that I can't get it to work with a t.Date but I could get it working with t.enums.. I don't really understand the code to change it myself though

—
You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub

@SudoPlz try using t.form.DatePicker instead of t.form.Date, hence:

class AccordionPicker extends t.form.DatePicker {
  ....
}

You can find the default exported factories here:

https://github.com/gcanti/tcomb-form-native/blob/master/lib/components.js#L559

@SudoPlz yes, in your _renderContent you're doing this:

{t.form.Form.templates.select({...locals, label: null})}

It's not the select component, it should be the DatePicker.

To be clearer, the error message "Super expression must either be null or a function, not undefined" is issued because t.form.Date is undefined so you can't extend that, use t.form.DatePicker (camelcase) instead. The corresponding default template for a t.Date type is t.form.Form.templates.datepicker (lowercase).

Ok my DatePicker now looks like this:

import React, { View, Text } from 'react-native'
import t from 'tcomb-form-native'
import Accordion from 'react-native-collapsible/Accordion'

const Date = t.form.DatePicker;

class AccordionPicker extends Date {
  _renderHeader (locals) {
    const valueText = locals.options.find(element => {
      return element.value === locals.value
    })
    let controlLabelStyle = locals.stylesheet.controlLabel.normal
    if (locals.hasError) {
      controlLabelStyle = locals.stylesheet.controlLabel.error
    }
    return (
      <View style={[styles.header, styles.textContainer]}>
        <Text style={[styles.label, controlLabelStyle]}>
          {locals.label}
        </Text>
        <Text style={[locals.stylesheet.controlLabel.normal, styles.value]}>
          {valueText.text}
        </Text>
      </View>
    )
  }

  _renderContent (locals) {
    return (
      <View>
        {t.form.Form.templates.DatePicker({...locals, label: null})}
      </View>
    )
  }

  getTemplate () {
    var self = this
    return function (locals) {
      return (
        <Accordion
          style={styles.container}
          sections={['Date']}
          renderHeader={self._renderHeader.bind(self, locals)}
          renderContent={self._renderContent.bind(self, locals)}
        />
      )
    }
  }
}

const styles = React.StyleSheet.create({
  container: {
    flex: 1,
    marginBottom: 10,
    backgroundColor: '#f9f9f9',
  },
  textContainer: {
    flexDirection: 'row',
    paddingLeft: 5,
    paddingRight: 5,
  },
  label: {
    fontSize: 16,
    alignSelf: 'center'
  },
  value: {
    flex: 1,
    fontSize: 16,
    textAlign: 'right',
    alignSelf: 'center',
    color: '#666'
  },
  header: {
    flex: 1,
    height: 44,
    backgroundColor: '#f9f9f9',
  },
})

export default AccordionPicker

but I'm not getting an error in

   const valueText = locals.options.find(element => {
      return element.value === locals.value
    })

it says:

undefined is not an object (evaluating locals.options.find)

apparently theres no options property in there.

If I just give it a string like so

const valueText = "Value"

I still get

undefined is not a function (evaluating 

'_tcombFormNative2.default.form.Form.templates.DatePicker(babelHelpers.extends({},locals))')

The corresponding default template [...] is t.form.Form.templates.datepicker (lowercase)

You are still using t.form.Form.templates.DatePicker in your code

Don't want to be rude @SudoPlz, but you're copy&pasting a code that was used for some other component without even reading what the code is really doing. Keep in mind that we, the author of this library @gcanti and me as a collaborator, put a lot of effort into develop and maintain the code (sometimes not only at work, but during weekends and family time). Unfortunately we cannot code the solution for you, so take our advice and read carefully the code you're testing (it's pretty simple!).

The following code:

const valueText = locals.options.find(element => {
  return element.value === locals.value
})

Is only valid for enums, because it is trying to find the selected element into the possible values (locals.options).

What you need to do is to read the code and adapt it for your needs. In this case, _renderHeader should get the Date object and format it to display as a value.

PS: please follow Giulio advice too https://github.com/gcanti/tcomb-form-native/issues/61#issuecomment-204334062

@alvaromb You are right man, I was just trying to quickly find some ready code, without spending the time to learn how tcomb-form-native works on the background. I have no idea what locals is and how to get the date from it, so I was just trying to get it working without pain..

Ok I understand what the code does now, the only trouble I have is trying to figure how to instantiate the t.form.Form.templates.datepicker({...locals})

in:

  _renderContent (locals) {
    let date = new Date()
    return (
      <View>
        {t.form.Form.templates.datepicker({...locals, date: locals.value, mode:'date'})}
      </View>
    )
  }

I get undefined is not a function (evaluating props.date.getTime) or undefined is not an object (evaluating 'props.type') depending on what variation I try..

I guess it has something to do with the way I call the datepicker, but I'm not sure how to call it correctly. Does date and mode pass as properties within the datepicker template?

Apparently all it needed is to call my form with

value={new Date()} I will post the full code here for future reference once I get it working.

Ok for future reference I got this working using this

AccordionPicker.js

  import React, { View, Text } from 'react-native'
  import t from 'tcomb-form-native'
  import Accordion from 'react-native-collapsible/Accordion'
  import moment from 'moment';

  const Date = t.form.DatePicker;

  class AccordionPicker extends Date {
    _renderHeader (locals) {

      let curSelectedDate = moment(locals.value).format('MM-DD-YYYY');

      return (
        <View style={[styles.header, styles.textContainer]}>
          <Text style={[locals.stylesheet.controlLabel.normal, styles.label]}>
            {locals.label}
          </Text>
          <Text style={[styles.value]}>
           {curSelectedDate}
         </Text>
        </View>
      )
    }

    _renderContent (locals) {
      return (
        <View>
          {t.form.Form.templates.datepicker({...locals, date: locals.value, mode:'date'})}
        </View>
      )
    }

    getTemplate () {
      var self = this
      return function (locals) {
        // console.log("locals: "+JSON.stringify(locals));
        return (
          <Accordion
            style={styles.container}
            sections={['Date']}
            renderHeader={self._renderHeader.bind(self, locals)}
            renderContent={self._renderContent.bind(self, locals)}
          />
        )
      }
    }
  }

  const styles = React.StyleSheet.create({
    container: {
      flex: 1,
      marginBottom: 10,
      // backgroundColor: '#f9f9f9',
    },
    textContainer: {
      flexDirection: 'column',
      // paddingLeft: 5,
      // paddingRight: 5,
    },
    label:{
      flex: 1,
      fontSize: 16,
      // backgroundColor: 'red'
    },
    value: {
      flex: 1,
      // backgroundColor:'red',
      fontSize: 16,
    },
    header: {
      flex: 1,
      height: 44,
      // backgroundColor: '#f9f9f9',
    },
  })

  export default AccordionPicker

this render() within my form Form.js

with a this.props.value initial value of new Date()

<Form ref="form"
    type={dateZipcodeForm}
    options={options}
    value={this.props.value}
    onChange={this.props.onChange}
  />

Special thanks to my brother @alvaromb and @gcanti for being patient with me.

cool Thanks @SudoPlz . you are really saving my time. Thanks @alvaromb @gcanti for the explanation too.

@alvaromb I'm new to react and react native and looking at your code I can't quite tell how to pass the locals to your accordion picker:

var Condiments = t.enums({
      'ketchup': 'ketchup',
      'mustard': 'mustard'
    });



    var Order = t.struct({
      condiments: Condiments,    
    });

    var options = {
      fields: {
        condiments: {
          factory: AccordionPicker,
        }
      }
    };

    getInitialState: function() {
    return {
        form_fields: Order,
      form_values: {},
      options: options
    };
  },

    var Form = t.form.Form;
    <Form
    ref="form"
    type={this.state.form_fields}
    value={this.state.form_values}
    options={this.state.options} />

The error I get is:

Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object. Check the render method of Struct.

@SudoPlz trying your solution gives me an error:
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object. Check the render method ofComponent.
Have you encountered this?

@tharrington can you post your full source code?

@amirfl Not really, yeah just post the full code, where do you get this error?

@SudoPlz I Copy-pasted AccordionPicker.js file from your comment above.
Imported it:
var AccordionPicker = require('./../components/AccordionPicker');

Set it as factory in the options

var options = {
  fields: {
    startDate: {mode:"date", label:"Start Date", factory: AccordionPicker, },
  },
}; 

Set up a date in value state:

getInitialState:function(){
    return {
      isEditing: false,
      value:{
        startDate: (new Date()),
      },
    };
  },

And created a Form inside the render method of my component:

<Form 
              ref="form"
              type={Person}
              onChange={this.onChange}
              value={this.state.value}
              options={options}>
            </Form>

But still getting the error:
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object. Check the render method ofComponent.

You are passing a Person as a type, I think thats your problem.

What I'd do is I'd create a struct like so:

let startDateType = t.struct({
    startDate: t.Date    //the name here is the same as the property inside your fields object which for you is startDate
}

and the pass it on the type instead of the Person

<Form 
      ref="form"
      type={startDateType}
      onChange={this.onChange}
      value={this.state.value}
      options={options}>
  </Form>

p.s: t is 'tcomb-form-native'

Person is defined as a t.struct(), just like in your example.

var Person = t.struct({
  firstName: t.String,             
  middleName: t.maybe(t.String),
  lastName: t.String,  
  startDate:t.Date,
});

@amirfl can you post your full source code?

'use strict';
import React, { Component } from 'react';
import ReactNative,{
  StyleSheet,
  Text,
  View,
  ScrollView,
} from 'react-native';

var t = require('tcomb-form-native');
var AccordionPicker = require('./../components/AccordionPicker');
var Form = t.form.Form;
// define the form's domain model
var Person = t.struct({
  firstName: t.String, 
  startDate:t.Date,
});

// optional rendering options (see tcomb-form documentation)
var options = {
  fields: {
    firstName: {label:"First Name", placeholder:"Joe",},
    startDate: {mode:"date", label:"Start Date", factory: AccordionPicker,},
 },
}; 
module.exports = React.createClass({
  getInitialState:function(){
    return {
      isEditing: false,
      value: {
        firstName: "Joe",
        startDate: (new Date()),
      },
    };
  },

  render: function(){
    return (
      <View style={styles.container}>
        <ScrollView>
          <View style={formStyles.container}>
            <Form 
              ref="form"
              type={Person}
              onChange={this.onChange}
              value={this.state.value}
              options={options}>
            </Form>
          </View>
          </ScrollView>
      </View>
      );
  }
});

const styles = StyleSheet.create({
  container:{
    flex: 1,
  },
});

var formStyles = StyleSheet.create({
  container: {
    justifyContent: 'center',
    marginTop: 50,
    padding: 20,
    backgroundColor: '#ffffff',
  },
});

Try this (notice the ../../ vs ./../):

var AccordionPicker = require('../../components/AccordionPicker');

@alvaromb changing this line actually throws a 'unable to resolve module' error. The component is located in a sibling of the folder where the screen component is located so "./../" looks correct.
./App/components ->this is where AccordionPicker.js is located
./App/screens ->this is where the example file from above is located

Ok, so I think your problem is the following. Try this:

var AccordionPicker = require('./../components/AccordionPicker').default;

Thanks, I still had to use './..' to access the file, but when using '.default', I'm getting a new error:
_reactNative2.default.createElement is not a function. (In '_reactNative2.default.createElement', '_reactNative2.default.createElement' is undefined)

in AccordionPicker.js @38
line 38 is the beginning of the call to the accordion component:
<Accordion
So I'm checking now if its an issue with the collapsible module.

P.S
Sorry if its a dumb question, but what is '.default' doing?

Please, don't just copy&paste the code here. It's just for reference purpose and it's actually outdated, you have to import React from 'react' instead of react-native.

Now the error isn't related to tcomb-form-native or any of its components, it's just a pure React Native thing.

@alvaromb thank you.
Changing the begining of AccordionPicker.js to:

import React, { Component } from 'react';
import { View, Text, StyleSheet } from 'react-native'

and then the stylesheets to:
const styles = StyleSheet.create({ ...

removed the errors. However, the date picker doesnt appear when tapping the date field. Working on this now.

No errors, but can't get it to work quite yet.
This is how I was trying to troubleshoot it:
console.log("render contnent") inside _renderContent verifies that it is called when user taps the header, but it will not open the content section of the accordion.
I tried to put a different content (like Hello!) inside _renderContent, instead of the picker.
Tried increasing the style height, margins, and paddings, hoping that the content section is just hidden in the form, but the content will still not reveal itself when user taps the header.

It might be related to the collapsible library, but outside of a tcomb-form the accordion works as expected.

Thanks again for the help and for this great module!

EDIT:
This was a Style problem. removing flex:1 from the container style solved the issue.

Here is the updated AccordionPicker.js, if it helps anybody:

import React, { Component } from 'react';
import { View, Text, StyleSheet, DatePickerIOS } from 'react-native'
import t from 'tcomb-form-native'
import Accordion from 'react-native-collapsible/Accordion'
import moment from 'moment';

const Date = t.form.DatePicker;
function customDatePickerTemplate(locals) {
  var stylesheet = locals.stylesheet;
  var formGroupStyle = stylesheet.formGroup.normal;
  var controlLabelStyle = stylesheet.controlLabel.normal;
  var datepickerStyle = stylesheet.datepicker.normal;
  var helpBlockStyle = stylesheet.helpBlock.normal;
  var errorBlockStyle = stylesheet.errorBlock;

  if (locals.hasError) {
    formGroupStyle = stylesheet.formGroup.error;
    controlLabelStyle = stylesheet.controlLabel.error;
    datepickerStyle = stylesheet.datepicker.error;
    helpBlockStyle = stylesheet.helpBlock.error;
  }
  return (
    <View style={formGroupStyle}>
      <DatePickerIOS
        ref="input"
        maximumDate={locals.maximumDate}
        minimumDate={locals.minimumDate}
        minuteInterval={locals.minuteInterval}
        mode={locals.mode}
        timeZoneOffsetInMinutes={locals.timeZoneOffsetInMinutes}
        style={locals.datepickerStyle}
        onDateChange={(value) => locals.onChange(value)}
        date={locals.value}
      />
    </View>
  );
}
class AccordionPicker extends Date {

  _renderHeader (locals) {

    let curSelectedDate = moment(locals.value).format('MMM Do YYYY');

    return (
      <View style={[styles.header, styles.textContainer]}>
        <Text style={[locals.stylesheet.controlLabel.normal, styles.label]}>
          {locals.label}
        </Text>
        <Text style={[styles.value]}>
         {curSelectedDate}
       </Text>
      </View>
    )
  }

  _renderContent (locals) {
    return (
      <View style={styles.content}>
        {customDatePickerTemplate({...locals,date: locals.value, mode:'date'})}
      </View>
    )
  }

  getTemplate () {
    var self = this
    return function (locals) {
      // console.log("locals: "+JSON.stringify(locals));
      return (
        <Accordion
          style={styles.container}
          sections={['Date']}
          renderHeader={self._renderHeader.bind(self, locals)}
          renderContent={self._renderContent.bind(self, locals)}
        />
      )
    }
  }
}

const styles = StyleSheet.create({
  container: {

  },
  textContainer: {
  },
  content:{
  },
  label:{
    flex: 1,
    fontSize: 16,
  },
  value: {
    flex: 1,
    // backgroundColor:'red',
    fontSize: 16,
  },
  header: {
    flex: 1,
    //height: 44,
    backgroundColor: '#f9f9f9',
    padding: 10
  },
})

export default AccordionPicker

@gcanti this issue pops out very frequently, how do you feel about changing the Date component under iOS to handle a collapsible date picker? It would only affect the iOS implementation and in the end it would match better how the date picker is currently displayed under Android.

I have to implement this for one of our projects, so if you feel that's ok I can integrate it inside tcomb-form-native instead of building a separate factory.

@alvaromb No problem for me. If you go this route though, as a warm suggestion and speaking from my experience, make sure it's a bare working component if you want to avoid people opening issues about customisations, styling, etc... Let's make clear that it's just a default implementation.

When https://github.com/gcanti/tcomb-form-native/pull/167 gets merged, you'll have a working example of a collapsible date picker.

68559525

Source code here https://github.com/gcanti/tcomb-form-native/pull/167

Basically, you'll get a collapsible date picker out of the box under iOS, and it will be customizable through stylesheets. If it's not enough, you'll have a working example of a custom template.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

alexicum picture alexicum  Â·  6Comments

scarlac picture scarlac  Â·  4Comments

muthuraman007 picture muthuraman007  Â·  4Comments

ilyadoroshin picture ilyadoroshin  Â·  4Comments

flyingace picture flyingace  Â·  5Comments