A minimal way of implementing select options field in React Final Form (with example)

In my opinion, React final form is one of the most popular form libraries in ReactJS. It provides with different ways to implement different types of fields such as text input, textarea, radio button and select input fields.

The most common method to implement react-final-form is to use components (third party as well) in which you create custom input or select component and wrap them with <Field /> component provided by React Final Form they do the rest for you.

But it is always not that simple, when it comes to automatically validate the different types of fields and show error message beneath using {meta.error} property. If you have worked with react-final-form for sometime you may know what I mean.

In this post I am going to show an example of implementing select dropdown list in React Final form without any select or input components, and with simple <select><options></select> approach.

In fact one can use fancy select input components provided by React libraries such as reactstrap or react-select if your environment allowed you. But if you want it simple and do not want to use any third party component, then you could achieve desired results with a very minimal lines of code.

I will show you the example which I have used to implement my custom select options list while keeping the “value” chain and validations working as usual.

Before diving into the simple example lets start with an example of using react-select to create a fancy dropdown box. Note: the code below may not be complete and working as it is. It is just for general understanding.

...
import renderSelectField from '@/shared/components/form/Select'
....
const MyForm = () => {

    const onSubmit = (values) => {
        //do domething
    };

    return(
        <Form
            onSubmit={onSubmit}
            validate={validate}
        >
            {({ handleSubmit, form, submitting, pristine, values }) => (
            <form onSubmit={handleSubmit}>
                <Row>
                    <Col md="4">
                        <Label>Color</Label>
                    </Col>
                    <Col md="8">
                        <Field 
                                name="color"
                                className="react-select"
                                options={colorOptions}
                                placeholder={'Select Color'}
                                component={renderSelectField}
                        />
                    </Col>
                </Row>                                        
                <div className='d-flex justify-content-end'>
                    <Button disabled={loading} color='danger' type='submit'>{'Save'}</Button>
                </div>
            </form>
            )}
        </Form>
    )
}

const validate = values => {
    let errors = {}
    if( !values.color )   {
        errors.color = 'Please select color'
    }
    return errors
}

export default MyForm;

where Select.jsx is:

import React from 'react';
import Select from 'react-select';
import PropTypes from 'prop-types';
import { renderComponentField } from '@/shared/components/form/FormField';

export const SelectField = React.forwardRef(({
  onChange, value, name, placeholder, options,
}, ref) => {
  const handleChange = (selectedOption) => {
    onChange(selectedOption);
  };

  return (
    <Select
      name={name}
      value={value}
      onChange={handleChange}
      options={options}
      clearable={false}
      className="react-select"
      placeholder={placeholder}
      classNamePrefix="react-select"
      ref={ref}
    />
  );
});

SelectField.propTypes = {
  onChange: PropTypes.func.isRequired,
  name: PropTypes.string.isRequired,
  placeholder: PropTypes.string,
  options: PropTypes.arrayOf(PropTypes.shape({
    value: PropTypes.string,
    label: PropTypes.string,
  })),
  value: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.shape({
      value: PropTypes.string,
      label: PropTypes.string,
    }),
  ]).isRequired,
};

SelectField.defaultProps = {
  placeholder: '',
  options: [],
};

export default renderComponentField(SelectField);

And FormField.jsx is

import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import Error from '@/shared/components/form/Error';

const FormField = ({
  input, meta: { touched, error }, component: Component, isAboveError, wrapperClassName, ...props
}) => (
  <div className={classNames(
    'form__form-group-input-wrap',
    { 'form__form-group-input-wrap--error-above': isAboveError },
    wrapperClassName,
    )}
  >
    <Component {...props} {...input} />
    {touched && error && <Error error={error} />}
  </div>
);

FormField.propTypes = {
  input: PropTypes.shape(),
  meta: PropTypes.shape({
    touched: PropTypes.bool,
    error: PropTypes.string,
  }),
  component: PropTypes.elementType,
  isAboveError: PropTypes.bool,
  wrapperClassName: PropTypes.string,
};

FormField.defaultProps = {
  input: null,
  meta: {
    touched: false,
    error: '',
  },
  component: 'input',
  isAboveError: false,
  wrapperClassName: '',
};

export const renderComponentField = component => props => (<FormField component={component} {...props} />);

export default FormField;

Using the approach above one would see a beautiful looking dropdown

Minimal Approach

Now lets see the simple implementation with help of mutators provided by React-final-form

const MyForm = () => {

    const handleChange = ([field], state) => {
        const tfield = state.fields["color"];
        tfield.change(field.target.value)
    }

    const onSubmit = (values) => {
        //do domething
    };

    return(
        <Form
            onSubmit={onSubmit}
            validate={validate}
            mutators={{
                handleChange
            }}
        >
            {({ handleSubmit, form, submitting, pristine, values }) => (
            <form onSubmit={handleSubmit}>
                <Row>
                    <Col md="4">
                        <Label>Color</Label>
                    </Col>
                    <Col md="8">
                        <Field name="color">
                            {({ input, meta }) => (
                                <>
                                    <select onChange={form.mutators.handleChange}>
                                        <option value="">-- select --</option>
                                        <option value="#ff0000">❤️ Red</option>
                                        <option value="#00ff00">💚 Green</option>
                                        <option value="#0000ff">💙 Blue</option>
                                    </select>
                                    {meta.touched && meta.error && <span className="text-danger">
                                    {meta.error}
                                    </span>}
                                </>
                            )}
                        </Field>
                    </Col>
                </Row>                                        
                <div className='d-flex justify-content-end'>
                    <Button disabled={loading} color='danger' type='submit'>{'Save'}</Button>
                </div>
            </form>
            )}
        </Form>
    )
}

const validate = values => {
    let errors = {}
    if( !values.name )   {
        errors.name = 'Please enter leaderboard name'
    }
    if( !values.color )   {
        errors.color = 'Please select color'
    }
    return errors
}

export default MyForm;

You see how it looks!

What did we do?

In the <Form component it binds our custom method handleChange as a mutator(as shown below)

<Form onSubmit={onSubmit} validate={validate} mutators={{ handleChange }} >

And then we call it in the <select> input field as onChange event (as shown below)

<select onChange={form.mutators.handleChange}>

Rest of things are same and it works just as normal as it should.

What is the difference?

Although the first approach, that is working with react-select looks awesome sometimes you may be wanting to get just the simple dropdown working without needing any fancy stuff, so in that case second one can be handy.

Please let me know your thoughts on my post on react-final-form custom select options dropdown with validation.

Leave a Reply

Your email address will not be published.