modular-forms icon indicating copy to clipboard operation
modular-forms copied to clipboard

qwik, routeloader vs signal store as loader for useForm

Open genox opened this issue 2 years ago • 8 comments

On https://modularforms.dev/qwik/guides/create-your-form loader is suggested to be routeLoader, but technically any store (using signals) should work, correct?

Great library btw. I first started doing things manually but soon realised it is better in the long run to use this approach. Thank you.

genox avatar Jun 24 '23 15:06 genox

Hey, thanks a lot! Yes that is correct. You can also just pass { value: { /* initial values here */ } } to loader. I am aware that this is currently not optimal for static values and I am in the process of improving this. What do you think about the following solution?

const [loginForm, { Form, Field }] = useForm<LoginForm>({
  loader: staticValues({ email: '', password: '' }),
});

With the name, I am not sure yet. Instead of staticValues it could also be staticLoader or useStaticLoader.

Alternatively it is also possible to use useSignal. However, I find this somehow a bit confusing, since it is basically not a reactive state.

const [loginForm, { Form, Field }] = useForm<LoginForm>({
  loader: useSignal({ email: '', password: '' }),
});

fabian-hiller avatar Jun 24 '23 15:06 fabian-hiller

Hmm well in my case I was simply looking for a way to set initial values on a form. since it is a form that is not directly attached to a route but instead receives data from a parent component, I initialised a loader "store":

  const loader = useSignal<AddressFormType>({
    firstName: address?.firstName || '',
    lastName: address?.lastName || '',
    streetAddress1: address?.streetAddress1 || '',
    streetAddress2: address?.streetAddress2 || '',
    city: address?.city || '',
    postalCode: address?.postalCode || '',
    phone: address?.phone || '',
  });

and then attached this to useForm. I don't see a big issue with doing it this way. A signal is a signal, right? It seems to work, anyways. ;-)

Of course, for DX, you could provide an immediate handler and name it something smart like "initialValueLoader<AddressFormType>({...})" but it would essentially be syntactical sugar. I was just wondering wether this has some kind of side effect that I am not aware of.

edit: Ah, fired the comment too soon - so this essentially means I could save myself a signal here and just drop in the data as you described above.

Later down I track values that I use to update the form contents dynamically via context and for that I set them directly on the form using

  useTask$(async ({ track }) => {
    const firstName = track(() => storeContext.profile?.firstName);
    const lastName = track(() => storeContext.profile?.lastName);
    addressForm.internal.fields.firstName!.value = firstName;
    addressForm.internal.fields.lastName!.value = lastName;
  });

Does the fact that the object is named internal imply that it should not be called directly?

genox avatar Jun 25 '23 07:06 genox

Thank you for your further feedback. You misunderstood me, I meant the following:

const [loginForm, { Form, Field }] = useForm<LoginForm>({
  loader: { value: { email: '', password: '' } },
});

Correct, internal should not be used, as there is a risk that the state becomes inconsistent. Instead, the various methods should be used for modifications.

You are right, a separate function like staticValues would only be syntax sugar, but in the long run we should choose a uniform procedure and document it that way. Alternatively I could add initialValues next to loader, where the initial data can be passed directly.

Here another tip: To change the initial values afterwards, the reset method should be used.

fabian-hiller avatar Jun 25 '23 14:06 fabian-hiller

For simplicities sake, I would prefer to set a form value on an existing form object.

Let's say I setup a form like this:

  const [addressForm, { Form, Field }] = useForm<AddressFormType>({
    loader,
  });

To set a form field's value later it would be much more convenient to use something like

addressForm.update('fieldname', value);

and let the form handle the rest by itself. Right now it is not entirely clear to me how I should set up a form if I need to change values dynamically. Do I need to initialise using useFormStore instead of useForm? Or a separate instance? how do I get my values into useFormStore? It feels a little bit unintuitive.

genox avatar Jun 28 '23 07:06 genox

The library is built with a modular design. Thus the bundle size is small and you can decide yourself which functionality you need. You can find more information here.

The store of your form is therefore the core, because it holds all the information. To make changes to it, you use methods like setValue. You can find more about this here.

setValue(addressForm, 'fieldname', 'New value');

fabian-hiller avatar Jun 28 '23 10:06 fabian-hiller

Thank you for elaborating. I appreciate it.

genox avatar Jun 28 '23 19:06 genox

@fabian-hiller could we just make the loader optional or is a default zero value always required for it to work?

DustinJSilk avatar Nov 07 '23 11:11 DustinJSilk

Currently it is required. But you're right, there are ways around it. I'm planning a major update in January that will simplify the API and increase stability.

fabian-hiller avatar Nov 07 '23 14:11 fabian-hiller