Git Product home page Git Product logo

modular-forms's People

Contributors

aivarsliepa avatar aliyss avatar aurelienbobenrieth avatar brandonpittman avatar duluca avatar fabian-hiller avatar gabrielelpidio avatar hookyns avatar iainsimmons avatar jmcelreavey avatar johndunderhill avatar katywings avatar ofsen avatar robertwach avatar sacarvy avatar sibiakkash avatar susickypavel avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

modular-forms's Issues

Validate confirmation field (Qwik)

I have a registration form with the fields password and confirmPassword. In the validate property of confirmPassword, how to make it validate that its value is the same as the value of password. I tried this:

validate = value( getValue(form,"password"), "Password does not match");

but getValue is returning undefined even when the password field is filled.

[Question]Export FieldProps type

Hi! First of all, thanks for your work! I've just started using your library and it seems very promising ๐Ÿ˜ƒ!

I'd like to ask if it can be useful to export the FieldProps type from the Field component. The request comes from the fact that I'm creating my own custom input component. But instead of doing exactly what you have suggested on your doc, I'm also wrapping the Field component. Basically what I'd like to achive (and I'm able to do) is creating an Input component such as this one:

// Input.tsx
import { Field, FieldPath, FieldProps, FieldValues } from "@modular-forms/solid";
import { JSX, splitProps } from "solid-js";

type InputProps<
  TFieldValues extends FieldValues,
  TFieldName extends FieldPath<TFieldValues>
> = JSX.InputHTMLAttributes<HTMLInputElement> & { field: Omit<FieldProps<TFieldValues, TFieldName>, "children"> };

export const Input = <TFieldValues extends FieldValues, TFieldName extends FieldPath<TFieldValues>>(
  props: InputProps<TFieldValues, TFieldName>
) => {
  const [p, other] = splitProps(props, ["class", "field"]);

  return (
    <Field {...p.field}>
      {(field) => {
        const inputClasses = "rounded-xl border border-blue-400 p-2 text-black outline-none";
        const errorClass = () => (field.dirty && field.error ? "bg-red-400" : "");
        const externalClasses = () => p.class;
        const classes = () => `${inputClasses} ${errorClass()} ${externalClasses()}`;

        return <input {...field.props} class={classes()} {...other} />;
      }}
    </Field>
  );
};

In this way, from other components, I can just do the following:

// App.tsx
import { Component } from "solid-js";
import { Input } from "./components/Input";
import { createForm, Form, zodForm } from "@modular-forms/solid";
import { z } from "zod";

const UserSchema = z.object({ name: z.string().trim().min(1).max(20), email: z.string().trim().min(1).max(50) });
type User = z.infer<typeof UserSchema>;

const submit = (user: User) => console.log(user);

export const App: Component = () => {
  const form = createForm<User>({ validate: zodForm(UserSchema), validateOn: "input" });

  return (
    <Form of={form} onSubmit={submit}>
      <div class="flex flex-col gap-2">
        <Input field={{ of: form, name: "name" }} placeholder="Name" />
        <Input field={{ of: form, name: "email" }} placeholder="Email" />
        <button type="submit">Submit</button>
      </div>
    </Form>
  );
};

As you can see the code on App.tsx is very clean since I moved all the Field management on the Input component itself. The only problem was that to preserve type safety and Typescript autocompletion, I needed the FieldProps type. To fix this I just updated the Field.d.ts file to export it, but I wonder if this export can be included on your project. Unless, of course, there is already something better to achieve the same results that I'm not aware of.

Let me know if you need more info and if this request make sense at all! And of course, if needed, I can make a pull request, but maybe it's unnecessary since it's just a single line of code ๐Ÿ˜ƒ!

Thanks!

zod validation

Question,

i'm trying either

export const countrySchema = z.object({
  id: z
    .string()
    .optional(),
  code: z
    .string()
    .min(1, 'required.'),
    name: z
    .string()
    .min(1, 'required.')
    .regex(/^[a-zA-Z'-]+$/, "Only ' and - characters are allowed")
});

export type TCountry = z.input<typeof countrySchema>;

or

export const countrySchema = z.object({
  id: z
    .string()
    .optional(),
  code: z
    .string()
    .min(1, 'required.'),
    name: z
    .string()
    .min(1, 'required.')
    .regex(/^[a-zA-Z'-]+$/, {message: "Only ' and - characters are allowed"})
});

export type TCountry = z.input<typeof countrySchema>;

both seem to error out qwik on the regex stuff.
I'm overlooking something again... ?

still using your template as a form with the TextField

Best Regards,

formAction$ unknown errors should be logged or propagated

Unknown errors that occur inside a formAction are being returned to the client but do not generate error logs, that's dangerous because unknown errors cannot be monitored on the backend side, as they do not generate any logs. Making it challenging to spot new bugs

// If an error occurred, set error response
} catch (error: any) {
formActionStore.response = {
status: 'error',
message: 'An unknown error has occurred.',
};
}
}

transformers

Howdy,

Any idea on how to handle transforms for example only uppercase.

react has something like this

<Field name="code" transform={value => value.toUpperCase()}>
  {(field, props) => (
    <TextInput {...props} value={field.value} error={field.error} type="text" label="code" required />
  )}
</Field> 

i kinda like your but the value is set as string | number | undefined. so something like ;

onInput$={value.toUpperCase()}

doesn't work out of the box and i don't wanna mess with the components just to keep them inline with your components.

Thanks upfront for the input

[QWIK] Populating initial data?

I'm trying to use my restaurant data that is returned in my resource but I can't seem to figure out how to do it ๐Ÿค”

import { component$, Resource } from "@builder.io/qwik";
import { routeLoader$, z } from "@builder.io/qwik-city";
import { formAction$, useForm, zodForm$, type InitialValues } from "@modular-forms/qwik";
import { Button } from "~/components/base/Button";
import { Main } from "~/components/base/Main";
import { prisma } from "~/db";

const restaurantFormSchema = z.object({
    id: z.string().nonempty(),
    name: z.string().nonempty(),
    location: z.string().nonempty(),
    postcode: z.string().regex(/^[A-Z]{1,2}[0-9][0-9A-Z]?\s?[0-9][A-Z]{2}$/i),
    phone: z.string().regex(/^[0-9]{3}-[0-9]{3}-[0-9]{4}$/),
    doesDelivery: z.boolean(),
    addressLine1: z.string().optional(),
});

type RestaurantForm = z.input<typeof restaurantFormSchema>;

export const useRestaurantFormLoader = routeLoader$<InitialValues<RestaurantForm>>(() => ({
    id: "",
    name: "",
    location: "",
    addressLine1: "",
    postcode: "",
    phone: "",
    doesDelivery: false,
}));

export const useRestaurantFormAction = formAction$<RestaurantForm>(async (values) => {
    await prisma.restaurant.create({
        data: {
            name: values.name,
            location: values.location,
            addressLine1: values.addressLine1,
            postcode: values.postcode,
            phone: values.phone,
            doesDelivery: values.doesDelivery,
            relatedRestaurantId: values.id,
        },
    });
}, zodForm$(restaurantFormSchema));

export const useRestaurant = routeLoader$(async ({ params, redirect }) => {
    const restaurant = await prisma.restaurant.findUnique({
        where: {
            id: params.id,
        },
    });

    if (!restaurant) {
        console.log("redirecting user");
        throw redirect(301, "/");
    }

    return restaurant;
});

export default component$(() => {
    return (
        <Main>
            <Resource
                value={useRestaurant()}
                onPending={() => <div>Loading...</div>}
                onResolved={(restaurant) => {
                    const [, { Form, Field }] = useForm<RestaurantForm>({
                        loader: useRestaurantFormLoader(),
                        action: useRestaurantFormAction(),
                        validate: zodForm$(restaurantFormSchema),
                    });

                    return (
                        <Form>
                            <Field name="name">
                                {(field, props) => (
                                    <>
                                        <label for={field.name}>Your name</label>
                                        <input {...props} value={field.value} type="text" />
                                        <div>{field.error}</div>
                                    </>
                                )}
                            </Field>
                            <Field name="location">
                                {(field, props) => (
                                    <>
                                        <label for={field.name}>Your location</label>
                                        <input {...props} value={field.value} type="text" />
                                        <div>{field.error}</div>
                                    </>
                                )}
                            </Field>
                            <Field name="addressLine1">
                                {(field, props) => (
                                    <>
                                        <label for={field.name}>Your address line 1</label>
                                        <input {...props} value={field.value} type="text" />
                                        <div>{field.error}</div>
                                    </>
                                )}
                            </Field>
                            <Field name="postcode">
                                {(field, props) => (
                                    <>
                                        <label for={field.name}>Your postcode</label>
                                        <input {...props} value={field.value} type="text" />
                                        <div>{field.error}</div>
                                    </>
                                )}
                            </Field>
                            <Field name="phone">
                                {(field, props) => (
                                    <>
                                        <label for={field.name}>Your phone</label>
                                        <input {...props} value={field.value} type="text" />
                                        <div>{field.error}</div>
                                    </>
                                )}
                            </Field>
                            <Field name="doesDelivery">
                                {(field, props) => (
                                    <>
                                        <label for={field.name}>Does deliver?</label>
                                        <input {...props} checked={field.value} type="checkbox" />
                                        <div>{field.error}</div>
                                    </>
                                )}
                            </Field>

                            <Button>Send</Button>
                        </Form>
                    );
                }}
            />
        </Main>
    );
});

It doesn't seem possible as I need to basically give the values to the Loader which I don't have access to at this level?

onSubmit event does not require event.preventDefault()

Hi,

Great library. I am experimenting with the library and noticed that if I use event.preventDefault() in my submit handler for the form, the handler does not execute. Only if I remove event.preventDefault() does the handler execute. I also notice that the browser does not refresh without event.preventDefault. I pressume this was intentional. It would be nice to have that in the documentation.

Thanks again for putting together such a great library, it's very useful!

Submit a form and hard redirect if successful (qwik)

Is it possible to force a hard page load from within formAction$ only when intentional. For example when calling throw redirect()? I'm not using "SPA" mode in Qwik.

export const useFormAction = formAction$<LoginForm>(
  async (values, { redirect, fail }) => {
    const res = await fetch('/api/login');

    if (res.success) {      
      // do a hard refresh
      throw redirect(302, '/account');
    }

    return fail(400, {
      message: 'Login failed',
    });
  },
  zodForm$(loginSchema),
);

Are there any examples of using forms in different components?

I was wondering if there's any example of passing a Qwik action and loader into another component file. Currently I get linting errors for using them outside of the root.

I know I'm likely doing something silly so I'm wondering if there're any examples?

I'd also like to return the on submit event to the parent component on submit.

Redirect not working in formAction$

Hi! Thanks for your amazing work, this lib fills many gaps

I'm trying to do a redirect after processing some values, like this:

export const useFormAction = formAction$((values, { redirect }) => {
  console.log("VALUES", values);

  throw redirect(301, "/reports");
}, zodForm$(businessInfoSchema));

But it just does not work

Question: Creating components for subforms

Mentioned in the SolidJS Discord earlier and was recommended to post about it here.

I'm trying to figure out a good way to separate out a reusable component for an X/Y position form field, something like so:

type MyForm = {
  position: {
    x: number
    y: number
  }
};

// template:
<Field of={myForm} name="position"
  {field => (
    // where `PositionInput` consists of 2 text inputs: one for x and another for y
    <PositionInput {...field.props} value={field.value} />
  )}
</Field>

But with the above, I don't see how the onBlur, onInput, and onChange callbacks could be passed to 2 <input> elements.

So I've been trying an alternative approach, but here I'm unsure how to make the TS types behave correctly:

// Parent:
<Form of={myForm}>
  <PositionField of={myForm} name="position" />
</Form>

// PositionField:
export function PositionField<T>(props: { of: FormState<T>, name: string }) {
  return (
    <Field of={props.of} name={`${props.name}.x`}> // throws: Type '`${string}.x`' is not assignable to type 'ValuePaths<T>'
      {field => <input {...field.props) value={field.value} />}
    </Field>
    <Field of={props.of} name={`${props.name}.y`}>
      {field => <input {...field.props) value={field.value} />}
    </Field>
  )
}

In short, is there a recommendation on how to create reusable subforms?

Circular reference when 'strict: true' in tsconfig.json

I was getting the following error from Typescript when trying to build a basic form from the documentation in Typescript and SolidJS:

Type of property 'prototype' circularly references itself in mapped type '{ [K in keyof Blob]-?: ValuePath<K & string, Blob[K]>; }'.ts(2615)

I created a base project from the Stackblitz template, installed @modular-form/solid and set it up with the same eslint/tslint configuration as my main project. I found I could get the error to go away in the sample project by removing "vitest/globals" from the "types" section of tsconfig.json, but this did not work for my main project.

The only thing I found that will cause the error to go away in my main project is to set "strict: false" in tsconfig.json. Any ideas on a better solution to satisfy ts-lint but still have it set to strict mode?

Response object TypeScript

Hi (me again) ๐Ÿ‘‹,

I've got more of a question than an issue. I find the response property on the form handy. However, its type feels somewhat cumbersome as it is a union type of { status: string, message: string } and empty object, and it's a must to narrow the type, which can become kinda wordy in large forms and it also feels like work that doesn't need to be done. Also, the individual types such as ResponseObject or the mentioned anonymous object aren't exported from the lib, so they need to be redeclared on the user's side when using them for type narrowing.

I'm wondering whether it is a good idea to leave it up to the consumer the type and initial value of the response object? I'd probably find it more useful that way; at the very least, exporting the mentioned types would help a ton.

Server side validation support?

Hi ๐Ÿ‘‹

I'm curious and I couldn't see anything in the documentation but is it possible to pass back specific field errors from the server for example "Email address is already taken"

Ideally, I'd like a way of returning that error so that the appropriate field.error gets updated.

Apologies if it is in the documentation and I missed in (Great job on that documentation though, it's really easy to follow)

Missing Typings for `Field` child

When using the newest version of Qwick 1.102.0 the typings for the field and prop arguments are coming through as any.
This issue is not there when Qwik is downgraded to 1.100.0
Screenshot 2023-04-20 at 10 03 35

error when build or trying a preview build

Following your qwik repo comment here some more code.

in dev mode the login works but when building with npm build or preview we get the following

the trace looks like;


Oops! Something went wrong! :(

ESLint: 8.36.0

TypeError: Cannot read properties of undefined (reading 'name')
Occurred while linting C:\Users\Bleyenberg\IdeaProjects\dare-qwik-2\src\routes\auth\login\index.tsx:56
Rule: "qwik/valid-lexical-scope"
at JSXAttribute (C:\Users\Bleyenberg\IdeaProjects\dare-qwik-2\node_modules\eslint-plugin-qwik\index.js:7:1553)
at ruleErrorHandler (C:\Users\Bleyenberg\IdeaProjects\dare-qwik-2\node_modules\eslint\lib\linter\linter.js:1118:28)
at C:\Users\Bleyenberg\IdeaProjects\dare-qwik-2\node_modules\eslint\lib\linter\safe-emitter.js:45:58
at Array.forEach ()
at Object.emit (C:\Users\Bleyenberg\IdeaProjects\dare-qwik-2\node_modules\eslint\lib\linter\safe-emitter.js:45:38)
at NodeEventGenerator.applySelector (C:\Users\Bleyenberg\IdeaProjects\dare-qwik-2\node_modules\eslint\lib\linter\node-event-generator.js:297:26)
at NodeEventGenerator.applySelectors (C:\Users\Bleyenberg\IdeaProjects\dare-qwik-2\node_modules\eslint\lib\linter\node-event-generator.js:326:22)
at NodeEventGenerator.enterNode (C:\Users\Bleyenberg\IdeaProjects\dare-qwik-2\node_modules\eslint\lib\linter\node-event-generator.js:340:14)
at CodePathAnalyzer.enterNode (C:\Users\Bleyenberg\IdeaProjects\dare-qwik-2\node_modules\eslint\lib\linter\code-path-analysis\code-path-analyzer.js:795:23)
at C:\Users\Bleyenberg\IdeaProjects\dare-qwik-2\node_modules\eslint\lib\linter\linter.js:1153:32


I've pretty must followed your guide to get a look and feel of your api abd ended up with the following code snippet;

export const loginFormLoader = routeLoader$<InitialValues<LoginForm>>(() => ({
    email: '[email protected]',
    password: 'ponies',
}));

export const loginFormActionServerSide = formAction$<LoginForm>((values) => {

}, zodForm$(loginSchema));

export const Login = component$(() => {

    const navigate = useNavigate();
    const loadingIndicator = useStore({ loading: false });
    const serverError = useSignal(false)

    const [loginForm, { Form, Field }] = useForm<LoginForm>({
        loader: loginFormLoader(),
        action: loginFormActionServerSide(),
        validate: zodForm$(loginSchema),
    });

    const loginSubmit: SubmitHandler<LoginForm> = $((values) => {
        LoginApi(values).then((success) => {
            if (success) {
                navigate('/dashboard');
            } else {
                console.log('Login failed');
                serverError.value = true;
                setTimeout(() => {
                    serverError.value = false;
                }, 2500);
            }
        }).catch((error) => {
            console.error(error);
        });;
    });

    return (
        <div class="min-h-screen bg-base-200 flex items-center">
            <div class="card mx-auto w-full max-w-5xl  shadow-xl">
                <div class="grid  md:grid-cols-2 grid-cols-1  bg-base-100 rounded-xl">
                    <div class=''>
                        <LoginIntro />
                    </div>
                    <div class='py-24 px-10'>
                        <h2 class='text-2xl font-semibold mb-2 text-center'>Login</h2>
                        <Form id="login-form" onSubmit$={loginSubmit}>
                            <div class="flex flex-col mb-4 space-y-6">
                                <Field name="email">
                                    {(field, props) => (
                                        <TextInput
                                            {...props}
                                            value={field.value}
                                            error={field.error}
                                            type="email"
                                            label="Email"
                                            required
                                        />
                                    )}
                                </Field>
                                <Field name="password">
                                    {(field, props) => (
                                        <TextInput
                                            {...props}
                                            value={field.value}
                                            error={field.error}
                                            type="password"
                                            label="Password"
                                            required
                                        />
                                    )}
                                </Field>
                            </div>
                            <div class='text-right text-primary'>
                                <Link href="/forgot-password">
                                    <span class="text-sm  inline-block  hover:text-primary hover:underline hover:cursor-pointer transition duration-200">Forgot Password?</span>
                                </Link>
                                <button type="submit" class={"btn mt-2 w-full btn-primary" + (loadingIndicator.loading ? " loading" : "")}>Login</button>
                                <div class='text-center mt-4'>
                                    Don't have an account yet?
                                    <Link href="/register">
                                        <span class="  inline-block  hover:text-primary hover:underline hover:cursor-pointer transition duration-200">
                                            Register
                                        </span>
                                    </Link>
                                </div>
                            </div>
                        </Form>
                    </div>
                </div>
            </div>
            <div class="toast toast-top toast-end z-50 mt-16 mr-5 toast-overide-animation" style={{ display: serverError.value ? '' : 'none' }}>
                <div class="alert alert-error">
                    <div>
                        <span>email or password is wrong !!!</span>
                    </div>
                </div>
            </div>
        </div>
    );

});

export default component$(() => {
    return (
        <Login />
    );
});

export const head: DocumentHead = {
    title: 'login page'
};

Hope it helps.

interface does not satisfy the constraint FieldValues

Hello @fabian-hiller, very promising library and awesome documentation, thank you.

I'm using auto-generated interfaces for my types, but unfortunately I ran into an error:

Type 'UserAuthLogin' does not satisfy the constraint 'FieldValues'.
Index signature for type 'string' is missing in type 'UserAuthLogin'.

The interface:

export interface UserAuthLogin { login: string, password: string }

Thanks

dirty check when setting data from routeLoader

Hello,

I have the following;


export const useRouteLoaderFormCountry = routeLoader$<InitialValues<TCrmCountry>>(() => ({
    id: '',
    code: '',
    name: ''
}));


    useTask$(() => {
        console.log(countryForm.dirty)
        if (_useRouteLoaderCountry && _useRouteLoaderCountry.id !== '') {
            setValue(countryForm, 'id', _useRouteLoaderCountry?.id);
            setValue(countryForm, 'code', _useRouteLoaderCountry.code);
            setValue(countryForm, 'name', _useRouteLoaderCountry.name);
            console.log(countryForm.dirty)
        }
    })

    //FORM INPUT TRANSFORM
    useTask$(({ track }) => {
        console.log(countryForm.dirty)
        const code = track(() => getValue(countryForm, 'code')?.trim())
        const name = track(() => getValue(countryForm, 'name')?.trim())
        if (code) {
            setValue(countryForm, 'code', code.toUpperCase())
        }
        if (name) {
            const capitalized = name.charAt(0).toUpperCase() + name.slice(1)
            setValue(countryForm, 'name', capitalized)
        }
        console.log(countryForm.dirty)
    })

So we get a country from a the routeLoader and we trying to have the save button disabled with a css class

${!countryForm.dirty ? 'disabled' : null}

but on all the console.log values the dirty is true which make sence but if we replace those with

countryForm.dirty = false;

so we can start the filled form with a dirty of false, it just stays true.

Are we overlooking something simple again...

Thanks for you time

Zod Validation: required does not work

I am using the zod validation integration for validating my form.
When I focus an input, which is required, but has no other requirements, and leave it, no error shows up. It even doesn't show an error if I type something and remove it and then leave the input.
(I don't know if there is an option to solve this or if it is a bug)

[Suggestion] Remove "of" property from Field

Hello i just discovered SolidJS and this ReactHookForm-like version, very nice !

But i noticed in docs the Field is duplicating of={logingForm} while it is already defined in parent component <Form of={logingForm}.
Also a Field will most probably never be used outside of a Form ?

Suggestion: maybe there is a way for Field to grab of's value from Form ?

Edge case: if used inside a native form element, Field could make the "of" property as required only in the case where not used as child of Form ?

Up to date documentation for Solid?

Hey ๐Ÿ‘‹

I'm following through on your solid guide, specifically this page though it seems outdated.

There's no action property available on the Form and onSubmit() now seems to be required? I had assumed I somehow have to call my server function loggingInAction i.e const [loggingIn, loggingInAction] = createServerAction$(...) from the onSubmit now but I can't seem to see how?

Thanks in advance

Issue with simple demo

Awesome job and thinking by having this really great library :)

But I'm facing an Issue with just a simple Form demo,

Followed your steps for installation in a new solid-js project (not SolidStart).

Imported as shown to your examples

import { createForm, Form, Field } from '@modular-forms/solid';

Copy/Pate the form example

`export default function App() {
const loginForm = createForm();

const loginUser = () => {
console.log("TODO: login values")
}

return (



{(field) => <input {...field.props} type="email" />}


{(field) => <input {...field.props} type="password" />}



);
}`

but I get the following error...

Uncaught ReferenceError: React is not defined
at Form (index.esm.jsx:1454:3)

PackageJson:

"devDependencies": { "@babel/core": "^7.20.5", "@storybook/addon-actions": "^6.5.13", "@storybook/addon-essentials": "^6.5.13", "@storybook/addon-interactions": "^6.5.13", "@storybook/addon-links": "^6.5.13", "@storybook/builder-vite": "^0.2.5", "@storybook/html": "^6.5.13", "@storybook/testing-library": "^0.0.13", "@typescript-eslint/eslint-plugin": "^5.43.0", "@typescript-eslint/parser": "^5.43.0", "autoprefixer": "^10.4.13", "babel-loader": "^8.3.0", "eslint": "^8.28.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^17.0.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-solid": "^0.8.0", "postcss": "^8.4.19", "prettier": "^2.7.1", "tailwindcss": "^3.2.4", "typescript": "^4.8.2", "vite": "^3.2.3", "vite-plugin-solid": "^2.3.0" }, "dependencies": { "@modular-forms/solid": "^0.9.1", "@solid-primitives/i18n": "^1.1.2", "@solidjs/router": "^0.5.1", "@thisbeyond/solid-dnd": "^0.7.3", "solid-devtools": "^0.23.1", "solid-js": "^1.6.2" }, "engines": { "node": ">=14" }

viteConfig:

`import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';

export default defineConfig({
plugins: [solidPlugin()],
server: {
port: 3000,
},
build: {
target: 'esnext',
},
optimizeDeps: {
extensions: ['jsx', 'tsx'],
},
});
`

I can't locate what I do wrong in the example, also tried to replicate examples from your github but had the same outcome.

previous two qwik update result in error

Think you need to update the deps tree of the library

npm i @modular-forms/qwik

npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR! 
npm ERR! While resolving: my-qwik-basic-starter@undefined
npm ERR! Found: @builder.io/[email protected]

after running -force it's clear

npm WARN using --force Recommended protections disabled.
npm WARN ERESOLVE overriding peer dependency
npm WARN While resolving: @modular-forms/[email protected]
npm WARN Found: @builder.io/[email protected]
npm WARN node_modules/@builder.io/qwik
npm WARN   peer @builder.io/qwik@">=0.22.0" from @builder.io/[email protected]
npm WARN   node_modules/@builder.io/qwik-city
npm WARN     dev @builder.io/qwik-city@"~0.100.0" from the root project
npm WARN   1 more (the root project)
npm WARN 
npm WARN Could not resolve dependency:
npm WARN peer @builder.io/qwik@"^0.23.0" from @modular-forms/[email protected]
npm WARN node_modules/@modular-forms/qwik
npm WARN   @modular-forms/qwik@"*" from the root project
npm WARN
npm WARN Conflicting peer dependency: @builder.io/[email protected]
npm WARN node_modules/@builder.io/qwik
npm WARN   peer @builder.io/qwik@"^0.23.0" from @modular-forms/[email protected]
npm WARN   node_modules/@modular-forms/qwik
npm WARN     @modular-forms/qwik@"*" from the root project
npm WARN ERESOLVE overriding peer dependency
npm WARN While resolving: @modular-forms/[email protected]
npm WARN Found: @builder.io/[email protected]
npm WARN node_modules/@builder.io/qwik-city
npm WARN   dev @builder.io/qwik-city@"~0.100.0" from the root project
npm WARN 
npm WARN Could not resolve dependency:
npm WARN peer @builder.io/qwik-city@"^0.6.5" from @modular-forms/[email protected]
npm WARN node_modules/@modular-forms/qwik
npm WARN   @modular-forms/qwik@"*" from the root project
npm WARN
npm WARN Conflicting peer dependency: @builder.io/[email protected]
npm WARN node_modules/@builder.io/qwik-city
npm WARN   peer @builder.io/qwik-city@"^0.6.5" from @modular-forms/[email protected]
npm WARN   node_modules/@modular-forms/qwik
npm WARN     @modular-forms/qwik@"*" from the root project

Qwik - Form resets on validation error.

This could be a Qwik issue rather than a Modular-Forms issue but I'd like to get thoughts on it.

I have the following page:

import { component$, useStylesScoped$, useSignal } from "@builder.io/qwik";
import { routeLoader$, z } from "@builder.io/qwik-city";
import {
  formAction$,
  useForm,
  zodForm$,
  type InitialValues,
} from "@modular-forms/qwik";
import ContactStyles from "./contact.css?inline";

const contactSchema = z.object({
  name: z.string().min(5),
  message: z.string().min(5),
});

type ContactForm = z.input<typeof contactSchema>;

export const useFormLoader = routeLoader$<InitialValues<ContactForm>>(() => ({
  name: "",
  message: "",
}));

export const useFormAction = formAction$<ContactForm>((values) => {
  return {
    errors: { name: "Name is already taken" },
    // Form response
    response: {
      status: "error",
      message: "An error has occurred.",
    },
  };
}, zodForm$(contactSchema));

export default component$(() => {
  useStylesScoped$(ContactStyles);
  const formVisible = useSignal(false);
  const [, { Form, Field }] = useForm<ContactForm>({
    loader: useFormLoader(),
    action: useFormAction(),
    validate: zodForm$(contactSchema),
  });

  return (
    <article>
      <h2>Contact</h2>
      <button onClick$={() => (formVisible.value = true)}>Contact Me</button>

      {formVisible.value && (
        <Form>
          <Field name="name">
            {(field, props) => (
              <>
                <label for={field.name}>Your name</label>
                <input {...props} type="text" />
                {field.error && <div>{field.error}</div>}
              </>
            )}
          </Field>
          <Field name="message">
            {(field, props) => (
              <>
                <label for={field.name}>Your name</label>
                <textarea {...props} />
                {field.error && <div>{field.error}</div>}
              </>
            )}
          </Field>
          <button>Send</button>
        </Form>
      )}
    </article>
  );
});

When I submit the page for the first time it resets the form even though no redirect has occurred and there's validation errors. After the first submit it keeps the input values as expected.

Demo:

localhost_5173_contact_.mp4

[Qwik] Support for server$ in addition to action?

Would it be possible to set a server function rather than an action for validation? This would give us access to the current component scope. I imagine it would look like this:

import { $, component$ } from "@builder.io/qwik";
import { routeLoader$, z } from '@builder.io/qwik-city';
import { type InitialValues, formAction$, zodForm$ } from '@modular-forms/qwik';

const loginSchema = z.object({ โ€ฆ });

type LoginForm = z.input<typeof loginSchema>;

export const useFormLoader = routeLoader$<InitialValues<LoginForm>>(โ€ฆ);

export const useFormAction = formAction$<LoginForm>((values) => {
  // Runs on server
}, zodForm$(loginSchema));

export default component$(() => {
  const useServer = server$(() => {
     // Runs on server
  }, zodForm$(loginSchema));

  const [loginForm, { Form, Field }] = useForm<LoginForm>({
    loader: useFormLoader(),
    action: useFormAction(),
    server: useServer(),
    validate: zodForm$(loginSchema),
  });

  const handleSubmit: SubmitHandler<LoginForm> = $((values, event) => {
    // Runs on client
  });

  return (
    <Form onSubmit$={handleSubmit}>
      โ€ฆ
    </Form>
  );
}

This would also allow us to update the page without doing a full redirect. i.e. repopulate Resources

setting a object from select

considering the following;

export const relationSchema = z.object({
  id: z
    .string()
    .optional(),
  name: z
    .string()
    .min(1, 'required.')
    .regex(/^[a-zA-Z0-9]/, "Only characters and numbers are allowed"),
    country: z.object({
      id: z.string().optional(),
      code: z.string().optional(),
      name: z.string().optional()
    })
});

export type TRelation = z.input;

export const useRelationFormLoader = routeLoader$<InitialValues<TRelation>>(() => ({
  id: '',
  name: 'pony company',
  country: {
    id: '',
    code: '',
    name: ''
  }
}))
  const [relationForm, { Form, Field }] = useForm<TRelation>({
    loader: useRelationFormLoader(),
    validate: zodForm$(relationSchema)
  })
                <Field name="country">
                    {(field, props) => (
                      <SelectCountry
                        class="input-lg"
                        {...props}
                        value={field.value}
                        options={[{ id: 'fr', code: 'fr', name: 'france'}]}
                        error={field.error}
                        label="country"
                      />
                    )}
                  </Field>

name="country" gives a error;
Type '"country"' is not assignable to type '"id" | "name" | "country.id" | "country.name" | "country.code"'.ts(2322)

Question is can the <Field> handle this case of setting a object back...

Thanks upfront

[Qwik] Support Date in FieldValues

Hello,
I'm trying to select a date in my form but I've got some issues. I've tried with and without NoSerialize (note: Qwik does serialize Date)

Without NoSerialize

type TimestampForm = {
  date: NoSerialize<Date>;
}

// ERROR: Type 'Date' is not assignable to type 'FieldValues'
export const useTimestampAction = formAction$<TimestampForm>((values) => {
  console.log(values);
});

With NoSerialize

type TimestampForm = {
  date: NoSerialize<Date>;
}

// ERROR: Type 'Date & { __no_serialize__: true; }' is not assignable to type 'FieldValues'
export const useTimestampAction = formAction$<TimestampForm>((values) => {
  console.log(values);
});

Build error (qwik)

pnpm preview returns this error: โŒ TypeError [ERR_INVALID_MODULE_SPECIFIER]: Invalid module "@qwik-city-plan" is not a valid package name imported from C:\_work\websites\_sandbox\qwik-gfs\node_modules\.pnpm\@[email protected][email protected][email protected]\node_modules\@builder.io\qwik-city\index.qwik.mjs

Version:
@modular-forms/qwik: 0.1.1

System:
    OS: Windows 10 10.0.19045
    CPU: (20) x64 12th Gen Intel(R) Core(TM) i9-12900HK
    Memory: 11.06 GB / 31.68 GB
  Binaries:
    Node: 18.12.1 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.19 - C:\Program Files\nodejs\yarn.CMD
    npm: 8.19.2 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Edge: Spartan (44.19041.1266.0), Chromium (111.0.1661.44)
    Internet Explorer: 11.0.19041.1566
  npmPackages:
    @builder.io/qwik: 0.23.0 => 0.23.0
    @builder.io/qwik-city: 0.6.6 => 0.6.6
    undici: 5.21.0 => 5.21.0
    vite: 4.2.0 => 4.2.0

Update resources onSubmit?

Maybe this falls out of the remit of module-forms (more than likely) but I was wondering if you've encountered this scenario.

I have a basic search form that when the action is submitted I would like to update the resource with the filtered result. Is there a simple way to do this? Here's my code as an example:

export const useRestaurants = routeLoader$(async () => {
    return await prisma.restaurant.findMany();
});

export const useSearchFormLoader = routeLoader$<InitialValues<SearchForm>>(() => ({
    searchPhrase: "",
}));

export const useSearchFormAction = formAction$<SearchForm>((values) => {
    // I've got the search values here, but I don't know how to use them to update the restaurants list
}, zodForm$(searchSchema));

export default component$(() => {
    return (
        <Main>
            <H1>Discover Northern Ireland's Best Restaurants</H1>
            <P>
                Find the perfect spot for your next meal in Belfast, Derry, or anywhere in between
                with our directory of top-rated restaurants in Northern Ireland. Simply enter your
                address or location to browse our listings and discover everything from cozy local
                favorites to fine dining experiences. Our comprehensive directory makes it easy to
                treat your taste buds to a culinary adventure, with something for every taste and
                budget. Start exploring today!
            </P>
            <SearchForm />
            <DivGrid>
                <Resource
                    value={useRestaurants()}
                    onPending={() => <div>Loading restaurants...</div>}
                    onResolved={(restaurants) => (
                        <>
                            {restaurants &&
                                restaurants.map((restaurant) => (
                                    <RestaurantCard restaurant={restaurant} />
                                ))}
                        </>
                    )}
                />
            </DivGrid>
        </Main>
    );
});

Field with error is not focused when field is disabled while submitting

Hey ๐Ÿ‘‹,

It seems like Fields that have isDisabled={form.isSubmitting} can't be focused when an error is present. I think this is unexpected.

Example form:

type LoginForm = {
	email: string
	password: string
}
export const ContactForm: Component<ContactFormProps> = () => {
	const loginForm = createForm<LoginForm>()

	return (
		<Form
			class="space-y-12 md:space-y-14 lg:space-y-16"
			of={loginForm}
			onSubmit={values => alert(JSON.stringify(values, null, 4))}
		>
			<div class="space-y-8 md:space-y-10 lg:space-y-12">
				<Field
					of={loginForm}
					name="email"
					validate={[
						required("Please enter your email."),
						email("The email address is badly formatted."),
					]}
				>
					{field => (
						<input
							{...field.props}
							value={field.value || ""}
							disabled={loginForm.submitting}
							type="email"
							placeholder="[email protected]"
							required
						/>
					)}
				</Field>
				<Field
					of={loginForm}
					name="password"
					validate={[
						required("Please enter your password."),
						minLength(8, "You password must have 8 characters or more."),
					]}
				>
					{field => (
						<input
							{...field.props}
							value={field.value || ""}
							disabled={loginForm.submitting}
							type="password"
							placeholder="********"
							required
						/>
					)}
				</Field>
			</div>
			<button type="submit">Submit</button>
		</Form>
	)
}

Zod validation: `transform` results in type errors

I want to use the transform method of zod validation to transform some values into numbers. However if I do so, I get type errors for the validate option of createForm. It has most likely something to do with different input and output types zod is able to handle. Modular forms is passing the TFieldValues to theOutput generic type of ZodType in the definition of zodForm where it should probably pass it as Input generic type.

Dropping same file to field between resets doesn't trigger reactivity for field.value

Hi,

Reproduction

  1. Use either form with input field here https://modularforms.dev/playground/special.
  2. Drop a file to the input
  3. Reset the form with the button on the top
  4. Try to drop the same file into the input

Expected behaviour

The name of the file should be again visible in the field.

Actual behaviour

The name doesn't show. Instead, the label value stays.

Workaround

I had same problem and one obscure workaround that I found was:

<Field ...>
	{field => {
		// Line below solves the problem.
		field.value;

		return (
			<input ... />
		)
	}}
</Field>

Ability to pass dynamic values into Zod schema (qwik)

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 <></>;
})

[Qwik] Use Slot & useContext for custom component

[Food for thoughts]
As far as I understand Qwik, using children force the whole component to rerender at every changes (source) while Context seems to be the most efficient way to pass data (source: intro).

Based on that I think a good option would be to provide a FieldContext for custom component:
For example:

// Custom component using `useContext(FieldContext)`
const TextField = component$(({ type }) => {
  const { field, props } = useContext(FieldContext); // { field: { value, name, ... }, props: { onBlur$, onInput$, ... } }
  return <label>
    <Slot>
    <input {...props} type={type} value={field.value}/>
  </label>
});

export default component$(() => {
  ...
  return <Form>
    <FormField name="email">
      <TextField type="email">Enter your email</TextField>
    </FormField>
  </Form>
});

Note: I think this should be another api alongside Field's children as this useContext(FieldContext) would only work for @modular-forms/qwik native component.

The FormField component would look something like that :

const FieldContext = createContextId('FieldContext');
export const FormField = component$(({ name, ...props }) => {
  const { of: form } = props;
  const field = getFieldStore(form, name);
  const ref = $(() => ...);
  const onInput$ = $(() => ...);
  const onChange$ = $(() => ...);
  const onBlur$ = $(() => );
  useContextProvider(FieldContext, { ref, onInput$, onChanges$, onBlur$, ... });
  return <Lifecycle key={name} store={field} {...props}>
    <Slot />
  </Lifecycle>
})

I'm happy to provide a draft PR if you want :)

Support zod data transformation

Hi, awesome library!

Is your feature request related to a problem? Please describe.
I would like the option of transforming my form data with the same zod validation schema

Describe the solution you'd like

const schema = z.object({
  currency: z.string().length(3),
  amount: z.number().nonnegative().transform(amount => amount * 100),
});

const form = createForm(
  validate: zodForm(schema)
);

const onSubmit = data => {
    console.log(data.amount); // should be the user's input times 100
};

Describe alternatives you've considered
Passing a transform option to the schema validator, although IMO this should be an opt-out behavior since transforming data with zod is really common

const form = createForm(
  validate: zodForm(schema, { transform: true })
);

Passing a transform function

const form = createForm(
  validate: zodForm(schema)
  transform: zodTransform(schema)
);

Note that in this case zodForm could also be renamed to zodValidate to better reflect what it does here

Additional context
I'm currently able to achieve the same by just parsing the values again in the onSubmit which shouldn't fail since it was already validated with that same schema.

Validating optional fields

Hi ๐Ÿ‘‹,

I couldn't find any info according to validating optional fields. For example, I have an email validation, but the user can decide not to add an email, but validation requires it.

I did a workaround by creating a custom validator, but I just wondered whether I couldn't find it or if you decided not to include it in the library.

PS: Thank you for this library; it's great!

Type 'Date' is not assignable to type FieldValues

Is there a way to use Date types in modular-forms? like this:

const customerSchema = z.object({
  customerId: z.coerce.number().int().positive(),
  firstName: z.coerce.string().max(45),
  lastName: z.coerce.string().max(45),
  email: z.coerce.string().max(50).email(),
  lastUpdate: z.coerce.date().nullish(),
});

const customerForm = createForm({
  validate: zodForm(customerSchema),
});

Error:

Property 'lastUpdate' is incompatible with index signature.
  Type 'Date | null' is not assignable to type 'FieldValues | FieldValue | FieldValue[] | FieldValues[]'.
    Type 'Date' is not assignable to type 'FieldValues | FieldValue | FieldValue[] | FieldValues[]'.
      Type 'Date' is not assignable to type 'FieldValues'.
        Index signature for type 'string' is missing in type 'Date'.

validate: zodForm(customerSchema),
                  ~~~~~~~~~~~~~~

Cannot use SUID (Material UI) components

Hello, I recently started using solidjs for my project. I used SUID (Material UI for SolidJs) and wanted to use this library for form validations. When I pass regular input in Form component, everything works as expected. But when I use TextField from SUID instead, I cannot submit the form as the form has no values filled out. It took me couple of hours to debug this issue. It is due to SUID having different event value from onChange and onInput that this library needs. Is there any elegant workaround (using Typescript) to pass this issue? I have an working example code, but it gives me couple of typescript warnings and Im not sure, if overwriting some values from target and currentTarget will not mess up anything else.

      <Form of={loginForm} onSubmit={handleSubmit}>
          <Field
            of={loginForm}
            name="email"
          >
            {(field) =>
                <TextField
                  {...field.props}
                  type="email"
                  // Reassign onInput and onChange, overwrite events
                  onInput={(e) => field.props.onInput({ ...e, currentTarget: { ...e.currentTarget, value: e.target.value } }) }
                  onChange={(e) => field.props.onInput(e)}
                  variant="outlined"
                  component="input"
                  label="E-mail"
                  required
                  error={Boolean(field.error)}
                  helperText={field.error}
                  value={field.value || ''}
                />}
          </Field>
          <div class="signin-form--button">
            <Button
              type="submit"
              fullWidth
              color="secondary"
              variant="contained"
            >
              Sign in
            </Button>
          </div>
        </Form>

Return additional data from formAction$ (qwik)

Is there a way to return additional data from formAction$? Only status and message is returned.

export const useFormAction = formAction$<SignupForm>(async (values, event) => {
  // create account and stuff

  return {
    status: 'success',
    message: 'Account created successfully!',
    data: {
      user: {
        id: 1,
        name: 'Bill',
      },
      cart: {
        quantity: 2,
      }
    },
  };
});

// component$
{signupForm.response.status === 'success' ? (
  <div>Thanks for signing up {signupForm.response.data.user.name}!</div>
) : (
  <SignupForm form={signupForm} />
)}

Storybook integration bug.

Hey I am hitting same error as #8.
I am using storybook with vite plugin. Vite looks something like this.

async viteFinal(config, { configType }) {
    config.plugins.unshift(Solid({ hot: false }))
    config.plugins.unshift(WindiCSS.default())
    config.plugins.unshift(tsconfigPath({ root: path.resolve(__dirname, '../') }))
    config.optimizeDeps = undefined
    const a = mergeConfig(config, {
      define: {
        'process.env.NODE_DEBUG': 'false'
      }
    })
    console.log(a)
    return {
      ...a,
      optimizeDeps: undefined
    }
  }

As you can see i tried removing optimizeDeps but that did not worked for me.

setValues as a method

Looking at the api docs we have a setValue but no setValues.

what i like todo is open a modal which hold a form and set all the values at once's from a object given as argument.

atm i'm doing it like this

  const openModal = $((country: ICountry) => {
      modal.isOpen = true,
      modal.title = 'country',
      countryForm.internal.fields.id.value = country.id,
      countryForm.internal.fields.code.value = country.code,
      countryForm.internal.fields.name.value = country.name
  });

But would like todo something like this

  const openModal = $((country: ICountry) => {
      modal.isOpen = true,
      modal.title = 'country',
      countryForm.values = country
       or
      setValues('countryForm', country)
  });

I'm i overlooking something in the api docs ?

required validation is skipped

Hello,

I'm trying to have a reactive value of two form fields, which I managed to do with the following:

createMemo(() => {
    const firstName = useField(form, "firstName").value;
    const lastName = useField(form, "lastName").value;
    return `${firstName} ${lastName}`
  })

However, when I submit fistName cand lastNamed required validation get skipped.

Not sure, maybe required isn't checking for empty string ๐Ÿชต or useField influences validation and creating a bug.

commenting out useField fixes the issue and required validation works

Regards

Nested form example better clarity

In Nested form under playground page, I assume that the buttons "Move first to end" and "Swap first two" to swap any items, when it only swaps Option items. This lead me to believe that this playground example or library was broken.

Easy solution is to include "Option" in button text, such as "Move first Option to end" and "Swap first two Options". But I know that visually it's not pleasing to see button with a lot of text, so I don't know the solution to make this example aseptically clearer.

Screenshot 2023-02-21 at 11 11 59 AM

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.