react-native-advanced-forms

Flexible React Native architecture for building and managing forms.

react-native-advanced-forms

Features:

  • Cross-platform (iOS, Android)
  • Allow for flexible form layouts
  • Easily manage and validate field input values
  • Auto-focussing of next field for better user experience
  • Easily integrate your own custom components
  • Component-based - use with or without Redux
  • Compatible with React Native 0.40+
  • Successfully used in production apps

Installation

Use NPM/Yarn to install package: react-native-advanced-forms

Note: You must also have the prop-types package installed.

npm install

Demo

A working demo app can be found in the demo folder (Note that you will need
to copy src to demo/src for the app to successfully build).

Usage

This code will render a form similar to what can be seen in the demo animation above:

import React from 'react'
import { Button, Alert, StyleSheet, Text, View } from 'react-native'
import Form from 'react-native-advanced-forms'

export default class App extends React.Component {
  constructor (props, ctx) {
    super(props, ctx)

    this.state = {
      firstName: null,
      lastName: null,
      age: null,
      country: null,
    }
  }

  render() {
    const {
      firstName, lastName, age, country
    } = this.state

    return (
      <View style={styles.container}>
        <Form ref={this._onFormRef} onChange={this.onChange} onSubmit={this.onSubmit} validate={this.validate}>
          <Form.Layout style={styles.row}>
            <Form.Layout style={styles.columns}>
              <Form.Field name="firstName" label="First name" style={styles.field}>
                <Form.TextField value={firstName} />
              </Form.Field>
              <Form.Field name="lastName" label="Last name" style={styles.field}>
                <Form.TextField value={lastName} />
              </Form.Field>
            </Form.Layout>
          </Form.Layout>
          <Form.Layout style={styles.row}>
            <Form.Field name="age" label="Age" style={styles.ageField}>
              <Form.TextField value={age} keyboardType='numeric'/>
            </Form.Field>
          </Form.Layout>
          <Form.Layout style={styles.row}>
            <Form.Field name="country" label="Country" style={styles.field}>
              <Form.TextField value={country} />
            </Form.Field>
          </Form.Layout>
        </Form>
        <View style={styles.button}>
          <Button
            disabled={this.form && !this.form.canSubmit()}
            onPress={() => this.form.validateAndSubmit()}
            title="Submit"
            color="#841584"
          />
        </View>
      </View>
    )
  }

  _onFormRef = e => {
    this.form = e
  }

  onChange = (values) => {
    this.setState(values)
  }

  onSubmit = (values) => {
    Alert.alert('Submitted: ' + JSON.stringify(values))
  }

  validate = (values) => {
    const ret = Object.keys(this.state).reduce((m, v) => {
      if (!values[v] || !values[v].length) {
        m[v] = Form.VALIDATION_RESULT.MISSING
      }
      return m
    }, {})

    if (!ret.age && isNaN(values.age)) {
      ret.age = Form.VALIDATION_RESULT.INCORRECT
    }

    return ret
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    paddingTop: 100,
    paddingHorizontal: 30
  },
  row: {
    marginBottom: 20,
  },
  columns: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'flex-start',
  },
  field: {
    marginRight: 10,
  },
  ageField: {
    width: 60,
  },
  button: {
    width: 80,
    marginTop: 15,
  },
  error: {
    marginTop: 10,
  },
  errorMsg: {
    color: 'red'
  }
})

API and props

Form

This is the root component for a form and is responsible for co-ordinating
form value changes and auto-focussing components.

Properties:

Prop Type Default Description
submitOnReturn boolean true If turned off then the form will NOT be auto-submitted when the user presses the Return key after filling in the final field
onChange function (Object values) required Called whenever form values change
onSubmit function (Object values) required Called form values have passed validation and form is to be submitted
validate function (Object values) required Called to validate form values. Must return an Object mapping field name to validation error. If it returns {} then it means all fields are valid.
onValidationError function (Array badFieldNames) null Called when validation fails.
onFocusField function (String fieldName, Function callback) null Called when form is about to auto-focus a field. The callback must be invoked for focussing to proceed.
style Any null Styling to apply to form container element.

Methods:

