dotansimha / graphql-code-generator-community

MIT License
118 stars 155 forks source link

React Form Typescript Plugin #123

Open ericwooley opened 3 years ago

ericwooley commented 3 years ago

Forms are the majority of the front end work in a lot of applications. Graphql Schemas provide everything we need to generate the form.

Some tooling exists for doing similar things. EG: uniform.tools and a few other dead packages that attempt to do this at runtime.

IMO generating the forms would be a much better approach.

Formik is a very popular react form library with support for typescript, validation, and nested objects.

A form generation plugin is tricky because it needs to be customizable enough to suit peoples needs, with regards to styles and implementations of inputs, as well as validation, and hidden elements. The way formik is built provides an easy (ish) path to code generation, because it uses context for values, change handlers, metadata, etc...

The plugin should generate a "base" input for each built in scalar. Then Input Object types, and lists can be recursively composed from the base inputs.

Lists should be their own component as well, with an delete for each button in the list, and an add button at the end.

Circular references for Input Types can be handled by an Add <InputTypeName>.

Customization could be handled by generating a context Provider, which allows over rides for scalar types.

EG of a simple output

mutation myMutation (username: String!, $id: ID!) {
  updateMyUserName(username: $username, id: $id) {
    username
    id
  }
}
// generatedCode.tsx
import React, { createContext, useContext } from 'React'
export const StringInput = (originalProps) => {
   const inputContext = useContext(myMutationContext);
   const { label, ...props } = originalProps;
   const [field, meta] =  useField(props);
   if(inputContext.String) return <inputContext.String {...originalProps} />
   return (
     <>
       <label htmlFor={props.id || props.name}>{label}</label>
       <input className="text-input" {...field} {...props} />
       {meta.touched && meta.error ? (
         <div className="error">{meta.error}</div>
       ) : null}
     </>
   );
 };

 export const IDInput = (originalProps) => {
   const inputContext = useContext(myMutationContext);
   const { label, ...props } = originalProps;
   const [field, meta] =  useField(props);
   if(inputContext.ID) return <inputContext.ID {...originalProps} />
   return (
     <>
       <label htmlFor={props.id || props.name}>{label}</label>
       <input className="text-input" type="number" {...field} {...props} />
       {meta.touched && meta.error ? (
         <div className="error">{meta.error}</div>
       ) : null}
     </>
   );
 };

export const myMutationContext = createContext<{[key: ScalarNames]: React.Component}>({})

 export const MyMutationForm = (props: {onSubmit: () => unknown } ) =>{
    return (
      <Formik onSubmit={onSubmit}>
        <Form>
          <StringInput name="username" />
          <IntInput name="id" />
        </Form>
      </Formik>
    )
 }

example usage

import React from 'react'
import {myMutationContext , MyMutationForm } from './generated' 

export const MyComponent () {
  return (
    <myMutationContext.Provider context={{
        ID: MyCustomIDLookupComponent
      }} >
      <MyMutationForm  onSubmit={(formValues) => console.log({formValues}) } />
     </myMutationContext.Provider />
    )
}

Additionally, default types, and basic yup validations could also be generated.

ericwooley commented 3 years ago

I have a branch started https://github.com/ericwooley/graphql-code-generator/tree/react-formik

However, I am running into a strange issue when using yarn build && yarn generate:examples. If anyone knows why I'm getting this issue, that would be very helpful!

Found 1 error

  ✖ ./dev-test/test-schema/formik.tsx
    Error: Cannot use GraphQLScalarType "Int" from another module or realm.

    Ensure that there is only one instance of "graphql" in the node_modules
    directory. If different versions of "graphql" are the dependencies of other
    relied on modules, use "resolutions" to ensure only one version is installed.

    https://yarnpkg.com/en/docs/selective-version-resolutions

    Duplicate "graphql" modules cannot be used at the same time since different
    versions may have different capabilities and behavior. The data from one
    version used in the function from another could produce confusing and
    spurious results.
ericwooley commented 3 years ago

