Skip to content

feat(react-form): Add withFormLens #1469

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 31 commits into
base: main
Choose a base branch
from

Conversation

LeCarbonator
Copy link
Contributor

@LeCarbonator LeCarbonator commented Apr 30, 2025

This is not confirmed to be added. This is an opportunity to get feedback on this suggestion and/or implementation.

Todos

- [ ] Is this concept okay to begin with? It's more related to discussion than an actual Todo.

  • Implementation
  • Unit tests
  • Documentation

This PR implements a variation of withForm that can be used to create form groups. This form group allows extending defaultValues and has no expectations of form level validators.

This distinguishes it both from withForm as well as instantiations of forms.

Here's an extract of the documentation:

Reusing groups of fields in multiple forms

Sometimes, a pair of fields are so closely related that it makes sense to group and reuse them — like the password example listed in the linked fields guide. Instead of repeating this logic across multiple forms, you can utilize the withFormLens higher-order component.

Unlike withForm, validators cannot be specified and could be any value.
Ensure that your fields can accept unknown error types.

Rewriting the passwords example using withFormLens would look like this:

const { useAppForm, withForm, withFormLens } = createFormHook({
  fieldComponents: {
    TextField,
    ErrorInfo,
  },
  formComponents: {
    SubscribeButton,
  },
  fieldContext,
  formContext,
})

type PasswordFields = {
  password: string
  confirm_password: string
}

// These values are only used for type-checking, and are not used at runtime
// This allows you to `...formOpts` from `formOptions` without needing to redeclare the options
const defaultValues: PasswordFields = {
  password: '',
  confirm_password: '',
}

const PasswordFields = withFormLens({
  defaultValues,
  // You may also restrict the lens to only use forms that implement this submit meta.
  // If none is provided, any form with the right defaultValues may use it.
  // onSubmitMeta: { action: '' }

  // Optional, but adds props to the `render` function in addition to `form`
  props: {
    // These default values are also for type-checking and are not used at runtime
    title: 'Password',
  },
  // Internally, you will have access to a `lens` instead of a `form`
  render: function Render({ lens, title }) {
    // access reactive values using the lense's store
    const password = useStore(lens.store, (state) => state.values.password)
    const isSubmitting = useStore(lens.store, (state) => state.isSubmitting)

    return (
      <div>
        <h2>{title}</h2>
        {/* Lenses also have access to Field, Subscribe, Field, AppField and AppForm */}
        <lens.AppField name="password">
          {(field) => <field.TextField label="Password" />}
        </lens.AppField>
        <lens.AppField
          name="confirm_password"
          validators={{
            onChangeListenTo: ['password'],
            onChange: ({ value, fieldApi }) => {
              // The form could be any values, so it is typed as 'unknown'
              const values: unknown = fieldApi.form.state.values
              // use the lens methods instead
              if (value !== lens.getFieldValue('password')) {
                return 'Passwords do not match'
              }
              return undefined
            },
          }}
        >
          {(field) => (
            <div>
              <field.TextField label="Confirm Password" />
              <field.ErrorInfo />
            </div>
          )}
        </lens.AppField>
      </div>
    )
  },
})

We can now use these grouped fields in any form that implements the default values:

// You are allowed to extend the lens fields as long as the
// existing properties remain unchanged
type Account = PasswordFields & {
  provider: string
  username: string
}

// You may nest the lens fields wherever you want
type FormValues = {
  name: string
  age: number
  account_data: PasswordFields
  linked_accounts: Account[]
}

const defaultValues: FormValues = {
  name: '',
  age: 0,
  account_data: {
    password: '',
    confirm_password: '',
  },
  linked_accounts: [
    {
      provider: 'TanStack',
      username: '',
      password: '',
      confirm_password: '',
    },
  ],
}

function App() {
  const form = useAppForm({
    defaultValues,
    // If the lens didn't specify an `onSubmitMeta` property,
    // the form may implement any meta it wants.
    // Otherwise, the meta must be defined and match.
    onSubmitMeta: { action: '' },
  })

  return (
    <form.AppForm>
      <PasswordFields
        form={form}
        // You must specify where the fields can be found
        name="account_data"
        title="Passwords"
      />
      <form.Field name="linked_accounts" mode="array">
        {(field) =>
          field.state.value.map((account, i) => (
            <PasswordFields
              key={account.provider}
              form={form}
              // The fields may be in nested fields
              name={`linked_accounts[${i}]`}
              title={account.provider}
            />
          ))
        }
      </form.Field>
    </form.AppForm>
  )
}

