Coder Social home page Coder Social logo

seasonedcc / remix-forms Goto Github PK

View Code? Open in Web Editor NEW
472.0 8.0 24.0 5.34 MB

The full-stack form library for Remix and React Router

Home Page: https://remix-forms.seasoned.cc

License: MIT License

TypeScript 97.87% JavaScript 1.47% Dockerfile 0.31% CSS 0.12% HTML 0.09% MDX 0.14%
form forms react-hook-form react-router react-router-dom react-router-dom-v6 react-router-v6 remix remix-run zod

remix-forms's People

Contributors

andrepinho avatar bvangraafeiland avatar danielweinmann avatar diogob avatar felipefreitag avatar gustavoguichard avatar jacob-ebey avatar kaciakmaciak avatar reptoxx avatar vedovelli 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  avatar

remix-forms's Issues

Send array fields using dot notation from react-hook-form don't work

Hello Guys,

So, I'm trying to create a form with a collection of items. I encountered two problems:

  1. The component Errors on Field children does not return an error from my collection items for my item name.
    I only can get error messages for this field when I use formState.errors.

  2. When I submit the valid form the FormData send the data in this shape: { name: '', 'items.0.name': '' }.
    Debugging, I discovered that the parser of form date used on remix-domains inside remix-forms use the lib qs to parse as query-string that lib accepts only brackets notation.

I changed the notation to brackets, I can reach the mutation with correct json structure as { name: '33', items: [ { name: '3' } ] } and typescript complains about the name of the field.

    Argument of type '"items[0][name]"' is not assignable to parameter of type '"name" | "items" | "organization_id" | `items.${number}` | `items.${number}.name`'.ts(2345)

Component

import type { ActionFunction } from "@remix-run/node";
import { formAction } from "remix-forms";
import Input from "~/components/app/Input";
import Form from "~/components/Form";
import { createNewPlan } from "~/domain/plans/createPlan";
import { PlanSchema } from "~/domain/plans/schema";
import { requireOrganizationId, requireUserId } from "~/session.server";

export const action: ActionFunction = async ({ request }) => {
  const { organizationId } = await requireOrganizationId(request);

  // when uses dot notation print { name: '', 'items.0.name': '' }
  console.log(Object.fromEntries(await request.clone().formData()));

  return formAction({
    request,
    schema: PlanSchema,
    mutation: createNewPlan,
    successPath: "plans",
    environment: { organization_id: organizationId },
  });
};

export default function NewPlanPage() {
  return (
    <div className="rounded-xl p-6">
      <Form
        schema={PlanSchema}
        values={{
          items: [],
        }}
      >
        {({ Field, Button, Errors, register, formState }) => (
          console.log(formState.errors),
          (
            <>
              <Field name="name" />
              <Field name="items">
                {({ Label, Errors, errors }) => (
                  console.log(errors, `errors`),
                  (
                    <>
                      <Label />
                      <Input type="text" {...register(`items.0.name`)} />
                      <Errors />
                      <p>{formState.errors.items?.[0]?.name?.message}</p>
                    </>
                  )
                )}
              </Field>
              <Errors />
              <Button />
            </>
          )
        )}
      </Form>
    </div>
  );
}

Schema:

import z from "zod";
import type { Merge } from "type-fest";
import type { Base } from "~/types/common/base";

export const PlanSchema = z.object({
  name: z.string().min(1, `name required`),
  items: z.array(
    z.object({
      name: z.string().min(1, "required name in items"),
    })
  ),
  organization_id: z.string().optional(),
});

export type Plan = Merge<PlanForm, Base>;
export type PlanForm = z.infer<typeof PlanSchema>;

Mutation:

export const createNewPlan = makeDomainFunction(
  PlanSchema,
  EnvironmentSchema
)(
  async (values, env) => console.log(values)
);

Bug: typecheck fails

tsc output:

node_modules/remix-forms/src/getFormValues.ts:16:56 - error TS2345: Argument of type 'string | string[] | ParsedQs | ParsedQs[] | undefined' is not assignable to parameter of type 'FormDataEntryValue | null'.
  Type 'undefined' is not assignable to type 'FormDataEntryValue | null'.

16     values[key as keyof z.infer<Schema>] = coerceValue(value, shape)

How to handle error on resource route?

