T
TanStack2mo ago
other-emerald

Use app field components inside another field component

Hi there. I'm stuck in form composition. I'm building a form where I definitely need form composition - it's where I'm struggling. Here, a component with withForm
<form.AppField name="items" mode="array">
{(field) => (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{field.state.value.map((item, index) => (
<form.AppField name={`items[${index}]`}>
{(field) => <field.ArticleField key={index} />}
</form.AppField>
))}

<field.AddArticleButton />
</div>
)}
</form.AppField>
<form.AppField name="items" mode="array">
{(field) => (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{field.state.value.map((item, index) => (
<form.AppField name={`items[${index}]`}>
{(field) => <field.ArticleField key={index} />}
</form.AppField>
))}

<field.AddArticleButton />
</div>
)}
</form.AppField>
Now, when it comes to my ArticleField, I want to still access the other field components so I can add inputs there.
export default function ArticleField() {
const field = useFieldContext<ClientApplicationItemFormValues>();

const sku = useStore(field.store, (state) => state.value.sku);

const quantitiesBySize = useStore(
field.store,
(state) => state.value.quantities_by_size
);

return (...)
}
export default function ArticleField() {
const field = useFieldContext<ClientApplicationItemFormValues>();

const sku = useStore(field.store, (state) => state.value.sku);

const quantitiesBySize = useStore(
field.store,
(state) => state.value.quantities_by_size
);

return (...)
}
I want to create an input for my sku field, also will need to add another array field for quantitiesBySize, splitted into another component. I have no idea how to achieve that.
12 Replies
other-emerald
other-emeraldOP2mo ago
Is there a way to do something like that inside ArticleField which is already a field component registered in the form context?
<field.TextField />
<field.ArticleQuantityBySize />
<field.TextField />
<field.ArticleQuantityBySize />
harsh-harlequin
harsh-harlequin2mo ago
so you want to group multiple fields into one component?
other-emerald
other-emeraldOP2mo ago
No, quite the opposite of it. I want everything to be separated. But I'm failing to access the field components to manipulate the form state.
harsh-harlequin
harsh-harlequin2mo ago
well, useFieldContext and useFormContext are context objects, so they can be nested. Of course that assumes that the nested component still has the same type for field context (or a wider one). When it comes to logic (setting other form state based on this field state), then that would belong in form.AppField, not your field component
other-emerald
other-emeraldOP2mo ago
Does it mean I should have another nested form component instead of a field component? (my ArticleField to actually use useFormContext cuz I can't use something like <field.CustomComponent /> when using useFieldContext) e.g
export default function ArticleField() {
const field = useFieldContext<ClientApplicationItemFormValues>();

const sku = useStore(field.store, (state) => state.value.sku);

const quantitiesBySize = useStore(
field.store,
(state) => state.value.quantities_by_size
);

return (
<>
<field.TextField /> // already registered as a field component in the form context
<field.ArticleQuantityBySize /> // can't access it this way - another custom field component
</>
)
}
export default function ArticleField() {
const field = useFieldContext<ClientApplicationItemFormValues>();

const sku = useStore(field.store, (state) => state.value.sku);

const quantitiesBySize = useStore(
field.store,
(state) => state.value.quantities_by_size
);

return (
<>
<field.TextField /> // already registered as a field component in the form context
<field.ArticleQuantityBySize /> // can't access it this way - another custom field component
</>
)
}
Sorry if struggling to explain it or understand it properly
harsh-harlequin
harsh-harlequin2mo ago
Remove the preceding field and import the component directly the field helps with autocomplete when you‘re using the form, it‘s unrelated to building the field components also keep in mind that the nested components also expect ClientApplicationItemFormValues as field type
other-emerald
other-emeraldOP2mo ago
oh, so I can't narrow it down and focus only on the part of the form values which the component is responsible for?
export type ClientApplicationItemFormValues = {
sku: string; // Like one component focus only here so the field context is type of string
quantities_by_size: { // next one focuses only here so the type is { size: number; quantity: number }
size: number;
quantity: number;
}[];
...
};
export type ClientApplicationItemFormValues = {
sku: string; // Like one component focus only here so the field context is type of string
quantities_by_size: { // next one focuses only here so the type is { size: number; quantity: number }
size: number;
quantity: number;
}[];
...
};
Also, I need to map through the quantitiesBySize
{quantitiesBySize.map((quantity) => (
<ArticleQuantityBySize
size={quantity.size}
quantity={quantity.quantity}
key={quantity.size}
/>
))}
{quantitiesBySize.map((quantity) => (
<ArticleQuantityBySize
size={quantity.size}
quantity={quantity.quantity}
key={quantity.size}
/>
))}
so we do it like that? Not using the AppField with mode array?
harsh-harlequin
harsh-harlequin2mo ago
That would be what this question was referring to at the moment, it's not possible. But there is a PR version if you'd like to give it a spin
other-emerald
other-emeraldOP2mo ago
oh, okaay. Thank you. Can I have a look at PR? I guess I need some stable solution. What can I do for now, while it's not possible? Should I just not split into components too much)
harsh-harlequin
harsh-harlequin2mo ago
either a compound field (one field at object level, inputs change the property of the object), or splitting it into multiple field components PR: https://github.com/TanStack/form/pull/1469 (includes docs description) To use it:
npm i https://pkg.pr.new/@tanstack/react-form@1469
npm i https://pkg.pr.new/@tanstack/react-form@1469
other-emerald
other-emeraldOP2mo ago
thank you, just tried modifying the object properties In case of arrays, this is the only thing I could do, right? (Checking the field I want to modify by index or size in my case, also keeping the previous state in the handleChange)
export default function ArticleQuantityBySize({ size, quantity }: Props) {
const field = useFieldContext<ClientApplicationItemFormValues>();

return (
<div className="p-1 flex flex-col items-center space-y-2">
<p className="text-muted-foreground">{size}</p>

<Input
defaultValue={quantity}
type="number"
inputSize="sm"
onChange={(e) =>
field.handleChange({
...field.state.value,
quantities_by_size: field.state.value.quantities_by_size.map(
(q) => ({
...q,
quantity: q.size === size ? e.target.valueAsNumber : q.quantity,
})
),
})
}
className="min-w-16 max-w-[80px] text-center"
/>
</div>
);
}
export default function ArticleQuantityBySize({ size, quantity }: Props) {
const field = useFieldContext<ClientApplicationItemFormValues>();

return (
<div className="p-1 flex flex-col items-center space-y-2">
<p className="text-muted-foreground">{size}</p>

<Input
defaultValue={quantity}
type="number"
inputSize="sm"
onChange={(e) =>
field.handleChange({
...field.state.value,
quantities_by_size: field.state.value.quantities_by_size.map(
(q) => ({
...q,
quantity: q.size === size ? e.target.valueAsNumber : q.quantity,
})
),
})
}
className="min-w-16 max-w-[80px] text-center"
/>
</div>
);
}
harsh-harlequin
harsh-harlequin2mo ago
consider passing the index as prop so you don't need to refind it on every change

Did you find this page helpful?