LittleSound/slimeform

Feature request for a `createForm` function or another way to wrap `useForm`

innocenzi opened this issue · 9 comments

Hey, I'm building a library where I have a custom form helper that looks exactly like SlimeForm. I think SlimeForm is doing a very good job, and I would love to depend on it for my own form helper.

The issue is that I need to override the submit function with custom logic (this is for a routing library). I wanted to provide my own useForm that would defer the logic to SlimeForm, but I can't get the typings because they're not exported.

I was initially going to PR to export them, but I thought it would be nice if SlimeForm provided some API to generate a pre-configured useForm hook.

I'd happily PR that but I wanted to see if that would be accepted. I'm not sure about the API, but I'm thinking something like that:

// Somewhere in my own library
import { router } from './router'
import type { VisitOptions } from './types'
import { createForm } from 'slimeform'

export const useForm = createForm({
  submit: ({ form }) => async (overrides?: VisitOptions) => {
    // Here I implement my custom submit method, and this method is returned
    return await router.visit({
      method: overrides.method ?? 'POST',
      data: form,
      ...overrides
    })
  },
})
// Somewhere in userland, using my library
import { useForm } from 'my-library'

const { form, submit } = useForm({
  form: () => ({
    email: '',
    password: '',
  })
})

// These parameters are from `VisitOptions`
submit({ url: '/register', method: 'POST' })

Basically, every property from the object given to createForm would be a callback that return a function, and that function would be provided to the user. The callback would have form and status given to it.

It seems a bit complicated, but it's also flexible. If you have another idea on how I could achieve my goal, I'm open to it too!

First of all, thanks for your suggestion.

  1. You said SlimeForm doesn't export some types, if you need it, you can specify which types need to be exported, I'd love to see a PR to complete type export.

  2. If the function of createForm is just to call back the submit function, you can do it by wrapping useForm in a secondary way.
    So I hope createForm is not just for calling back a function, if you have a more complete and useful design/concept for createForm, please share it with me.

  3. I'm already planning to improve the existing onSubmit function, in the future you should be able to implement it like this.

const { submit, submitting, /* ... */ } = onSubmit(MySubmit({ url: '/register', method: 'POST' }))

submitting.value // true / false

submit() // callback

Hey, thanks for your answer!

You said SlimeForm doesn't export some types, if you need it, you can specify which types need to be exported, I'd love to see a PR to complete type export.

I was going to do just that, and I will do that if we can't find a good API for my purpose.

So I hope createForm is not just for calling back a function, if you have a more complete and useful design/concept for createForm, please share it with me.

Essentially, my idea was that createForm accepted an object, and each of the properties of that object would be returned by the resulting useForm. In the example I gave, I only created a submit function, but anyone with other needs could create something else. For instance, say I want a transform function that would mutate the form before submitting, I could do that:

export const useForm = createForm({
  transform: () => () => {/* implementation */},
  submit: () => () => {/* implementation */},
})

I do agree that this could be a bit complex for no reason though. Personally I would only like to be able to override that submit method on my side, but I thought of that API to be flexible.

I'm already planning to improve the existing onSubmit function

If that allows me, in a wrapper function, to override the submit behavior, that'd be all I need!

If you export some desired type, such as UseFormParam, you can append the functionality you need like this:

import { useForm } from 'slimeform'
import { router } from './router'
import type { VisitOptions } from './types'
import type { UseFormParam } from 'slimeform'

export function myUseForm<FormT extends {}>(param: UseFormParam<FormT>): UseFormReturn<FormT> {
  const { form, ...leftovers } = useForm(param)

  return {
    form,
    ...leftovers,

    // Custom submit function
    submit: async (overrides?: VisitOptions) => {
      // Here I implement my custom submit method, and this method is returned
      return await router.visit({
        method: overrides.method ?? 'POST',
        data: form,
        ...overrides,
      })
    },
  }
}

It's just as simple and flexible

I created an issue about enhancing onSubmit

Issue: #40

Released v0.6.0, it has a great submitter function and exports all types, hope you will like it.

Perfect timing, I'll try that asap, thanks a lot!

Hey, so I'm trying to make this submitter work. It's almost there, but a few things:

  1. The submitter function expects a function that performs the submitting, but I need to allow parameters on my exposed submit function. For this reason I had to create a submit function myself, that wraps a call to submitter and then calls the submitter's submit function. Was this intended?
	async function submit(overrides?: SubmitFunctionParameters) {
		const { submit } = submitter(async({ form, clearErrors, status, reset }) => {
			return await router.visit({
				...overrides,
				// ...
			})
		}, {
			enableVerify: false,
		})

		return await submit()
	}
  1. For some reason, when enableVerify is true, I can only submit once, the subsequent submits won't be triggered. I don't need that feature, but I wanted to tell you because that seems like a bug.

  2. form, dirtyFields and status are not reactive it seems - it's only updated upon submission. I'm investigating that to see if it's a bug.

Here is my complete wrapper in case you spot any obvious mistake in it that I missed: https://gist.github.com/innocenzi/f5d1308e3fd14e6cc5e66beb295d7dce


By the way, thanks for the amazing work! ❤️

About these questions:

Question 1

The submitter function was not designed to be used in this way, perhaps you can read the documentation to understand its intent.

import { mySubmitForm } from './myFetch.ts'
const { _, submitter } = useForm(/* ... */)
// Wrap the generic submission code and use it later
const { submit, submitting } = submitter(mySubmitForm({ url: '/register', method: 'POST' }))

In general, the submit function is bound to the form's @submit.prevent, so it doesn't really need to be called with parameters.
But you can provide various arguments when you create it as in the code above.

Question 2 and 3

I tried reading the code you provided, but I can't see what is causing these errors, I need a runnable minimal reproduction and an issue.
Note: They are currently working fine in 🚀 slimeform-playground.

I'll investigate and report back, thanks!