Hi, I'm just wondering if there's a good way to use formAction in a resource route.

My use case is that I have a page with multiple different forms. Ideally, each of the forms will be handled by a resource route for reusability. I imagine the behavior would be each resource route will redirect back to the original page with the errors somehow and the form can pick it up and display the error message correctly. I'm not sure if this is technically possible (maybe with a form prefix?).

I'm just wondering if I'm missing something here. Would love to hear your thoughts! Thanks

Set value with useState

Hello,

How i can set a hidden field value with a React useState. When i changed state it doesn't update the field value.

I do that :
Screenshot 2022-07-20 at 12 09 14

Support for input type datetime-local

I'm trying to use an <input type="datetime-local" /> (MDN) in my form, and it doesn't seem possible with how Remix Forms is implemented.

Here's my schema:

// Copied from zod docs
// https://github.com/colinhacks/zod#dates
const dateSchema = z.preprocess((arg) => {
  if (typeof arg == "string" || arg instanceof Date) return new Date(arg);
}, z.date());

export const schema = z.object({
  title: z.string().min(1, { message: "Title is required" }),
  details: z.string().min(1, { message: "Details are required" }),
  startTime: dateSchema,
  endTime: dateSchema,
  maxAttendees: z.number().min(2).optional(),
  free: z.boolean(),
});

And here's how I'm trying to use these fields in my form:

<Form schema={schema}>
  {({ Field, Errors, Button }) => (
    {/* Rest of form is excluded */}
    <Field name="startTime" label="Starts at" type="datetime-local" />
    <Field name="endTime" label="Ends at" type="datetime-local" />
  )}
</Form>

When I try to save, I get an error Invalid date on these inputs. Changing the input type to "date" fixes the error.

I put a console.log() inside of z.preprocess() to see the value of arg and it prints Invalid Date in my console instead of the date string. I think what's happening is this function in your code is trying to coerce the string to a date (code assumes there's no timestamp) and it returns "Invalid Date" before it can make it to zod's preprocess function.

I also tried updating my schema to just use z.date() for startTime and endTime but I still get an Invalid date error.

If there's some way to skip the default coercion behavior, that would probably work. Or maybe it'd better if Remix Forms just natively supported <input type="datetime-local" />.

Errors not shown with zod refine

I have a schema with a refinement:

const schema = z.object({
    email: z.string().min(1).email(),
    password: z.string().min(1),
    passwordConfirmation: z.string().min(1)
}).refine((data) => data.password === data.passwordConfirmation, {
    message: 'Passwords do not match',
    path: ['passwordConfirmation'],
})

I would have expected this to work with remix-forms, ideally with the error either being global or attached to the field belonging to the path. However it is not working. I see a typescript error when passing the schema:

Type 'ZodEffects<ZodObject<{ email: ZodString; password: ZodString; passwordConfirmation: ZodString; }, "strip", ZodTypeAny, { email: string; password: string; passwordConfirmation: string; }, { ...; }>, { ...; }, { ...; }>' is missing the following properties from type 'ZodObject<ZodRawShape, UnknownKeysParam, ZodTypeAny, any, any>': _shape, _unknownKeys, _catchall, _cached, and 16 more.

During runtime, the validation somehow works (if I print during the refine step, I can see that the validation is executed) and the form is submitted. However, validation errors are not shown and I do not get any runtime error / exception (in the browser or on the server side).

Docs: explain differences between fieldError, error, and globalError

While working with Remix forms I'm having trouble understanding which custom component is rendered when.

For example, when does remix-forms render the fieldErrorsComponent, when the globalErrorsComponent and when the errorComponent.

E.g. is the errorComponent the default component, but when a fieldErrorsComponent is specified and there are field specific errors, then that gets rendered instead of the errorComponent and when there are global errors then the globalErrorsComponent gets rendered? So when both fieldErrorsComponent and globalErrorsComponent are specified, errorsComponent is superflous?

It would be cool if the docs could answer that question.

Pass result into beforeSuccess callback along with request?

It would be nice if the result was passed in to this callback along with the request
https://github.com/SeasonedSoftware/remix-forms/blob/896ab6c5077918d19646f3f3c7017ac8f4c2b014/src/formAction.server.ts#L99

This would allow for something along the following lines:

