Visit each child in props.children and trigger a function

Viewed 103

I want to be able to visit the children <Textfield> of my form <Form> upon submit. In each child hook object, I also want to trigger a certain function (eg., validate_field). Not sure if this possible in hooks? I do not want to use ref/useRef and forwardRef is a blurred concept to me yet (if that's of any help).

My scenario is the form has been submitted while the user did not touch/update any of the textfields so no errors were collected yet. Upon form submit, I want each child to validate itself based on certain constraints.

I tried looking at useImperativeHandle too but looks like this will not work on props.children?

Updated working code in: https://stackblitz.com/edit/react-ts-jfbetn


submit_form(evt){
  props.children.map(child=>{
    // hypothetical method i would like to trigger. 
    // this is what i want to achieve
    child.validate_field()  // this will return "is not a function" error
  })
}

<Form onSubmit={(e)=>submit_form(e)}
  <Textfield validations={['email']}>
  <Textfield />
  <Textfield />
</Form>

Form.js

function submit_form(event){
  event.preventDefault();
  if(props.onSubmit){
    props.onSubmit()
  }
}
export default function Form(props){
  return (
    <form onSubmit={(e)=>submit_form(e)}>
      {props.children}
    </form>
  )
}

So the Textfield would look like this

…
const [value, setValue] = useState(null);
const [errors, setErrors) = useState([]);

function validate_field(){
   let errors = []; // reset the error list 
   props.validations.map(validation => {
     if(validation === 'email'){
       if(!some_email_format_validator(value)){
         errors.push('Invalid email format')
       }
     }
     // other validations (eg., length, allowed characters, etc)
   })
   setErrors(errors)
}

export default function Textfield(props){
  render (
    <input onChange={(evt)=>setValue(evt.target.value)} />
    {
      errors.length > 0 
      ? errors.map(error => {
        return (
          <span style={{color:'red'}}>{error}</span>
        )
      })
      : null
    }
  )
}
1 Answers

I would recommend moving your validation logic up to the Form component and making your inputs controlled. This way you can manage the form state in the parent of the input fields and passing in their values and onChange function by mapping over your children with React.cloneElement.

I don't believe what you're trying to do will work because you are trying to map over the children prop which is not the same as mapping over say an array of instantiated child elements. That is to say they don't have state, so calling any method on them wouldn't be able to give you what you wanted.

You could use a complicated system of refs to keep the state in your child input elements, but I really don't recommend doing that as it would get hairy very fast and you can just solve the issue by moving state up to the parent.

simplified code with parent state:

const Form = ({ children }) => {
  const [formState, setFormState] = useState(children.reduce((prev, curr) => ({ ...prev, [curr.inputId]: '' }), {}));

  const validate = (inputValue, validator) => {}
  const onSubmit = () => {
    Object.entries(formState).forEach(([inputId, inputValue]) => {
      validate(
        inputValue,
        children.filter(c => c.inputId === inputId)[0].validator
      )
    })
  }

  const setFieldValue = (value, inputId) => {
    setFormState({ ...formState, [inputId]: value });
  };
  const childrenWithValues = children.map((child) =>
    React.cloneElement(child, {
      value: formState[child.inputId],
      onChange: (e) => {
        setFieldValue(e.target.value, child.inputId);
      },
    }),
  );

  return (
    <form onSubmit={onSubmit}>
      {...childrenWithValues}
    </form>
  )
};

const App = () => 
  <Form>
    <MyInput validator="email" inputId="foo"/>
    <MyInput validator="email" inputId="foo"/>
    <MyInput validator="password" inputId="foo"/>
  </Form>

I still don't love passing in the validator as a prop to the child, as pulling that out of filtered children is kinda jank. Might want to consider some sort of state management or pre-determined input list.

Related