Method Returns Description
validateAndSubmit undefined Validate the form field values and submit it if validation succeeds.
validate undefined Validate the form field values and highlight errors if any. Returns true if validation passed.
unfocus undefined Unfocus all form fields.
getValues Object Get current form field values.
canSubmit boolean Get whether current form field values are valid such that form can be submitted.

Form.Layout

This component works similarly to React Native View and is to be used to create whatever type of layout your require for your form. Form.Layout instances can be nested within each other to multiple levels without issue.

Properties:

Prop Type Default Description
style Any null Styling to apply to form container element.

Form.Section

This is a convenience component which constructs a Form.Layout to wrap its children but additionally attaches text above it:

section

Properties:

Prop Type Default Description
title String null The title text to show.
style Any null Style to apply to root container.
layoutStyle Any null Style to apply to nested Form.Layout.
titleTextStyle Any null Style to apply to title text, is shown.

Form.Field

This component must wrap every actual input element. It is responsible for setting up onChange and onSubmit handlers as well as pass through focus/unfocus and error display commands from the parent form.

If a label gets passed in it will use Form.LabelGroup and Form.Label to render a label above the wrapped form field component.

Prop Type Default Description
name String required The field name.
label String null The field label.
labelStyle Any null The field label container style.
labelTextStyle Any null The field label text style.
labelRightContent Any null What to show in the right-hand side of the field label container (using Form.LabelGroup).
style Any null The style for the root container.
onSubmit Function null Usually set by the parent Form.
onChange Function null Usually set by the parent Form.

Form.TextField

A text field designed to work well with a Form. It handles validation error display and scroll view integration.

Prop Type Default Description
value Any null The field value.
error Any null The current field validation error. If set then the field is in "error" mode.
style Any null The style for the text field in non-error mode.
errorStyle Any null The style for the text field in error mode.
getParentScrollView Function null Function to get reference to scroll view further up the UI hierarchy (if any). This is used to auto-scroll the scroll view such that the text field is visible whenever it receives focus.
onSubmit Function null Usually set by the parent Form.Field.
onChange Function null Usually set by the parent Form.Field.

Methods:

Method Returns Description
focus undefined Focus this field (should show the keyboard).
unfocus undefined Unfocus this field (should hide the keyboard).
getValue Any Get current field value (will return this.props.values).

Form.Label

Text to display above a form field element as its label. Gets automatically rendered by Form.Field if a label has been set.

Prop Type Default Description
style Any null The style for the root container.
textStyle Any null The style for the label text.

Form.LabelGroup

A helper component which wraps a Form.Label, allowing for additional components to be displayed alongside it.

Flexible layouts

The Form.Layout component is key to achieving flexible layouts. By default, if you don't use Form.Layout then components will be stacked on top of each other (assuming default flexDirection for the Form):

<Form onChange={this.onChange} onSubmit={this.onSubmit} validate={this.validate}>
  <Form.Field name="firstName">
    <Form.TextField value={firstName} />
  </Form.Field>
  <Form.Field name="lastName">
    <Form.TextField value={lastName} />
  </Form.Field>
  <Form.Field name="age">
    <Form.TextField value={age} />
  </Form.Field>
</Form>

If you wish to place the first two components next to each other then simply wrap them within a Form.Layout with the appropriate styling:

<Form onChange={this.onChange} onSubmit={this.onSubmit} validate={this.validate}>

  <Form.Layout style={{ flexDirection: 'row' }}>
    <Form.Field name="firstName">
      <Form.TextField value={firstName} />
    </Form.Field>
    <Form.Field name="lastName">
      <Form.TextField value={lastName} />
    </Form.Field>
  </Form.Layout>

  <Form.Field name="age">
    <Form.TextField value={age} />
  </Form.Field>

</Form>

And you can nest layouts:

<Form onChange={this.onChange} onSubmit={this.onSubmit} validate={this.validate}>

  <Form.Layout style={{ marginBottom: 10 }}>
    <Form.Layout style={{ flexDirection: 'row' }}>
      <Form.Field name="firstName">
        <Form.TextField value={firstName} />
      </Form.Field>
      <Form.Field name="lastName">
        <Form.TextField value={lastName} />
      </Form.Field>
    </Form.Layout>
  </Form.Layout>

  <Form.Layout style={{ marginBottom: 10 }}>
    <Form.Field name="age">
      <Form.TextField value={age} />
    </Form.Field>
  </Form.Layout>

