Taming FormsWithout tearsSibelius Seraphini
Sibelius Seraphini
@sibelius
@sseraphini
Abstract Engineer

Overview

  • Forms
  • Fields
  • Validation

What is a Form

Form enables users to input data that will be saved locally or sent to a server

A Form can have one or more fields.

Form

What is a Field

A Field receives a single value of a Form.

A Field can be a `checkbox`, `textfield`, `radio button` and so on.

Field Meta (State)

  • A field have a value
  • A field can have a validation (it should be an email)
  • A field can have an error
  • A field can have a touched state

Field

Why Forms are hard?

  • It needs to control many fields state (value, error, touched, validation)
  • It can have validation per field, but also conditional validation
  • Fields can show and hide based on form state
  • A field can have a touched state
  • It can have multiple steps

Form useState

const FormUseState = () => {
const mapPropsToValues = () => {
return {
email: '',
password: '',
};
};
const [formState, setFormState] =
useState(mapPropsToValues());
const handleSubmit = () => {
console.log('submit: ', values);
};
const setFieldValue = (
fieldName: string,
value
) => {
setFormState({
...formState,
[fieldName]: value,
});
};
return (
<>
<TextField
placeholder='email'
mb={20}
value={formState.email}
onChange={(e) =>
setFieldValue(
'email',
e.target.value
)}
/>
<TextField
type='password'
placeholder='password'
mb={20}
value={formState.password}
onChange={(e) =>
setFieldValue(
'password',
e.target.value
)}
/>
<Button
type='submit'
onClick={handleSubmit}
>
Submit
</Button>
</Flex>
)
};

Form useState

Problems with useState approach

  • It does not handle touched and error per field
  • It does not have validation
  • It does not block submit if any field is invalid
  • It need to "bind" state and field value handlers for each field

Form useFormik

const FormUseFormik = (props: Props) => {
const onSubmit = (values) => {
console.log('submit: ', values);
};
const formikbag = useFormik({
initialValues: {
email: '',
password: '',
},
onSubmit,
});
const {
values,
setFieldValue,
handleSubmit
} = formikbag;
return (
<FormikProvider value={formikbag}>
<TextField
placeholder='email'
mb={20}
value={values.email}
onChange={(e) =>
setFieldValue(
'email',
e.target.value
)}
/>
<TextField
type='password'
placeholder='password'
mb={20}
value={values.password}
onChange={(e) =>
setFieldValue(
'password',
e.target.value
)}
/>
<Button
type='submit'
onClick={handleSubmit}
>
Submit
</Button>
</FormikProvider>
)
};

Form useFormik

Why do we need a Provider?

  • A provider "provide" values/functions to a given tree of components
  • We add Form values to a provider so our Fields can consume them directly from context

Form useField

const TextFieldFormik = (props) => {
const [field, meta] = useField(props.name);
return (
<TextField
{...props}
{...field}
/>
);
};
const FormUseFormik = (props: Props) => {
const onSubmit = (values) => {
console.log('submit: ', values);
};
const formikbag = useFormik<Values>({
initialValues: {
email: '',
password: '',
},
onSubmit,
});
const { handleSubmit } = formikbag;
return (
<FormikProvider value={formikbag}>
<TextFieldFormik
name='email'
placeholder='email'
mb={20}
/>
<TextFieldFormik
name='password'
type='password'
placeholder='password'
mb={20}
/>
<Button
type='submit'
onClick={handleSubmit}
>
Submit
</Button>
</FormikProvider>
)
};
export default FormUseFormik;

Form useField

Form Yup Validation

const TextFieldFormik = (props) => {
const [field, meta] = useField(props.name);
return (
<>
<TextField
{...props}
{...field}
/>
<ErrorMessage name={props.name} />
</>
);
};
const FormUseFormik = (props: Props) => {
const onSubmit = (values) => {
console.log('submit: ', values);
};
const validationSchema = yup.object().shape({
email: yup.string().email()
.required('Email is required'),
password: yup.string()
.required('Password is required'),
});
const formikbag = useFormik<Values>({
initialValues: {
email: '',
password: '',
},
validationSchema,
onSubmit,
});
const { handleSubmit } = formikbag;
return (
<FormikProvider value={formikbag}>
<Flex flexDirection='column'>
<TextFieldFormik
name='email'
placeholder='email'
mb={20}
/>
<TextFieldFormik
name='password'
type='password'
placeholder='password'
mb={20}
/>
</Flex>
<Button
type='submit'
onClick={handleSubmit}
>
Submit
</Button>
</FormikProvider>
)
};

Form Yup

usePrompt

export const usePrompt = (
when: boolean,
message: string,
) => {
const { history } = useRouter();
const self = useRef(null);
const onWindowOrTabClose = event => {
if (!when) {
return;
}
// eslint-disable-next-line
if (typeof event == 'undefined') {
event = window.event;
}
if (event) {
event.returnValue = message;
}
return message;
};
useEffect(() => {
if (when) {
self.current = history.block(message);
} else {
self.current = null;
}
window.addEventListener(
'beforeunload',
onWindowOrTabClose
);
return () => {
if (self.current) {
self.current();
self.current = null;
}
window.removeEventListener(
'beforeunload',
onWindowOrTabClose
);
};
}, [message, when]);
};

usePrompt

usePrompt usage

const FormUseFormik = (props: Props) => {
const formikbag = useFormik({
initialValues: {
email: '',
password: '',
},
onSubmit,
});
usePrompt(
formikbag.dirty,
'Are you sure to leave this form?'
);
};

usePrompt usage

What's Next?

  • Form Builder
  • Conditional fields
  • Complex Form Validation
  • Multi Step Forms
  • Typesafe Forms

References

  • Formik
  • Forms Examples of this Talk
  • Form Builder
  • ReForm
Thanks!
We are hiring!
https://entria.contrata.vc
Give me a Feedback:
https://entria.feedback.house/sibelius