Closed jferrettiboke closed 6 years ago
@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âd 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âs value has changed, regardless of whether or not I even use that fieldâs value to render my dependent component.
Yeah, everything depends on the scale, but for small to medium forms itâs easily sufficient.
Agreed, as long as render doesnât 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>
.
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?
it solves the simple case. but not good for complex case. if props A changed, after a calculate with B or/and C, change X, Y, Z. how to handle this?
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.