fabian-hiller/modular-forms

Ability to pass dynamic values into Zod schema (qwik)

Closed this issue · 10 comments

tuurbo commented

I have a product form component and some products have different validation rules.

<QuantityInput min={1} max={10} multipleOf={2} />

I've tried passing props directly into the zod schema but I get error Internal server error: Qrl($) scope is not a function, but it's capturing local identifiers: props. Would something like this be doable?

export const QuantityInput = component$((props) => {

  const productForm = useFormStore<ProductForm>({
    loader,
    validate: zodForm$(
        z.object({
          qty: z.coerce
            .number()
            .min(props.min)
            .max(props.max)
            .multipleOf(props.multipleOf),
        }),
    ),
  });

  return <></>;
})

Thanks again for your commitment to create an issue! I will update zodForm$ so you can pass a function. Then it should work. I will let you know when the update is available.

const productForm = useFormStore<ProductForm>({
  loader,
  validate: zodForm$(() => // Here is the difference
    z.object({
      qty: z.coerce
        .number()
        .min(props.min)
        .max(props.max)
        .multipleOf(props.multipleOf),
    })
  ),
});

The new version is available. 🎉

tuurbo commented

I updated to the latest version and implemented the code as you wrote in a previous comment but when the validation triggers I get error Error: Code(14): Invoking 'use*()' method outside of invocation context.

import {useLexicalScope} from "/node_modules/.pnpm/@builder.io+qwik@0.24.0_undici@5.21.0/node_modules/@builder.io/qwik/core.mjs?v=7aacb625";
import {z} from "/node_modules/.pnpm/@builder.io+qwik-city@0.7.0_@builder.io+qwik@0.24.0/node_modules/@builder.io/qwik-city/index.qwik.mjs?v=7aacb625";
export const qty_input_component_loginForm_useFormStore_zodForm_uLT58SNrVtg = ()=>{
    const [props] = useLexicalScope(); // <----------- ERROR POINTS HERE
    return z.object({
        qty: z.coerce.number().min(props.min).max(props.max).multipleOf(props.multipleOf)
    });
}

It worked for me. Can you send me your code or a snippet of it?

Did you use component$ for your component?

tuurbo commented

This is the exact code

import { component$, useSignal } from '@builder.io/qwik';
import { zodForm$, Field, useFormStore } from '@modular-forms/qwik';
import { z } from '@builder.io/qwik-city';
import clsx from 'clsx';

interface Props {
  min: number;
  max: number;
  multipleOf: number;
}

type QtyForm = { qty: number };

export default component$<Props>((props) => {
  const loader = useSignal<QtyForm>({
    qty: 1,
  });

  const productForm = useFormStore<QtyForm>({
    loader,
    validate: zodForm$(() =>
      z.object({
        qty: z.coerce
          .number()
          .min(props.min)
          .max(props.max)
          .multipleOf(props.multipleOf),
      }),
    ),
    validateOn: 'input',
  });

  return (
    <>
      <Field of={productForm} name="qty">
        {(field, fieldProps) => (
          <>
            <input
              {...fieldProps}
              value={field.value}
              class={clsx('form-input')}
              type="text"
              inputMode="numeric"
              autoComplete="off"
            />
            <div>Error: {field.error}</div>
          </>
        )}
      </Field>
    </>
  );
});
tuurbo commented
@builder.io/qwik": "0.24.0"
@builder.io/qwik-city": "0.7.0"
@modular-forms/qwik": "^0.4.0"

I have investigated the problem. When a lexically scoped variable is accessed, Qwik executes useLexicalScope under the hood. Since validation can be performed at various points outside of an invocation context, this breaks access to those variables and an error is thrown.

Unfortunately, I don't currently have a solution for this. However, I have two workarounds that may work for you. Instead of accessing props within the formAction$ function, you can pass the whole schema via props.

import { $, component$, type QRL } from '@builder.io/qwik';
import { Field, useFormStore, zodFormQrl } from '@modular-forms/qwik';
import { z, type ZodType } from 'zod';

interface Props {
  schema: QRL<ZodType<any, any, any>>;
}

type QtyForm = { qty: number };

export const YourForm = component$<Props>((props) => {
  const productForm = useFormStore<QtyForm>({
    loader: { value: { qty: 1 } },
    validate: zodFormQrl(props.schema),
    validateOn: 'input',
  });

  return (
    <Field of={productForm} name="qty">
      {(field, fieldProps) => (
        <>
          <input
            {...fieldProps}
            value={field.value}
            class="form-input"
            type="text"
            inputMode="numeric"
            autoComplete="off"
          />
          <div>Error: {field.error}</div>
        </>
      )}
    </Field>
  );
});

export default component$(() => (
  <YourForm
    schema={$(
      z.object({
        qty: z.coerce.number().min(1).max(2).multipleOf(5),
      })
    )}
  />
));

The second workaround is to forego validate in useFormStore with a Zod schema and do the validation via our internal validation functions.

import { component$ } from '@builder.io/qwik';
import {
  custom$,
  Field,
  maxNumber,
  minNumber,
  useFormStore,
} from '@modular-forms/qwik';

interface Props {
  min: number;
  max: number;
  multipleOf: number;
}

type QtyForm = { qty: number };

export const YourForm = component$<Props>((props) => {
  const productForm = useFormStore<QtyForm>({
    loader: { value: { qty: 1 } },
    validateOn: 'input',
  });

  return (
    <Field
      of={productForm}
      name="qty"
      validate={[
        minNumber(props.min, 'Number too small!'),
        maxNumber(props.max, 'Number too big!'),
        custom$(
          (value) => !!value && value % props.multipleOf === 0,
          `Not a multiple of ${props.multipleOf}.`
        ),
      ]}
    >
      {(field, fieldProps) => (
        <>
          <input
            {...fieldProps}
            value={field.value}
            class="form-input"
            type="text"
            inputMode="numeric"
            autoComplete="off"
          />
          <div>Error: {field.error}</div>
        </>
      )}
    </Field>
  );
});

export default component$(() => <YourForm min={2} max={20} multipleOf={5} />);

I am unsure if I should reopen the issue, as I don't currently know if there is a solution to this problem. However, if there is any sign of it, I will definitely reopen it.

tuurbo commented

Thanks for the help! I haven't had a chance to work on it again but I will probably go with your second example.