12

I have the following model for my React (TypeScript) app:

interface IProjectInput {
    id?: string;
    name: string | i18n;
    description: string | i18n;
}
export interface i18n {
    [key: string]: string;
}

I am using Formik and react-bootstrap to create a new ProjectInput from a Form:

import { i18n as I18n, ... } from 'my-models';

interface State {
    validated: boolean;
    project: IProjectInput;
}

/**
 * A Form that can can edit a project
 */
class ProjectForm extends Component<Props, State> {
    constructor(props: any) {
        super(props);
        this.state = {
            project: props.project || {
                name: {},
                description: ''
            },
            validated: false
        };
    }

    async handleSubmit(values: FormikValues, actions: FormikHelpers<IProjectInput>) {
        let project = new ProjectInput();
        project = { ...project, ...values };
        console.log("form values", values);
        // actions.setSubmitting(true);
        // try {
        //     await this.props.onSubmit(project);
        // } catch (e) { }
        // actions.setSubmitting(false);
    }

    render() {
        const { t } = this.props;
        const getCurrentLng = () => i18n.language || window.localStorage.i18nextLng || '';
        const init = this.state.project || {
            name: {},
            description: ''
        };

        return (
            <div>
                <Formik
                    // validationSchema={ProjectInputSchema}
                    enableReinitialize={false}
                    onSubmit={(values, actions) => this.handleSubmit(values, actions)}
                    initialValues={init}
                >
                    {({
                        handleSubmit,
                        handleChange,
                        handleBlur,
                        values,
                        touched,
                        errors,
                        isSubmitting,
                        setFieldTouched
                    }) => {

                        return (
                            <div className="project-form">
                                <Form noValidate onSubmit={handleSubmit}>
                                    <Form.Row>
                                        <Form.Group as={Col} md={{span: 5}} controlId="projectName">
                                            <Form.Label>
                                                {t('projectName')}
                                            </Form.Label>
                                            // Input for ENGLISH text
                                            <Form.Control
                                                type="text"
                                                name="name"
                                                value={(values['name'] as I18n).en}
                                                onChange={handleChange}
                                            />
                                            // Input for FRENCH text
                                            <Form.Control
                                                type="text"
                                                name="name"
                                                value={(values['name'] as I18n).fr}
                                                onChange={handleChange}
                                            />
                                        </Form.Group>

So in the end it should look like:

{
  "name": {
    "en": "yes",
    "fr": "oui"
  },
  "description" : "test",
  ...
}

My problem is, that the value for the name input stays empty.

I tried to add const init = this.state.project || { name: { 'en': '' }, in my render or for my state, but this did not do anything.

2 Answers 2

7

TL;DR

Change in your Form.Control the prop name to name.en/name.fr


First of all, initialValues is a prop that will be set and won't change unless you pass the prop enableReinitialize. So it isn't good to do this.state.project || { name: { 'en': '' } because it will only assume the first value of that, it can be this.state.project or { name: { 'en': '' }, but you will never know.

Second, to solve your problem, if you look at the docs about handleChange:

General input change event handler. This will update the values[key] where key is the event-emitting input's name attribute. If the name attribute is not present, handleChange will look for an input's id attribute. Note: "input" here means all HTML inputs.

But in your Form.Control you are passing the name attribute as name="name".

So it's trying to update name and not e.g. name.en.

You should change

   <Form.Control
        type="text"
        name="name"
        value={(values['name'] as I18n).en}
        onChange={handleChange}
    />
    // Input for FRENCH text
    <Form.Control
        type="text"
        name="name"
        value={(values['name'] as I18n).fr}
        onChange={handleChange}
    />

To

   <Form.Control
        type="text"
        name="name.en" // correct name
        value={(values['name'] as I18n).en}
        onChange={handleChange}
    />
    // Input for FRENCH text
    <Form.Control
        type="text"
        name="name.fr" // correct name
        value={(values['name'] as I18n).fr}
        onChange={handleChange}
    />

Here is the docs that shows why you should use name.en instead of just name.

Sign up to request clarification or add additional context in comments.

3 Comments

Sometimes I feel really dumb, like right now... THANK YOU @Vencovsky for taking your time and not only posting the correct answer but also a really good explanation! I read this document page but totally overlooked the namepart.
My last question for today will be, how I could declare name: {} to avoid getting Warning: A component is changing an uncontrolled input of type text to be controlled. I could do name: {'en':'','fr':''} everywhere but I do not want to hardcode it :) @Vencovsky
@mrks you need to hard code it. If the input on the page is hard coded, then you need to hard code it. If the input is conditionally rendered or rendered by .map and things like that, you should do the same in the name: {}. To avoid changing an uncontrolled input of type text to be controlled the input name rendered must be in the initialValues.
1

It described in docs https://formik.org/docs/guides/arrays

import React from 'react';
import { Formik, Form, Field } from 'formik';

export const NestedExample = () => (
  <div>
    <h1>Social Profiles</h1>
    <Formik
      initialValues={{
        social: {
          facebook: '',
          twitter: '',
        },
      }}
      onSubmit={values => {
        // same shape as initial values
        console.log(values);
      }}
    >
      <Form>
        <Field name="social.facebook" />
        <Field name="social.twitter" />
        <button type="submit">Submit</button>
      </Form>
    </Formik>
  </div>
);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.