Hi,
I am having an issue with chained selects. Is there any way to change values of other fields according to another field's value?
I created a sandbox so that it is easy to look at. If you select a currency from first select, let's say "usd", the country select doesn't select any option. As you can see in the state of the form, "country" value is still "IE" (the previous value).
I would like to select the first country available for a given currency. So, I thought removing <option /> it would work but it doesn't. Any idea how to solve this without wrapping the form and send data to its parent component? Is there any way to solve this just using RFF?
Thanks.
@jferrettiboke I've been struggling with this one recently too, and here's the approach that I'm currently taking:
const isString = (v) => typeof v === 'string'
// function keyword required for recursion
function DependentField({
parentFields,
...remainingProps
}) {
if (!parentFields?.length) {
return <Field {...remainingProps} />
}
const [nextParent, ...remainingParents] = parentFields
const parentName = isString(nextParent) ? nextParent : nextParent.parentName
// If injectedProp not specified, use parentName
const injectedProp = isString(nextParent) ?
nextParent :
nextParent.injectedProp || parentName
return (
<Field
name={parentName}
subscription={{value: true}}
render={({input: {value}}) => {
const renderProps = {
...remainingProps,
[injectedProp]: value,
}
return (
<DependentField parentFields={remainingParents} {...renderProps} />
)
}}
/>
)
}
If any listed parent field changes, then the changed value is passed to the child as an updated prop. It can be used like so:
const MyDependentField = () => (
<DependentField
parentFields=['parentField1', { parentName: 'parentField2', injectedProp: 'parentFieldTwo' }]
name='myDependentField'
>
{({ input, meta, parentField1, parentFieldTwo}) => (
{/* render whatever you want here */}
)}
</DependentField>
)
I wish there were a nicer way of doing this out of the box or via an extension, but the DependentField approach here has worked for me nicely.
A few related issues:
https://github.com/final-form/react-final-form/issues/297
https://github.com/final-form/react-final-form/issues/273
EDIT: added support for injectedProp for use in field arrays
We use a FormSpy component to do for example something like this:
<Field name="first">
{ /* ... */ }
</Field>
<FormSpy
subscription={{ /* ... */ }}
render={({ form }) => (
<Field name="second">
{ /* you can do something like this here: */ }
{({ input }) => (
<Input {...input} enabled={form.getFieldState('first').valid} />
)}
</Field>
)}
/>
In short: with FormSpy you get the FormApi object to interact with the form, for example, use getFieldState method.
My challenge with FormSpy is the subscription prop. In the above case, you鈥檇 need to subscribe to {{ values: true }} at the very least for this component to render appropriately.
When subscribed to values, the FormSpy component will trigger a rerender if any field鈥檚 value has changed, regardless of whether or not I even use that field鈥檚 value to render my dependent component.
Yeah, everything depends on the scale, but for small to medium forms it鈥檚 easily sufficient.
Agreed, as long as render doesn鈥檛 trigger unnecessary side effects, it should be fine for smaller forms (e.g. a dropdown that depends on another field and a server response should cache the server responses appropriately).
It just seems counter to the design of this library to subscribe to more fields than necessary.
As you can see in the state of the form, "country" value is still "IE" (the previous value).
Seems like this could be handled with a Declarative Form Rule, no?
Thanks @erikras. This seems to be the solution.
:man_facepalming: can't believe I didn't see that before. thanks @erikras
In case someone is interested how I solved my needs, check this sandbox out.
/*
For use inside a react-final-form context - change a field based on another field.
Example: Change everytime the other field changes
<WhenFieldChanges
field='department'
set='subDepartment'
to={-1}
/>
Example: Only change if `shouldChangeHandler` condition is true
<WhenFieldChanges
field='department'
set='subDepartment'
shouldChangeHandler=(department => {
if (department === -1) return true
else return false
}}
to={-1}
/>
*/
import React from 'react'
import PropTypes from 'prop-types'
import { Field, useFormState } from 'react-final-form'
import { OnChange } from 'react-final-form-listeners'
const WhenFieldChanges = ({ shouldChangeHandler, field, set, to }) => {
const { values } = useFormState()
return (
<Field name={set} subscription={{}}>
{(
// No subscription. We only use Field to get to the change function
{ input: { onChange } },
) => (
<OnChange name={field}>
{() => {
if (shouldChangeHandler && shouldChangeHandler(values[field]))
onChange(to)
else onChange(to)
}}
</OnChange>
)}
</Field>
)
}
WhenFieldChanges.propTypes = {
field: PropTypes.string.isRequired,
set: PropTypes.string.isRequired,
shouldChangeHandler: PropTypes.func,
to: PropTypes.any.isRequired,
}
WhenFieldChanges.defaultProps = {}
export default WhenFieldChanges
hey @engineforce since this is a hot topic and I don't think the previous solution is very neat (it seems a lot of boilerplate to me), can you share a solution using the hook?
@dbertella, turn out the solution posted by jferrettiboke is more declarative than mine and quite clean. You can find my solution using form.change (not hook, my mistake) at my sandbox.
I actually found out myself how to do this using hooks and it looks very easy, maybe less polish but very neat, this is an example:
const FormComponent = ({handleSubmit}) => {
const input1Field = useField('input1')
const input2Field = useField('input1')
return (
<form onSubmit={handleSubmit}>
<input
{...input1Field.input}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
input2Field.input.onChange(e.target.value)
}}
/>
<input {...input21Field.input} />
</form>
)
}
Super easy actually and it works quite well!
what do you think about it?
(I didn't test this particular code but it should work more or less, I can make a sandbox perhaps)
EDIT: forked your example above using hooks (thanks for the tip again!)
https://codesandbox.io/s/react-final-form-issue-348b-ixj9n
I like crobinson42's solution quite a bit, but it does have two bugs:
1) onChange will always fire, even if shouldChangeHandler returns false.
2) values[field] assumes values is a flat object.
I went with a patched version that also uses form.change:
import React from "react";
import PropTypes from "prop-types";
import { useForm, useFormState } from "react-final-form";
import { OnChange } from "react-final-form-listeners";
import get from "lodash/get";
const WhenFieldChanges = ({ shouldChangeHandler, field, set, to }) => {
const { values } = useFormState();
const form = useForm();
return (
<OnChange name={field}>
{() => {
if (shouldChangeHandler)
shouldChangeHandler(get(values, field)) &&
form.change(set, to);
else form.change(set, to);
}}
</OnChange>
);
};
WhenFieldChanges.propTypes = {
field: PropTypes.string.isRequired,
set: PropTypes.string.isRequired,
shouldChangeHandler: PropTypes.func,
to: PropTypes.any.isRequired
};
WhenFieldChanges.defaultProps = {};
export default WhenFieldChanges;
@erikras I'm migrating an old codebase from redux-form and I was using a custom hook to get the value of a specific field and conditionally render some elements based on that. Basically similar to the old formValues HoC but less tedious. Using a declarative form rule wouldn't cut it in my case. And using values from FormRenderProps isn't ideal because you can't subscribe to just one field value in <Form>.
I tried doing const {input: {value}} = useField(name) but that doesn't seem to work and probably will cause problems since it calls registerField without any validation etc. unlike the main <Field> for that name.
A lot of people would find a useFieldValue hook a welcome addition, I assure you. There needs to be a way to subscribe to only the value of a single field without registering all the stuff that comes along with <Field>.
Most helpful comment
Seems like this could be handled with a Declarative Form Rule, no?