I have not contacted maintainers about this implementation. This may go against desired features or philosophies of the form, and I would like to hear input on that. You are free to commit to this branch.

Copy link

nx-cloud bot commented Apr 30, 2025

View your CI Pipeline Execution ↗ for commit c9ed053.

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 27s View ↗
nx run-many --target=build --exclude=examples/** ✅ Succeeded 1s View ↗

☁️ Nx Cloud last updated this comment at 2025-05-13 19:56:43 UTC

Copy link

pkg-pr-new bot commented Apr 30, 2025

More templates

@tanstack/angular-form

npm i https://pkg.pr.new/@tanstack/angular-form@1469

@tanstack/form-core

npm i https://pkg.pr.new/@tanstack/form-core@1469

@tanstack/react-form

npm i https://pkg.pr.new/@tanstack/react-form@1469

@tanstack/lit-form

npm i https://pkg.pr.new/@tanstack/lit-form@1469

@tanstack/solid-form

npm i https://pkg.pr.new/@tanstack/solid-form@1469

@tanstack/svelte-form

npm i https://pkg.pr.new/@tanstack/svelte-form@1469

@tanstack/vue-form

npm i https://pkg.pr.new/@tanstack/vue-form@1469

commit: c9ed053

@LeCarbonator
Copy link
Contributor Author

LeCarbonator commented Apr 30, 2025

Issues that could be addressed:

  • If the instantiated form has onSubmitMeta, it's no longer compatible with the form group. Perhaps if onSubmitMeta is unset and/or not called inside form Group, it should allow it outside.

This has now been addressed. If onSubmitMeta is unset, any value will do. If it is set, you must match it exactly.

While the previous separate implementation was compatible with AppForm, users
wouldn't have been able to use any field/form components
in the render itself. This commit allows them to do that,
at the expense of not being compatible with useForm.
@LeCarbonator LeCarbonator changed the title Draft: Suggestion for Form Group API feat(react-form): Add Form Group API May 1, 2025
@LeCarbonator LeCarbonator marked this pull request as ready for review May 1, 2025 11:46
@LeCarbonator
Copy link
Contributor Author

The unit tests should be reusable in case this isn't the desired approach.

Copy link

codecov bot commented May 1, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 90.01%. Comparing base (f58c1e7) to head (4ee6020).
Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1469      +/-   ##
==========================================
+ Coverage   89.13%   90.01%   +0.88%     
==========================================
  Files          31       32       +1     
  Lines        1417     1542     +125     
  Branches      362      371       +9     
==========================================
+ Hits         1263     1388     +125     
  Misses        137      137              
  Partials       17       17              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@LeCarbonator
Copy link
Contributor Author

Related PR: #1334

@LeCarbonator
Copy link
Contributor Author

LeCarbonator commented May 2, 2025

Something to consider:

  • You cannot nest these form groups (Address form group vs. Address form group in an array)
  • They can be conflicting with other groups (two groups sharing the name path). This can be averted by assuming a path namespace like address.name

@LeCarbonator LeCarbonator marked this pull request as draft May 5, 2025 07:44
This type will help with a lens API for forms
so that only names leading to the subset data
are allowed.
@LeCarbonator
Copy link
Contributor Author

#1475 sounded like an interesting idea, so I'll try to tinker with that and come up with unit tests. The note at the top of the PR still applies.

@LeCarbonator
Copy link
Contributor Author

LeCarbonator commented May 10, 2025

Strange, the derived state from the form lens isn't updating ...
Looks like it's because i'm not mounting it.

Issue: React input unfocuses when changing input. Unsure why.

@LeCarbonator
Copy link
Contributor Author

LeCarbonator commented May 11, 2025

There's currently a difference between a field's name and its actual field name. Looking into it later

It's because it forwards the form's Field component. I would much rather not mess with that as the lens should not have separate API as much as possible, especially on the React side of things.

@LeCarbonator
Copy link
Contributor Author

image
reminding myself for tomorrow. Definitely not useable like this.

@LeCarbonator LeCarbonator marked this pull request as ready for review May 12, 2025 05:45
@LeCarbonator LeCarbonator changed the title feat(react-form): Add Form Group API feat(react-form): Add withFormLens May 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants