Link Two Form Fields Together
There are times where you'll want to link two fields together; when one is validated another is as well. A prime example of this is when establishing password
and confirmpassword
fields, where you want to update the form's errors when password
is edited.
The first step to do this is to figure out how to cross-reference one field's value in another field's validation.
This can be done using the second argument of any Validate
function and utilizing thegetFieldValue
function:
import { Field } from "houseform";
const App = () => (
<Field
name="confirmpassword"
listenTo={["password"]}
onChangeValidate={(val, form) => {
if (val === form.getFieldValue("password")?.value) {
return Promise.resolve(true);
} else {
return Promise.reject("Passwords must match");
}
}}
>
{/* ... */}
</Field>
);
While this works, it introduces some edgecase bugs. Imagine the following userflow:
- User updates confirm password field.
- User updates the non-confirm password field.
In this example, the form will still have errors present, as the confirm password field validation has not been re-ran to mark as accepted.
To solve this, we need to make sure that the confirm password validation is re-ran when the password field is updated. To do this, you just need to add a listenTo
field to one of the two form fields.
import { Field } from "houseform";
const App = () => (
<>
<Field
name="password"
onChangeValidate={z.string().min(8, "Must be at least 8 characters long")}
>
{({ value, setValue, onBlur, errors }) => {
return (
<div>
<input
value={value}
onBlur={onBlur}
onChange={(e) => setValue(e.target.value)}
placeholder={"Password"}
type="password"
/>
{errors.map((error) => (
<p key={error}>{error}</p>
))}
</div>
);
}}
</Field>
<Field
name="confirmpassword"
listenTo={["password"]}
onChangeValidate={(val, form) => {
if (val === form.getFieldValue("password")?.value) {
return Promise.resolve(true);
} else {
return Promise.reject("Passwords must match");
}
}}
>
{({ value, setValue, onBlur, errors, isTouched }) => {
return (
<div>
<input
value={value}
onBlur={onBlur}
onChange={(e) => setValue(e.target.value)}
placeholder={"Password Confirmation"}
type="password"
/>
{isTouched && errors.map((error) => <p key={error}>{error}</p>)}
</div>
);
}}
</Field>
</>
);
This listenTo
field expects the name of another field to be passed to it as an array. When the listened to field has events ran on it (say, onBlur
), it will run the respective validation for the field doing the listening as well.
Conditional Fields
Similarly, you may have instances where you want to show or hide a field based on the value of another field. This can be done using a form's value
property:
import { Form, Field } from "houseform";
function App() {
return (
<Form
onSubmit={(values) => {
alert("Form was submitted with: " + JSON.stringify(values));
}}
>
{({ isValid, submit, value: formValue }) => {
// On first render, `value` is an empty object, since the `email` field is not yet initialized.
const isGmail = formValue.email?.endsWith("@gmail.com");
return (
<form
onSubmit={(e) => {
e.preventDefault();
submit();
}}
>
<Field
name="email"
initialValue={"test@gmail.com"}
onBlurValidate={z.string().email("This must be an email")}
>
{({ value, setValue, onBlur, errors }) => {
return (
<div>
<input
value={value}
onBlur={onBlur}
onChange={(e) => setValue(e.target.value)}
placeholder={"Email"}
/>
{errors.map((error) => (
<p key={error}>{error}</p>
))}
</div>
);
}}
</Field>
{isGmail && (
<p style={{ color: "red" }}>
We don't currently support gmail as an email host
</p>
)}
{!isGmail && (
<Field
name="password"
onChangeValidate={z
.string()
.min(8, "Must be at least 8 characters long")}
>
{({ value, setValue, onBlur, errors }) => {
return (
<div>
<input
value={value}
onBlur={onBlur}
onChange={(e) => setValue(e.target.value)}
placeholder={"Password"}
type="password"
/>
{errors.map((error) => (
<p key={error}>{error}</p>
))}
</div>
);
}}
</Field>
)}
<button disabled={!isValid || isGmail} type="submit">
Submit
</button>
</form>
);
}}
</Form>
);
}