</Form>

Note: A working example of the above can be found in the demo folder.

Form validation and submission

You must provide a validate property to the form, the value of which is a
function which returns which fields have failed validation, for example:

render () {
  return (
    <Form onSubmit={this.onSubmit} validate={this.validate} ...>
      ...
    </Form>
  )
}

validate = (values) => {
  const ret = {}

  if (!values.firstName) {
    ret.firstName = Form.VALIDATION_RESULT.MISSING
  }

  if (isNaN(values.age)) {
    ret.age = Form.VALIDATION_RESULT.INCORRECT
  }

  return ret
}

onSubmit = (values) = {
  ...
}

Note: The default VALIDATION_RESULT values can be found in src/utils.js:

If all fields have valid values then validate() must return {}. If
validation thus succeeds the form will be submitted and the onSubmit
handler which you passed in will be called with the values.

If you try submit the form programmatically (by calling validateAndSubmit())
and some fields have not yet been filled in, then they will be highlighted. On
the other hand, if you submit a
field (i.e. you enter text and then press done or the equivalent on your
keyboard) then the form logic will auto-focus on the next field that needs to
be filled in:

Demo2

Note: Form.Field components will pass an error object down to their children (the actual input elements) if a validation error occurs.

Custom components

You can integrate your own custom form component into a Form, as long as you follow certain rules:

  1. Your component must expose focus(), unfocus() and getValue() methods, just like as Form.TextField does.
  2. Your component must call this.props.onChange() when its value changes.
  3. Your component must call this.props.onSubmit() when it gets "submitted", e.g. for text fields the user may be press Done on the keyboard to submit the input.

Let's say we wish to integrate the React Native Switch component. Here is how we might define it:

import { Switch } from 'react-native'

class CustomSwitch extends Component {
  render () {
    const { turnedOn } = this.props

    return (
      <Switch
        value={turnedOn}
        onValueChange={this.onPress}
      />
    )
  }

  onPress = () => {
    this.props.onChange(!this.props.turnedOn)
  }

  getValue () {
    return this.props.turnedOn
  }

  // still need these methods even if they're empty
  focus () { }
  unfocus () {}
}

In our form code we would then use it as such:

const { name, isMale } = this.state

<Form onChange={this.onChange} onSubmit={this.onSubmit} validate={this.validate}>
  <Form.Field name="name">
    <Form.TextField value={name} />
  </Form.Field>
  <Form.Field name="isMale">
    <CustomSwitch turnedOn={isMale} />
  </Form.Field>
</Form>

Your component will receive an error prop containing a validation error whenever validation fails. It is up to you if/when you make use of this.

Note: There is a working example of a custom component (a dropdown using react-native-modal-filter-picker) in the demo folder.

Auto-scrolling to fields (ScrollView)

Sometimes you will need to place your form within a React Native ScrollView because it is longer than the height of the screen. In such cases the problem with auto-focussing fields is that the focussed field may not be visible in the current scroll area.

Thus, when a field receives focus it must be able to tell the ScrollView to scroll to it such that it's visible. The Form.TextField component already does this, as long as you pass in the getParentScrollView prop:

<ScrollView ref={e => { this.scrollView = e; }}>
  <Form onChange={this.onChange} onSubmit={this.onSubmit} validate={this.validate}>

    <Form.Layout>{ /* other stuff here */ }</Form.Layout>

    <Form.Field name="name">
      <Form.TextField value={name} getParentScrollView={() => this.scrollView} />
    </Form.Field>

    <Form.Layout>{ /* other stuff here */ }</Form.Layout>

    <Form.Field name="age">
      <Form.TextField value={age} getParentScrollView={() => this.scrollView} />
    </Form.Field>

    <Form.Layout>{ /* other stuff here */ }</Form.Layout>

  </Form>
</ScrollView>

Internally, Form.TextField uses Form.utils.scrollToComponentInScrollView() to actually perform the scroll. When building your own form component you can use this method too to achieve the same effect.

GitHub