-
Notifications
You must be signed in to change notification settings - Fork 414
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update status-button.tsx #394
base: main
Are you sure you want to change the base?
Conversation
updated `status-button` component to be conditionally enabled
Thanks. Can you give an example of why this is necessary? |
I think this is necessary for any route with multiple forms I can think of some use-cases like a create-update form, or an editable table const schema = {
delete: z.object({ id: z.string().nonempty() }),
create: z.object({ name: z.string().nonempty() }),
update: z.object({ id: z.string().nonempty(), name: z.string().nonempty() }),
}
const ActionForm = ({
formValue,
defaultValue,
}: {
formValue: 'create' | 'update'
defaultValue?: any
}) => {
const actionData = useActionData<typeof action>()
const isCurrent = actionData?.form === formValue
const isSubmitting = useSubmitting({
enabled: isCurrent,
})
const [form, fields] = useForm({
constraint: getFieldsetConstraint(schema[formValue]),
lastSubmission: isCurrent ? actionData.submission : undefined,
defaultValue,
})
return (
<Form method="POST" {...form.props}>
<input hidden readOnly name="form" value={formValue} />
<input {...conform.input(fields.id, { hidden: true })} />
<Field
inputProps={{
...conform.input(fields.name),
placeholder: 'Display name',
}}
errors={fields.name.errors}
/>
<StatusButton
status={isSubmitting ? 'pending' : actionData?.status ?? 'idle'}
enabled={isCurrent}
>
Continue
</StatusButton>
</Form>
)
}
const Delete = ({ id }: { id: string }) => {
const actionData = useActionData<typeof action>()
const formValue = 'delete'
const isCurrent = actionData?.form === formValue
const isSubmitting = useSubmitting({
enabled: isCurrent,
})
const [form, fields] = useForm({
constraint: getFieldsetConstraint(schema[formValue]),
lastSubmission: isCurrent ? actionData.submission : undefined,
defaultValue: { id },
})
return (
<Form method="POST" {...form.props}>
<input hidden readOnly name="form" value={formValue} />
<input {...conform.input(fields.id, { hidden: true })} />
<StatusButton
status={isSubmitting ? 'pending' : actionData?.status ?? 'idle'}
enabled={isCurrent}
>
Delete
</StatusButton>
</Form>
)
} |
Thanks for explaining that. The problem with this approach is In this case, I think it would be better to accept a |
@kentcdodds there is an issue with using I've chose I tweaked this component in my project type UseStatusBaseArgs = {
state?: 'submitting' | 'loading' | 'non-idle'
formAction?: string
formMethod?: 'POST' | 'GET' | 'PUT' | 'PATCH' | 'DELETE'
}
type UseStatusWithCurrent = UseStatusBaseArgs & {
currentValue?: string
currentKey?: string
}
type UseStatusWithoutCurrent = UseStatusBaseArgs & {
currentValue?: never
currentKey?: never
}
export function useStatus<T>(args: UseStatusWithCurrent): {
navigating: boolean
pending: boolean
current: boolean
actionData: SerializeFrom<T>
}
export function useStatus(args: UseStatusWithoutCurrent): boolean
export function useStatus<T extends Record<string, unknown>>(
args: Prettify<UseStatusWithCurrent> | Prettify<UseStatusWithoutCurrent>,
) {
const {
state = 'non-idle',
formAction,
formMethod = 'POST',
currentValue,
currentKey = 'actionId',
} = args ?? {}
// navigating
const navigation = useNavigation()
const navigating =
state === 'non-idle'
? navigation.state !== 'idle'
: navigation.state === state
// pending
const ctx = useFormAction()
const pending =
navigation.formAction === (formAction ?? ctx) &&
navigation.formMethod === formMethod &&
navigating
// current
const actionDataBase = useActionData<T>()
const current =
currentKey && currentValue
? actionDataBase?.[currentKey] === currentValue
: true
const actionData = currentValue
? current
? actionDataBase
: undefined
: actionDataBase
return currentValue
? ({ actionData, navigating, pending, current } as const)
: pending
}
export function useActionStatus<
T extends {
status: 'pending' | 'success' | 'error' | 'idle'
},
>() {
const actionData = useActionData<T>()
return actionData?.status ?? 'idle'
}
export const StatusButton = forwardRef<
HTMLButtonElement,
ButtonProps & {
message?: string | null
spinDelay?: Parameters<typeof useSpinDelay>[1]
currentValue?: string
currentKey?: string
}
>(
(
{
message,
className,
currentKey,
currentValue,
children,
spinDelay,
...props
},
ref,
) => {
const { pending, current } = useStatus({
currentValue: currentValue,
currentKey: currentKey || undefined,
})
const status = useActionStatus()
const delayedPending = useSpinDelay(
currentValue ? pending && current : pending,
{
delay: 400,
minDuration: 300,
...spinDelay,
},
)
const companion = {
pending: delayedPending ? (
<div className="inline-flex h-6 w-6 items-center justify-center">
<Icon name="update" className="animate-spin" />
</div>
) : null,
success: (
<div className="inline-flex h-6 w-6 items-center justify-center">
<Icon name="check" />
</div>
),
error: (
<div className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-destructive">
<Icon name="cross-1" className="text-destructive-foreground" />
</div>
),
idle: null,
}[currentValue ? (current ? status : 'idle') : status]
const type = currentValue
? current && status.match(/success|error/g)
? 'reset'
: 'submit'
: status.match(/success|error/g)
? 'reset'
: 'submit'
return (
<Button
ref={ref}
className={cn('flex justify-center gap-4', className)}
type={type}
disabled={currentValue ? delayedPending && current : delayedPending}
{...props}
>
<div>{children}</div>
{message ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>{companion}</TooltipTrigger>
<TooltipContent>{message}</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
companion
)}
</Button>
)
},
)
StatusButton.displayName = 'StatusButton' and then I use it like this const { actionData } = useStatus<typeof action>({
currentKey: 'actionId',
currentValue: 'create',
})
const [form, fields] = useForm({
constraint: getFieldsetConstraint(schema.update),
lastSubmission: actionData?.submission,
})
const Continue = () => (
<StatusButton
currentKey="actionId"
currentValue="create"
>
Continue
</StatusButton>
) |
Hmmm, I wonder if we could contribute this to remix-utils by @sergiodxa. That seems like a better place for this sort of utility. |
Would you mind if I make a package out of your code? |
Of course you could do that yourself if you prefer |
Sorry I was out for a while Of course that's an honor, you can do whatever you want |
I'm not certain I'll have time to do it, but possibly in the future I will. |
There's an update if you're interested interface UsePendingArgs extends UseSimplePendingArgs {
currentKey?: string
currentValue?: string
}
export function usePending<T>(args?: UsePendingArgs): {
actionData: SerializeFrom<T> | undefined
navigating: boolean
pending: boolean
current: boolean
} {
const {
state = 'non-idle',
formAction,
formMethod = 'POST',
currentKey,
currentValue,
} = args || {}
const navigation = useNavigation()
const navigating =
state === 'non-idle'
? navigation.state !== 'idle'
: navigation.state === state
// pending
const ctx = useFormAction()
const pending =
navigation.formAction === (formAction ?? ctx) &&
navigation.formMethod === formMethod &&
navigating
// current
const actionDataBase = useActionData<T>()
const current =
currentKey && currentValue
? // @ts-ignore
actionDataBase?.[currentKey] === currentValue ||
navigation.formData?.get(currentKey) === currentValue
: true
const actionData =
currentKey && currentValue
? current
? actionDataBase
: undefined
: actionDataBase
return { actionData, navigating, pending, current } as const
}
export const StatusButton = forwardRef<
HTMLButtonElement,
ButtonProps & {
message?: string | null
spinDelay?: Parameters<typeof useSpinDelay>[1]
currentValue?: string
currentKey?: string
}
>(
(
{
message,
className,
currentKey,
currentValue,
children,
spinDelay,
...props
},
ref,
) => {
const { pending, current, actionData } = usePending<{
status: 'pending' | 'success' | 'error' | 'idle'
}>({
currentValue: currentValue,
currentKey: currentKey,
})
const status = currentValue
? pending && current
? 'pending'
: actionData?.status ?? 'idle'
: pending
? 'pending'
: actionData?.status ?? 'idle'
const delayedPending = useSpinDelay(
currentValue ? pending && current : pending,
{
delay: 400,
minDuration: 300,
...spinDelay,
},
)
const companion = {
pending: delayedPending ? (
<div className="inline-flex h-6 w-6 items-center justify-center">
<Icon name="update" className="animate-spin" />
</div>
) : null,
success: (
<div className="inline-flex h-6 w-6 items-center justify-center">
<Icon name="check" />
</div>
),
error: (
<div className="inline-flex h-6 w-6 items-center justify-center">
<Icon name="cross-1" className="text-destructive" />
</div>
),
idle: null,
}[currentValue ? (current ? status : 'idle') : status]
const type = 'submit'
const child = (
<>
{companion ? (
message ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>{companion}</TooltipTrigger>
<TooltipContent>{message}</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
companion
)
) : (
children
)}
</>
)
return (
<Button
ref={ref}
className={cn('flex justify-center gap-4', className)}
type={type}
disabled={currentValue ? delayedPending && current : delayedPending}
{...props}
>
{child}
</Button>
)
},
)
StatusButton.displayName = 'StatusButton' |
|
updated
status-button
component to be conditionally enabledTest Plan
Checklist
Screenshots