Having played around with the concept a bit more. It may be sufficient to only support primitives and provide most of the value. Since you could build your mutations from primitives pretty easily. If Object types are never supported though, it could lead to a lot of long argument lists in mutations. Lists would also probably be requested rather quickly as well.

ericwooley commented 3 years ago

I decided to remove formik, because it can be done easily enough without formik.

Additionally, I could not get past the above issue about modules or realms. I think it has something to do with esm builds :shrug:

I moved development here: https://github.com/ericwooley/graphql-code-generator-react-form

I published the examples as a website, and re-used some of your iamges etc... Hopefully thats ok. Ideally, I would love the plugin to be merged into this repo, but not being able to use the graphql package was a blocker.

mmahalwy commented 3 years ago

@ericwooley imho, what would be cool is creating a yup or zod schema from the GraphQL schema (for inputs)

ericwooley commented 3 years ago

@mmahalwy :+1: I was thinking about yup for validation. But I didn't want to impose a library on the user. I have a near working version inspired by yup, that I think would be compatible. Then there could be another plugin for generating yup validations.

The API I have nearly working is a validate function, where you return an object. The object has keys that match your variable names. EG

mutation myMutation ($text1: String! $text2: String) {
# ...
}
function validateMyMutation (values: {text1: string, text2?: string}): {text1: string, text2?: string} {

  // IIRC there is a way to get errors out of yup in an object format.
  return {
     text1: values.text1? '' : 'Error, text1 is required',
     // text 2 is not required, so don't validate. 
  }
}

The validation is WIP right now. Hopefully, I'll make more progress this weekend. Enough progress to share and get feedback on.

mmahalwy commented 3 years ago

@ericwooley i created this. Still very raw: https://github.com/mmahalwy/codegen-graphql-zod

ericwooley commented 3 years ago

Nice, I'll take I'll take a look at that.

brabeji commented 3 years ago

Graphql Schemas provide everything we need to generate the form.

I have to disagree here. There is a great deal of validation information not described by GraphQL schema. At some point, you will need to extend the validation schema by adding constraints on minimum length, string URL format, default values (transformers), etc.

It might be possible using zod’s .extend but I think this can get pretty unreadable. Moreover, there can be cases where the form doesn’t match the mutation input completely. I tend to create form schemas per form (not per mutation) and let typescript check that both are compatible.

That being said, surely there are use-cases where the 1:1 mutation form schema is appropriate.

ericwooley commented 3 years ago

@brabeji You are right about validation. Part of the API is to allow validation on a per input basis. Which is what I'm currently working on.

You can also customize input components by scalar, per form, or globally through context.

ericwooley commented 3 years ago

For the more verbose update: https://twitter.com/ericwooley/status/1399191213501153289

as far as the validation API, here is what validation will look like for now: https://github.com/ericwooley/graphql-code-generator-react-form/blob/main/examples/components/addUser.tsx#L14 @mmahalwy @brabeji LMK what you think.

And here is a customized input that shows the errors: https://github.com/ericwooley/graphql-code-generator-react-form/blob/main/examples/components/addUser.tsx#L43

You can see it in action on the first form here: https://graphql-code-generator-react-form.thewooleyway.com/

It's not complete yet, so validation does not yet block form submission, and there is no validation setup on the complex forms yet, but it should work, according to typescript.

ericwooley commented 3 years ago

Validation took FOR EV ER. Lots of edge cases and small things to deal with. But I think it's basically done, there might be some small edge cases that I missed, but the api feels good, and handles all the cases I can think of.

Cases:

  1. The list can be invalid despite all elements being valid.
  2. Custom scalars can be validated.
  3. Lists and Custom scalars can be recursively validated.
  4. individual inputs can be valid or invalid (obviously)
  5. the entire form can be invalid despite all inputs being valid.

If you can think of any other cases, please LMK.

Next steps

  1. I didn't think about enums initially, so I am going to make those into a drop down, and that needs to support multi select or select.
  2. Split the plugin into two parts for the near file mode A. Reusable components/types/functions B. Form components for individual forms.