T
TanStack2mo ago
conscious-sapphire

Late validation on linked field

I am facing an strange issue in my Form using zod and refine a schemaa. 1. I have this schema
z
.number({
required_error: t('widgets.orderExecution.errors.triggerPriceRequired', {
defaultValue: 'Trigger price is required',
}),
invalid_type_error: t(
'widgets.orderExecution.errors.triggerPriceInvalid',
{
defaultValue: 'Please enter a valid trigger price',
}
),
})
.positive({
message: t('widgets.orderExecution.errors.triggerPriceMin0', {
defaultValue: 'Trigger price must be greater than 0',
}),
})
.min(price, {
message: t('widgets.orderExecution.errors.triggerPriceMin', {
defaultValue: 'Trigger price must be greater than current price',
}),
})
.refine(
(val: number) => {
const strValue = val.toString();
const decimalParts = strValue.split('.');
if (decimalParts.length === 2) {
return is3decimalPrice
? decimalParts[1].length <= 3
: decimalParts[1].length <= 2;
}
return true;
},
{
message: t('widgets.orderExecution.errors.triggerPriceDecimalPlaces', {
decimalPlaces: is3decimalPrice ? 3 : 2,
defaultValue: `Trigger price can have maximum ${is3decimalPrice ? 3 : 2} decimal places`,
}),
}
);
z
.number({
required_error: t('widgets.orderExecution.errors.triggerPriceRequired', {
defaultValue: 'Trigger price is required',
}),
invalid_type_error: t(
'widgets.orderExecution.errors.triggerPriceInvalid',
{
defaultValue: 'Please enter a valid trigger price',
}
),
})
.positive({
message: t('widgets.orderExecution.errors.triggerPriceMin0', {
defaultValue: 'Trigger price must be greater than 0',
}),
})
.min(price, {
message: t('widgets.orderExecution.errors.triggerPriceMin', {
defaultValue: 'Trigger price must be greater than current price',
}),
})
.refine(
(val: number) => {
const strValue = val.toString();
const decimalParts = strValue.split('.');
if (decimalParts.length === 2) {
return is3decimalPrice
? decimalParts[1].length <= 3
: decimalParts[1].length <= 2;
}
return true;
},
{
message: t('widgets.orderExecution.errors.triggerPriceDecimalPlaces', {
decimalPlaces: is3decimalPrice ? 3 : 2,
defaultValue: `Trigger price can have maximum ${is3decimalPrice ? 3 : 2} decimal places`,
}),
}
);
that is defined in an function called takeProfitTriggerPriceShcema that retunrs zod schema any i have two fields, Price and TriggerPrice i want everytime the price input changes to trigger validations on triggerPrice, so i did this using linked fields
7 Replies
conscious-sapphire
conscious-sapphireOP2mo ago
<div className="w-full">
<form.Field
// @ts-ignore for now
name="takeProfitTriggerPrice"
mode="value"
// @ts-ignore for now
validators={{
onChangeListenTo: ['price'],
onChange: takeProfitTriggerPriceSchema(
false,
t,
currentLimitPrice
),
}}
>
<div className="w-full">
<form.Field
// @ts-ignore for now
name="takeProfitTriggerPrice"
mode="value"
// @ts-ignore for now
validators={{
onChangeListenTo: ['price'],
onChange: takeProfitTriggerPriceSchema(
false,
t,
currentLimitPrice
),
}}
>
{(field: any) => (
<CompactLabeledInput
htmlFor="takeProfitTriggerPrice"
label={t(
'widgets.orderExecution.fields.triggerPrice'
)}
shouldAddMarginBottom={false}
isError={!field.state.meta.isValid}
hintMsg={field.state.meta.errors[0]?.message}
>
<InputNumber
{...inputNumberProps}
precision={is3decimalPrice ? 3 : 2}
value={field.state.value}
onChange={(val) =>
handleInputNumberChange(val, field)
}
onKeyDown={handleNumericInputkeyDown}
disabled={
!takeProfitEnabled || !!takeProfitTooltip
}
status={
!!field.state.meta.errors.length
? 'error'
: undefined
}
defaultValue={
initialPairs?.[0]?.take_profit
?.trigger_price || undefined
}
/>
</CompactLabeledInput>
)}
</form.Field>
</div>
{(field: any) => (
<CompactLabeledInput
htmlFor="takeProfitTriggerPrice"
label={t(
'widgets.orderExecution.fields.triggerPrice'
)}
shouldAddMarginBottom={false}
isError={!field.state.meta.isValid}
hintMsg={field.state.meta.errors[0]?.message}
>
<InputNumber
{...inputNumberProps}
precision={is3decimalPrice ? 3 : 2}
value={field.state.value}
onChange={(val) =>
handleInputNumberChange(val, field)
}
onKeyDown={handleNumericInputkeyDown}
disabled={
!takeProfitEnabled || !!takeProfitTooltip
}
status={
!!field.state.meta.errors.length
? 'error'
: undefined
}
defaultValue={
initialPairs?.[0]?.take_profit
?.trigger_price || undefined
}
/>
</CompactLabeledInput>
)}
</form.Field>
</div>
this resulted to late validation so if for example i changed the price input with valid value and then changed the trigger price value with invalid value (value that is larger than the price) it should trigger that the input is invalid but it doesn't i have to add one more value( one step late validation) imagine Price is : 60 trigger price is : 50 these are valid then i went changed price to 49, nothing happens even this is invalid, then if i changed price to 60 it will show invalid message (which is actually the got triggered with the previous value but it didn't re-render)
fair-rose
fair-rose2mo ago
could you wrap it as code block instead? it‘s ```tsx done like this ``` @Othman it helps the formatting and adds some syntax highlighting
conscious-sapphire
conscious-sapphireOP2mo ago
@Luca | LeCarbonator Done, Sorry a little bit new to discord xd:)
fair-rose
fair-rose2mo ago
it's fine! Alright, time to take a look Alright, so to summarize:
// This is a simple verison of the field schema based on what I can tell.
// Let me know if the parameters aren't correct
function takeProfitTriggerPriceSchema(is3DecimalPrice: boolean, minPrice: number) {
return z.number().positive().min(price).refine((val: number) => {
const strValue = val.toString();
const decimalParts = strValue.split('.');
if (decimalParts.length === 2) {
return is3decimalPrice
? decimalParts[1].length <= 3
: decimalParts[1].length <= 2;
}
return true;
});
}
// This is a simple verison of the field schema based on what I can tell.
// Let me know if the parameters aren't correct
function takeProfitTriggerPriceSchema(is3DecimalPrice: boolean, minPrice: number) {
return z.number().positive().min(price).refine((val: number) => {
const strValue = val.toString();
const decimalParts = strValue.split('.');
if (decimalParts.length === 2) {
return is3decimalPrice
? decimalParts[1].length <= 3
: decimalParts[1].length <= 2;
}
return true;
});
}
Your field looks like this:
<form.Field
name="takeProfitTriggerPrice"
// mode="value" (this is the default mode)
validators={{
onChangeListenTo: ['price'],
onChange: takeProfitTriggerPriceSchema(
false,
currentLimitPrice
)
}}
>
{ /* ... */}
</form.Field>
<form.Field
name="takeProfitTriggerPrice"
// mode="value" (this is the default mode)
validators={{
onChangeListenTo: ['price'],
onChange: takeProfitTriggerPriceSchema(
false,
currentLimitPrice
)
}}
>
{ /* ... */}
</form.Field>
So, how is currentLimitPrice defined?
conscious-sapphire
conscious-sapphireOP2mo ago
this is the currentLimitPrice
const currentLimitPrice = useStore(form.store, (state) => state.values.price)
const currentLimitPrice = useStore(form.store, (state) => state.values.price)
; And this is the params t: TFunction, price: number price = currentLimtPrice
fair-rose
fair-rose2mo ago
the reason it's out of date by one render is probably because it sets all the validators, then triggers the rerender (which recalculates the schema to compare against for next time the field changes) but what you actually want is for it to be up to date on the change itself. The fix for that is simple enough:
<form.Field
name="takeProfitTriggerPrice"
validators={{
onChangeListenTo: ['price'] // when price changes, run this one too in the same render cycle
onChange: ({ value, fieldApi }) => {
const minPrice = form.getFieldValue('price') // get the most up to date value at the moment
return fieldApi.parseValueWithSchema( // ... and generate the schema with it
takeProfitTriggerPriceSchema(false, minPrice)
)
}
}}
<form.Field
name="takeProfitTriggerPrice"
validators={{
onChangeListenTo: ['price'] // when price changes, run this one too in the same render cycle
onChange: ({ value, fieldApi }) => {
const minPrice = form.getFieldValue('price') // get the most up to date value at the moment
return fieldApi.parseValueWithSchema( // ... and generate the schema with it
takeProfitTriggerPriceSchema(false, minPrice)
)
}
}}
To elaborate on what exactly broke before: 1. price changes (50 -> 49) -> run price validation if there are any (change, onBlur etc.) -> run fields who listen to price's changes 2. takeProfitTriggerPrice listens to price, so it will run its validators -> The current validator is takeProfitTriggerPriceSchema(false, 50) ! we have not generated the new schema yet ! 3. It passes the validation -> finalize changes, useStore requests a rerender in React 4. Generate the new schema and assign it to the validator This means that next time price or takeProfitTriggerPrice changes, it will use the new schema I hope that makes sense, if not let me know @Othman
conscious-sapphire
conscious-sapphireOP2mo ago
it acutally makes perfect sense, i tried now and it works, this is amazing thank you very much

Did you find this page helpful?