export const action: ActionFunction = async ({ request }) =>
  formAction({
    request,
    schema,
    mutation,
    beforeSuccess: async ({ request, data }) => {
      const session = await getSession(request.headers.get("Cookie"));
      session.set("userId", data.userId);
      return redirect("/", {
        headers: { "Set-Cookie": await commitSession(session) },
      });
    },
  });

Would this be any better than just using performMutation directly? Seems like it might be a tiny bit nicer since by using beforeSuccess, the failure path doesn't need to be re-implemented.

export const action: ActionFunction = async ({ request }) => {
  const result = await performMutation({ request, schema, mutation });

  if (!result.success) {
    return json(result, 400);
  }

  const session = await getSession(request.headers.get("Cookie"));
  session.set("userId", result.data.userId);
  return redirect("/", {
    headers: { "Set-Cookie": await commitSession(session) },
  });
};

Edit: I think I do prefer to use performMutation directly for this case. I guess I am wondering what the ideal use-case for beforeSuccess would be but I'll go ahead and close this issue for now.

Fields with value equal to `0` are shown as empty

Here is a code example:

const schema = z.object({
  _method: z.literal('drop-time'),
  index: z.number().int(),
})

<Form
  schema={schema}
  method="post"
  values={{
    _method: 'drop-time',
    index: 0
  }}
/>

`successPath` based on an item created in the mutation

Question B

Similarly on the schema - I need to provide the enums/options to the user based on the DB entries:

const schema = z.object({
  productCategory: z.enum(["food", "soft-drink", "cosmetics", "other"]), // -- how to get these from the DB? ๐Ÿค”
  // ...
});

Fingers crossed I haven't missed something super obvious, haha ๐Ÿคž๐Ÿผ (guess I did miss the performMutation example for my Question A ๐Ÿ˜…)

Hey folks!

Was great meeting y'all in Remix Conf + so far loving the npm/Remix Form ๐Ÿฅณ

Question A

How can I define a dynamic successPath based on some value in the mutation.

Here are the relevant parts:

const mutation = makeDomainFunction(schema)(
  async (values) => {
    const createdItem = await createItem({
      name: "Will it Hurt Me?",
      brandName: "Silky Smooth",
    })

    return createdItem;
  }
);

and the action:

export const action: ActionFunction = async ({ request }) => {
  return formAction({
    request,
    schema,
    mutation,
    successPath: "/", // ---- this is where I would like to be able to use for example createdItem.slug (which I wouldn't pass from the form, but rather generate on the server)
  });
}

Transforms are not being applied on server side

Curiously transforms are not working on server site. I can see on the request payload that transforms are working fine on client side but when it gets to the serve they are ignored. I don't see why we want to have transforms on the client though. If I just transform a string to number on the client side, it should throw an error on the server since the validation would need a string instead of a number.

Mutations are not coercing nested z.object

Just tried a quick example with formAction mutations but it seems like the day prop from the schema below is not coercing as expacted.

const TimeSchema = z.string().regex(/\d{4}/)

 enum WeekDays {
    Sunday = 0,
    Monday = 1,
    Tuesday = 2,
    Wednesday = 3,
    Thursday = 4,
    Friday = 5,
    Saturday = 6
}

const schema = z.object({
  period: z.object({
    day: z.nativeEnum(WeekDays),
    from: TimeSchema,
    until: TimeSchema,
  }),
})

The form sends data correctly and inputFromForm function seems to be loading the data from formData correctly. I think the problem is on getFormValues that only loop through the first level of keys
Screen Shot 2022-05-31 at 3 06 09 PM

Default Values

I have noticed that just setting the value prop e.g. value="myDefaultValue" yields VERY odd results especially when routing to another route with the same form, sometimes the value is not changed to the new default value.

To clarify, my use case is the following.

After submit I redirect to the /new page which is essentially the form however the <Outlet /> renders it with the default value set as value="", in this specific situation the form is not cleared.

The same happens when moving from route to route within the same when it tries to render the form with value="" i.e. trying to clear it.

Custom styles example?

Can someone point me to how can custom styles be applied? I'm using Tailwind.

I'm extending the form as per docs but I can't figure out how to add the className to the element

 multilineComponent={"select"} /* your custom Multiline */

Thank you!

feat: add possibility to render enums as radios

