Ant design date and time pickers do not pass value through Formik (react)

12,349

Solution 1

Put simply, you'll need to utilize Ant Design's Form.Item inside of a Formik Field's component prop.

You'll be able to add other Antd form items as well, however, there are a few quirks. As such, I'd only recommend using one or the other (not both).

Working example: https://codesandbox.io/s/4x47oznvvx

components/AntFields.js (the reason behind creating two different onChange functions is because one of the ant components passes back an event (event.target.value) while the other passes back a value -- unfortunately, a quirk when using Formik with Antd)

import map from "lodash/map";
import React from "react";
import { DatePicker, Form, Input, TimePicker, Select } from "antd";

const FormItem = Form.Item;
const { Option } = Select;

const CreateAntField = Component => ({
  field,
  form,
  hasFeedback,
  label,
  selectOptions,
  submitCount,
  type,
  ...props
}) => {
  const touched = form.touched[field.name];
  const submitted = submitCount > 0;
  const hasError = form.errors[field.name];
  const submittedError = hasError && submitted;
  const touchedError = hasError && touched;
  const onInputChange = ({ target: { value } }) =>
    form.setFieldValue(field.name, value);
  const onChange = value => form.setFieldValue(field.name, value);
  const onBlur = () => form.setFieldTouched(field.name, true);
  return (
    <div className="field-container">
      <FormItem
        label={label}
        hasFeedback={
          (hasFeedback && submitted) || (hasFeedback && touched) ? true : false
        }
        help={submittedError || touchedError ? hasError : false}
        validateStatus={submittedError || touchedError ? "error" : "success"}
      >
        <Component
          {...field}
          {...props}
          onBlur={onBlur}
          onChange={type ? onInputChange : onChange}
        >
          {selectOptions &&
            map(selectOptions, name => <Option key={name}>{name}</Option>)}
        </Component>
      </FormItem>
    </div>
  );
};

export const AntSelect = CreateAntField(Select);
export const AntDatePicker = CreateAntField(DatePicker);
export const AntInput = CreateAntField(Input);
export const AntTimePicker = CreateAntField(TimePicker);

components/FieldFormats.js

export const dateFormat = "MM-DD-YYYY";
export const timeFormat = "HH:mm";

components/ValidateFields.js

import moment from "moment";
import { dateFormat } from "./FieldFormats";

export const validateDate = value => {
  let errors;

  if (!value) {
    errors = "Required!";
  } else if (
    moment(value).format(dateFormat) < moment(Date.now()).format(dateFormat)
  ) {
    errors = "Invalid date!";
  }

  return errors;
};

export const validateEmail = value => {
  let errors;

  if (!value) {
    errors = "Required!";
  } else if (!/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(value)) {
    errors = "Invalid email address!";
  }

  return errors;
};

export const isRequired = value => (!value ? "Required!" : "");

components/RenderBookingForm.js

import React from "react";
import { Form, Field } from "formik";
import { AntDatePicker, AntInput, AntSelect, AntTimePicker } from "./AntFields";
import { dateFormat, timeFormat } from "./FieldFormats";
import { validateDate, validateEmail, isRequired } from "./ValidateFields";

export default ({ handleSubmit, values, submitCount }) => (
  <Form className="form-container" onSubmit={handleSubmit}>
    <Field
      component={AntInput}
      name="email"
      type="email"
      label="Email"
      validate={validateEmail}
      submitCount={submitCount}
      hasFeedback
    />
    <Field
      component={AntDatePicker}
      name="bookingDate"
      label="Booking Date"
      defaultValue={values.bookingDate}
      format={dateFormat}
      validate={validateDate}
      submitCount={submitCount}
      hasFeedback
    />
    <Field
      component={AntTimePicker}
      name="bookingTime"
      label="Booking Time"
      defaultValue={values.bookingTime}
      format={timeFormat}
      hourStep={1}
      minuteStep={5}
      validate={isRequired}
      submitCount={submitCount}
      hasFeedback
    />
    <Field
      component={AntSelect}
      name="bookingClient"
      label="Client"
      defaultValue={values.bookingClient}
      selectOptions={values.selectOptions}
      validate={isRequired}
      submitCount={submitCount}
      tokenSeparators={[","]}
      style={{ width: 200 }}
      hasFeedback
    />
    <div className="submit-container">
      <button className="ant-btn ant-btn-primary" type="submit">
        Submit
      </button>
    </div>
  </Form>
);

components/BookingForm.js

import React, { PureComponent } from "react";
import { Formik } from "formik";
import RenderBookingForm from "./RenderBookingForm";
import { dateFormat, timeFormat } from "./FieldFormats";
import moment from "moment";

const initialValues = {
  bookingClient: "",
  bookingDate: moment(Date.now()),
  bookingTime: moment(Date.now()),
  selectOptions: ["Mark", "Bob", "Anthony"]
};

