fabian-hiller / modular-forms Goto Github PK
View Code? Open in Web Editor NEWThe modular and type-safe form library for SolidJS, Qwik and Preact
Home Page: https://modularforms.dev
License: MIT License
The modular and type-safe form library for SolidJS, Qwik and Preact
Home Page: https://modularforms.dev
License: MIT License
It would be awsome if it runs and validates on the server side too...
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.
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!
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,
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
modular-forms/packages/qwik/src/actions/formAction$.ts
Lines 106 to 113 in 43c304c
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
I have some external classes define the shape of some forms, as they are classes, inferring the shape of a Zod schema is non-trivial, I have previously used https://github.com/typestack/class-validator for validation, but I have run into the situation outlined in #2 are there any other workarounds that could enable this?
input type="radio" doesn't work with field.onInput prop. But if I do
<input type="radio" onInput={(e) => setValue(props.form, field.name, e.target.value)} />
it starts working.
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?
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!
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),
);
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.
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
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?
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?
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.
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)
Since anybody can send anything, it seems like a security issue to not require validation.
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.
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
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
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)
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 ?
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
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 (
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.
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
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:
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
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
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)
type TimestampForm = {
date: NoSerialize<Date>;
}
// ERROR: Type 'Date' is not assignable to type 'FieldValues'
export const useTimestampAction = formAction$<TimestampForm>((values) => {
console.log(values);
});
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);
});
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
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>
);
});
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>
)
}
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.
Hi,
The name of the file should be again visible in the field.
The name doesn't show. Instead, the label value stays.
I had same problem and one obscure workaround that I found was:
<Field ...>
{field => {
// Line below solves the problem.
field.value;
return (
<input ... />
)
}}
</Field>
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 <></>;
})
[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 :)
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.
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!
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),
~~~~~~~~~~~~~~
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>
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} />
)}
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.
It is a convention for solid-js packages to ship the source code / JSX as an extra export, so solid can bundle the code itself in the end. The tweet mentions the solid router package.json as an example, but in the solid-lib-starter package.json they are doing even a few more exports.
Could this be added to modular-forms?
I want to use the submitter
property of a SubmitEvent
and found no reason why the event argument passed to the onSubmit
callback of Form
currently is an Event
.
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 ?
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
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.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.