Feature Request

Current Behavior

Enums are always rendered as <Select /> components.

Desired Behavior

There should be a way to configure enums to be rendered as radio components as well as how the radio group should be rendered, instead of rendering them as selects.

For example:

import { forwardRef } from 'react';
import type { FormProps } from 'remix-forms';
import { Form as RemixForm } from 'remix-forms';
import type { SomeZodObject } from 'zod';

const Radio = forwardRef<HTMLInputElement, JSX.IntrinsicElements['input']>(
  ({ type = 'radio', ...props }, reference) => (
    <input
      className="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
      ref={reference}
      type={type}
      {...props}
    />
  ),
);

const Select = forwardRef<HTMLSelectElement, JSX.IntrinsicElements['select']>(
  (props, reference) => (
    <select
      className="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
      ref={reference}
      {...props}
    />
  ),
);

const Form = <Schema extends SomeZodObject>(props: FormProps<Schema>) => (
  <RemixForm<Schema>
    className="space-y-8 divide-y divide-gray-200"
    radioComponent={Radio}
    selectComponent={Select}
    renderRadioGroup={({ Radio, Label, options }) => (
      <div className="mt-4 space-y-4">
        {options.map(({ name, value }) => (
          <div className="flex items-center">
            <Radio value={value} />
            <Label>{name}</Label>
          </div>
        )}
      </div>
    )}
    {...props}
  />
);

Note: We have to flush out the details here. In a real form with radio inputs, you usually have labels and the inputs linked using ids and htmlFor tags.

And then you can use it like this:

import Form from '~/where-your-form-lives';
import { z } from 'zod';

const schema = z.object({
  country: z.enum(['United States', 'Canada', 'Mexico']),
  pushNotifications: z.enum([
    'Everything',
    'Same as email',
    'No push notifications',
  ]),
});

export default MyPage() {
  return (
    <Form radio=[['pushNotifications']] schema={schema}>
      {({ Field, Button }) => (
        <>
          <Field name="country" />
          <Field name="pushNotifications" />
          <Button />
        </>
      )
    </Form>
  )
}

Alternatives Considered

You can hack your input fields today by using register and rendering your own Radios and Labels - like this:

<fieldset className="mt-6">
  <legend className="contents text-base font-medium text-gray-900">
    Push Notifications
  </legend>

  <p className="text-sm text-gray-500">
    These are delivered via SMS to your mobile phone.
  </p>

  <Field
    className="mt-4 space-y-4"
    name="pushNotifications"
  >
    {({ Errors }) => (
      <>
        <div className="flex items-center">
          <Radio
            {...register('pushNotifications')}
            checked
            id="push-everything"
            value="Everything"
          />

          <Label
            className="ml-3 block text-sm font-medium text-gray-700"
            htmlFor="push-everything"
          >
            Everything
          </Label>
        </div>

        <div className="flex items-center">
          <Radio
            {...register('pushNotifications')}
            id="push-email"
            value="Same as email"
          />

          <Label
            className="ml-3 block text-sm font-medium text-gray-700"
            htmlFor="push-email"
          >
            Same as email
          </Label>
        </div>

        <div className="flex items-center">
          <Radio
            {...register('pushNotifications')}
            id="push-nothing"
            value="No push notifications"
          />

          <Label
            className="ml-3 block text-sm font-medium text-gray-700"
            htmlFor="push-nothing"
          >
            No push notifications
          </Label>
        </div>

        <Errors />
      </>
    )}
  </Field>
</fieldset>

However, that is very tedious and takes a lot of code.

throw multiple fields errors from mutation

Hey ๐Ÿ‘‹๐Ÿผ

In my mutation, I'm calling a distant API where validation (not covered by zod) takes place.

In case of problems, the API responds with a 422 error and a JSON describing all problems like this:

{
  "username": "This username have been taken",
  "email": "Your email provider is banished from this service"
}

I've seen that I can use throw new InputError('Error message', 'field') for a single field, but is there a way to throw for multiple fields simultaneously?

Thanks !

Unable to make checkbox required

I am trying to make a checkbox required to be true within the zod schema, but the form is still submitting no matter what I have tried so far. There is an example that shows this on the website, but it just sets the default to false. It has a required indicator, but accepts either true or false.

Is there a way to do this currently?

How to use the "register" hook with a nested component?

I am using Remix Forms with Mantine components. I have to use the Mantine rich text editor and that cannot be server rendered. So in order to render it in client, I have a wrapper around it so it only renders when it is in the browser.

Since it is the wrapper I will be using, ...register(descriptio) does not bind it properly to the control. Although rich text editor is not a simple HTML component, I would want to know how to integrate validations with those type of elements.

I know I am missing something obvious, any help is appreciated. Thanks

richtexteditor.tsx

import type { RichTextEditorProps } from "@mantine/rte";

export function RichText(props: RichTextEditorProps) {
  if (typeof window !== "undefined") {
    // eslint-disable-next-line import/extensions, global-require
    const { RichTextEditor } = require("@mantine/rte");
    return (
      <RichTextEditor
        controls={[
          ["bold", "italic", "underline", "link"],
          ["unorderedList", "h1", "h2", "h3"],
          ["sup", "sub"],
          ["alignLeft", "alignCenter", "alignRight"],
        ]}
        {...props}
      />
    );
  }

  // Render anything as fallback on server, e.g. loader or html content without editor
  return null;
}

usage in page.tsx inside remix form

<Field name="description" className="mt-5 mb-1">
  {({ Errors }) => (
    <>
      <RichText
        onChange={(event: any) => {}}
        value={description}
        placeholder=""
        className="mt-1 mb-1"
        classNames={{
          root: "min-h-[200px] border-gray-300",
        }}
      />
      <Errors className="mt-1 text-xs font-semibold text-red-500" />
    </>
  )}
</Field>

Feature: missing types for coercion

THIS IS MORE A DISCUSSION THAN PROPERLY AN ISSUE.

I'm kinda implementing the formAction for my remix app and came across with the following situation.

Our form is quite complicated with few dynamic pieces and with some transformations to make the data more appropriate for Prisma and other libraries.
Screen Shot 2022-06-03 at 7 12 15 PM

In this particular form, we have a couple of reusable schemas and some really complex ones...
As an example:

export const DaytimeRangeSchema = z.preprocess(
  // The field are setting their own index to be able to use the validation
  // This can cause a empty array to be set as default value
  // So we set empty indexes to undefined to keep their order
  arg => Array.apply(0, (arg as []) ?? []),

  // Validate the array
  TimeRangeSchema.array()
    .default([])
    // remove undefined values on server
    .transform(arg =>
      typeof document === 'undefined' ? arg.filter(a => !!a) : arg,
    )
    // Prisma friendly
    .transform(data => data as Prisma.JsonArray),
)

Now, I know this kind of dynamic form is not really "remix way" of doing things but sometimes we'll need to sacrifice the "javascript disabled" in exchange of having a better UX. With that said, coercion should be able to find out what kind of inputs it are dealing with and then coerce it.

I'm not sure exactly how much of these types are really necessary but I kinda have a feeling that some importants are left behind.

Those are the following types I managed to get:

  • ZodAny
  • ZodArray
  • ZodBigInt
  • ZodBoolean
  • ZodDate
  • ZodDefault
  • ZodDiscriminatedUnion
  • ZodDiscriminatedUnionOption
  • ZodEffects
  • ZodEnum
  • ZodFunction
  • ZodIntersection
  • ZodLazy
  • ZodLiteral
  • ZodMap
  • ZodNaN
  • ZodNativeEnum
  • ZodNever
  • ZodNonEmptyArray
  • ZodNull
  • ZodNullable
  • ZodNumber
  • ZodObject
  • ZodOptional
  • ZodPromise
  • ZodRawShape
  • ZodRecord
  • ZodSet
  • ZodString
  • ZodTuple
  • ZodTupleItems
  • ZodUndefined
  • ZodUnion
  • ZodUnknown
  • ZodVoid

Error: cannot clone body after it is used

Remix throws an error Error: cannot clone body after it is used if I try to access the formData before calling the formAction method

const formData = await request.formData()

Here is a code to reproduce it:

import { ActionFunction } from "@remix-run/server-runtime"
import { InputError, makeDomainFunction } from "remix-domains"
import { Form, formAction } from "remix-forms"
import { z } from "zod"

const schema = z.object({
  email: z.string().nonempty().email(),
})

const takenEmails = ["[email protected]", "[email protected]"]

const mutation = makeDomainFunction(schema)(async (values) => {
  if (takenEmails.includes(values.email)) {
    throw new InputError("Email already taken", "email")
  }

  return values
})

export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData()

  return formAction({ request, schema, mutation })
}

export default () => <Form schema={schema} />

`useFormContext` should be available through children

Since we are using react-hook-form. It would be nice to have access to stuff like isSubmitting from the form children without having to pass it down the three.

Right now, the form is controlling the label of the button to indicate the form is submiting but if you are working with UI libs like Chakra, you might want to use somethnig like <Button isLoading />

Bug: date inputs display wrong day (local timezone vs. UTC)

When a date is pushed and read again it shows as one day earlier, because:

Possible fix: always use UTC:

diff --git a/node_modules/remix-forms/lib/coercions.js b/node_modules/remix-forms/lib/coercions.js
index 0e5b886..5208adb 100644
--- a/node_modules/remix-forms/lib/coercions.js
+++ b/node_modules/remix-forms/lib/coercions.js
@@ -17,10 +17,9 @@ const coerceString = makeCoercion(String, '');
 const coerceNumber = makeCoercion(Number, null);
 const coerceBoolean = makeCoercion(Boolean, false);
 const coerceDate = makeCoercion((value) => {
-    if (typeof value !== 'string')
+    if (typeof value !== 'string' || !/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(value))
         return null;
-    const [year, month, day] = value.split('-').map(Number);
-    return new Date(year, month - 1, day);
+    return new Date(value);
 }, null);
 function coerceValue(value, shape) {
     const { typeName, optional, nullable } = (0, shapeInfo_1.shapeInfo)(shape);

Nested objects and arrays

Really interesting library already!

There does not seem to be support for nested objects and arrays yet, AFAICT.

For example based on a schema like this:

const schema = z.object({
  name: z.string(),
  contact: z.object({ email: z.string().nonempty().email() }),
  addresses: z.array(
    z.object({
      address: z.string(),
      country: z.string()
    })
  ),
});

It seems like it should be pretty easy to do this using register etc. from react-hook-form, but it would be nice to have support for that in remix-forms.

Add `revalidateMode` prop to `Form`

this is regarding: https://remix-forms.seasoned.cc/examples/actions/field-error

If I enter [email protected] and submit the form it returns with Email already taken. => Ok
If I change the email to foo the error changes to Invalid email without requiring a submit => not sure if this is a bug or a feature. I think it should only re-validate if I use mode="onChange"
If I now change the email to a valid one like [email protected] the old/cached error appears: Email already taken => Not Ok. I think this is confusing, as it might lead to the assumption that [email protected] is taken too.

My proposal would be to allow resetting errors

Docs: i18n and localization with Zod

How do you do internationalization for the error messages?

Let's say you have a schema:

const schema = z.object({
  name: z
    .string()
    .min(3, 'A name is required and needs to be at least 3 characters long.'),
});

The leading library for i18n for Remix is currently remix-i18next. It requires the use of a t function, which is available through the useTranslation hook.

However, that t value is in a component. The schema needs to be defined outside of the component, so it can be used in the action function. How would you solve this?

Add example when adding InputComponent

I'm using Mantine and want to supply it's own Input control, rather than the default html input.

https://v5.mantine.dev/core/input/

So I do something like:

import { Input } from "@mantine/core"

<Form
            inputComponent={Input}
            schema={schema}

It actually works, But I get a typescript error:

(property) inputComponent?: string | ForwardRefExoticComponent<Pick<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "key" | keyof InputHTMLAttributes<...>> & RefAttributes<...>> | undefined
Type '(<C = "input">(props: PolymorphicComponentProps<C, InputProps>) => ReactElement<any, string | JSXElementConstructor<any>>) & { ...; }' is not assignable to type 'string | ForwardRefExoticComponent<Pick<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "key" | keyof InputHTMLAttributes<...>> & RefAttributes<...>> | undefined'.
  Property '$$typeof' is missing in type '(<C = "input">(props: PolymorphicComponentProps<C, InputProps>) => ReactElement<any, string | JSXElementConstructor<any>>) & { ...; }' but required in type 'ForwardRefExoticComponent<Pick<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "key" | keyof InputHTMLAttributes<...>> & RefAttributes<...>>'.ts(2322)
index.d.ts(361, 18): '$$typeof' is declared here.
Form.tsx(75, 3): The expected type comes from property 'inputComponent' which is declared here on type 'IntrinsicAttributes & FormProps<ZodObject<{ email: ZodString; password: ZodString; }, "strip", ZodTypeAny, { email: string; password: string; }, { ...; }>>'

Please can you add an example in the get started section to supply a custom control, rather than breaking out into a 100% custom input.

Thanks!

About lodash

image

remix-form is only importing concat and startCase from lodash, but depends on the whole package, which is not tree-shakeable. What about switching to lodash-es or to individual packages (e.g. lodash.concat)? OR... moving away from lodash?

Can do a PR.

feat: allow SmartInput and Field to receive `autoComplete` prop

Feature request

Current Behavior

Right now, there is no way to give a Field or a SmartInput an autoComplete prop, e.g. autoComplete="username".

Let's say you have a custom Input configured and a Zod schema like this:

import { forwardRef } from 'react';
import type { FormProps } from 'remix-forms';
import { Form as RemixForm } from 'remix-forms';
import type { SomeZodObject } from 'zod';
import { z } from 'zod';

const Input = forwardRef<HTMLInputElement, JSX.IntrinsicElements['input']>(
  ({ type = 'text', ...props }, reference) => (
    <input
      className="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
      ref={reference}
      type={type}
      {...props}
    />
  ),
);

const Form = <Schema extends SomeZodObject>(props: FormProps<Schema>) => (
  <RemixForm<Schema>
    className="space-y-8 divide-y divide-gray-200"
    inputComponent={Input}
    {...props}
  />
);

const schema = z.object({
  firstName: z.string().min(1),
  email: z.string().min(1).email(),
})

And you want to have an input with an autoComplete property, you have to render it like this:

<Field className="sm:col-span-3" name="firstName">
  {({ Label, Errors }) => (
    <>
      <Label>First name</Label>

      <Input
        {...register('firstName')}
        autoComplete="given-name"
      />

      <Errors />
    </>
  )}
</Field>

<Field className="sm:col-span-3" name="emailAddress">
  {({ Label, Errors }) => (
    <>
      <Label>Email address</Label>

      <Input
        {...register('emailAddress')}
        autoComplete="email"
      />

      <Errors />
    </>
  )}
</Field>

This renders using SmartInput as well as configuring inputComponent on the Form useless in these cases.

Desired Behavior

Ideally, we could give the Field or the SmartInput the autoComplete property, so we could render:

<Field className="sm:col-span-3" name="firstName">
  {({ Label, SmartInput, Errors }) => (
    <>
      <Label>First name</Label>

      <SmartInput autoComplete="given-name" />

      <Errors />
    </>
  )}
</Field>

<Field autoComplete="email" className="sm:col-span-3" name="emailAddress" />

(Note that in this case, we could also render firstName straight up as a Field, but I wanted to show the "Custom Field, standard components" version, too.)

We also have to consider this for the Form's renderField prop, e.g.:

renderField={({ Field, autoComplete, ...props }) => {
  const { name } = props;

  return (
    <Field key={name as string} {...props}>
      {({ Label, SmartInput, Errors }) => (
        <>
          <Label />

          <div className="mt-1">
            <SmartInput autoComplete={autoComplete} />
          </div>

          <Errors />
        </>
      )}
    </Field>
  );
}}

Alternatives

Alternatively, maybe there is a way to infer the autoComplete prop from the schema for certain all the keys that are possible autoComplete values.

Feature: custom input registration

I think we could improve the DX a bit just by exporting the registered input already as a prop.

Currently if we want to make a custom input we need to do this:

<Field name="email">
  {({ Label, Errors }) => (
    <>
      <Label />
      <input
        type="email"
        {...register('email')}
        className="border-2 border-dashed rounded-md"
      />
      <Errors />
    </>
  )}
</Field>

Since the Field has the name it would make sense to just do this

<Field name="email">
  {({ Label, Errors, props }) => (
    <>
      <Label />
      <input type="email"  {...props}  className="border-2 border-dashed rounded-md" />
      <Errors />
    </>
  )}
</Field>

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.