const handleSubmit = formProps => {
  const { bookingClient, bookingDate, bookingTime, email } = formProps;
  const selectedDate = moment(bookingDate).format(dateFormat);
  const selectedTime = moment(bookingTime).format(timeFormat);
  alert(
    `Email: ${email} \nSelected Date: ${selectedDate} \nSelected Time: ${selectedTime}\nSelected Client: ${bookingClient}`
  );
};

export default () => (
  <Formik
    initialValues={initialValues}
    onSubmit={handleSubmit}
    render={RenderBookingForm}
  />
);

Solution 2

I don't understand why it won't read that data?

Formik passes values as values prop, they are updated using setFieldValue. When you store values in the state, Formik does't know anything about it.

Of course there is nothing wrong in storing values to the state (assuming it works) but you have to define internal submit handler to attach these values to others. By simple calling prop:

onSubmit={handleSubmit}

you have no chance to do that. Only Formik handled values will be passed. You need to define internal submit handler, sth like:

const handleSubmit = values => {
  // init with other Formik fields
  let preparedValues = { ...values }; 

  // values from state
  const { startTime, startDate } = this.state; 

  // attach directly or format with moment
  preparedValues["startTime"] = startTime;
  preparedValues["startDate"] = startDate;

  // of course w/o formatting it can be done shorter
  // let preparedValues = { ...values, ...this.state }; 

  console.log(preparedValues);

  // call external handler with all values
  this.prop.handleSubmit( preparedValues );
}
Share:
12,349

Related videos on Youtube

Michael Emerson
Author by

Michael Emerson

Symfony2 and PHP developer at Media Orb in Bridgwater. Interested in photography and also code in my spare time :)

Updated on June 04, 2022

Comments

  • Michael Emerson
    Michael Emerson over 1 year

    I'm currently working on a booking form which is in React using Formik. I've also incorporated Ant Design's Date Picker and Time Picker for the booking date and time respectively, but I'm having difficulties getting the values to be passed back to the component.

    Here is how I've set it up in the form component (I've omitted the other unrelated fields):

    const { booking, handleSubmit, mode } = this.props;
    
    ...
    
    <Formik
        initialValues={booking}
        onSubmit={handleSubmit}
        render={({errors, touched, isSubmitting}) => (
            <Form>
            ...
    <div className="form-group col-sm-4 col-md-6 col-lg-4">
        <label htmlFor="booking_date">
            Booking Date <span className="required">*</span>
        </label>
        <DatePicker onChange={ (date, dateString) => setFieldValue('booking_date', dateString)} defaultValue={this.state.bookingDate}
            className="form-control" format={this.state.dateFormat} />
    </div>
    <div className="form-group col-sm-4 col-md-6 col-lg-4">
        <label htmlFor="start_time">
            Start Time <span className="required">*</span>
        </label>
        <TimePicker
            defaultValue={this.state.startTime}
            format={this.state.timeFormat}
            className="form-control"
            onChange={this.handleStartTimeChange}
            minuteStep={5}
            id="start_time"
            name="start_time"
        />
    </div>
    

    This is the function that handles the time change (just a state set):

    handleStartTimeChange(time) {
        this.setState({
            startTime: time
        });
    }
    

    And then on the parent, the component is set up like so:

    <BookingForm
        show={true}
        booking={null}
        handleSubmit={this.saveBooking.bind(this)}
        mode="add"
    />
    

    And the saveBooking function simply console logs the params out. However, it only ever logs out the other fields such as firstname, surname and email. The dates are completely overlooked and I don't know how to be able to get the form to recognise them - I even tried creating a Formik hidden field to replicate the date value when submit but it still ignores it. The field name and ID are correct, and correlate with the database as do all the others - so I don't understand why it won't read that data?

  • Michael Emerson
    Michael Emerson almost 5 years
    I know this was answered a while ago and it does work - but I have a question. I need to pass data through to the rendered form (namely a list of clients to be chosen from a select box) but since the RenderBookingForm is just an exported constant, how can I pass this data through as a prop and then reference it in the RenderBookingForm component? Is this possible?
  • Matt Carlotta
    Matt Carlotta almost 5 years
    See updated answer above (codesandbox is also updated). Just note that you'll have to come up with a strategy to create selectOptions. Whether it's by making BookingForm a stateful component that conditionally renders (reactjs.org/docs/conditional-rendering.html) a spinner while it fetches a client list in its componentDidMount method from an API. This API client list is then set to state, which then gets passed down to the form. Or, by setting selectOptions as a static array of strings (as shown above). Either way, it needs to be structured like: [ "opt1", "opt2" ... ]
  • Michael Emerson
    Michael Emerson almost 5 years
    Ah, I didn't think of passing it through to the initialValues! Yes, I will be retrieving the clients from an API call, so I just need to convert to an array instead of the object I would get normally, thank you!