ciscoheat / sveltekit-superforms

Making SvelteKit forms a pleasure to use!
https://superforms.rocks
MIT License
2.16k stars 65 forks source link

Send nested data in a plain form #186

Open douglasward opened 1 year ago

douglasward commented 1 year ago

I would like to understand why forms with nested data require JS in order to work.

I thought one of the ideas of superforms was to make use of the progressive enhancement that sveltekit provides, i.e. allowing forms to work without JS if necessary. And I know that nested data works with forms in general. So why is JS required for superforms to allow nested data?

ciscoheat commented 1 year ago

Nested data doesn't work with forms in general, since html forms only deals with string values in non-nestable input elements. So a serialization is required for nested data (with the exception of arrays of primitive values).

// You can't express this structure in a plain html form.
{
  tags: [ { id: 1, name: 'first' }, { id: 2, name: 'second' }]
}
OllieJT commented 1 year ago

...with the exception of arrays of primitive types like string[] or number[] because with those, you can reuse the name attribute.

<input type="text" name="tag" id="tag-01" />
<input type="text" name="tag" id="tag-02" />
<input type="text" name="tag" id="tag-03" />
const formData = await event.request.formData()
formData.get('tag') // gets the first tag in the dom (string)
formData.getAll('tag') // gets all tags in the dom (string[])
ciscoheat commented 1 year ago

Yes, this is automatically handled by Superforms, so you don't need to go through FornData: https://superforms.rocks/concepts/nested-data#arrays-with-primitive-values

douglasward commented 1 year ago

Thank you for your answers! Would it be possible to somehow flatten before creating the form and unflatten again after validation in the action handler? Or is this kind of pattern not supported by zod? For example if you started with your example of

{
  tags: [ { id: 1, name: 'first' }, { id: 2, name: 'second' }]
}

this be flattened to something html forms would support like:

{
  "tags.0.id": 1,
  "tags.0.name": "first",
  "tags.1.id: 2,
  "tags.1.name": "second"
}

I just don't know how one could define a zod schema for this flattened version in order to create the superform, or if it is even possible.

ciscoheat commented 1 year ago

It's not a Zod thing, it only matches keys of the object to the schema. So it has to be a different dataType, nested-html for example. And the keys should be compatible with the FormPath type, so they should be in the format tags[0].id.

With that, it's not that hard to implement, but I haven't planned any feature releases yet, so this issue can be used as an enhancement, and I would be happy to see a PR for it!

douglasward commented 1 year ago

Okay, that sounds promising, thank you. I haven't looked into the codebase yet, but if you say it shouldn't be hard to implement then maybe I will try take a look this week. If you had any pointers off the top of your head to help me get started then that would be most appreciated - otherwise I'll give a shout if anything is unclear once I've taken an initial look.

ciscoheat commented 1 year ago

Sure, it's a server-side thing, so in superValidate.ts there is a formDataToValidation function that is the best entry point for this.

I'm not sure though how to detect that it's this nested-html datatype that's been posted, especially since it's plain html (assume no JS), so the only way to communicate things to the server is to add another field or as a query parameter.

denlukia commented 1 year ago

+1 for this issue. Nested fields is a great feature but it needing JS kinda defeats SvelteKit's "Let's make Apps that work without JS, let's use forms instead of fetch because they work without JS". So right now I'm doing it via functions nestify and flatify that convert to and from this dotted notation (although maybe better and more complex notations exist for such cases?) and I have to use them in every superForms in-out kind of "boundary"

ciscoheat commented 1 year ago

I find this "no JS"-idea to be wishful thinking in most cases. For just browsing a content site and maybe searching with a one-field form, fine, that should work. But for any kind of complexity, and especially if you're submitting nested data, a website that works without one line of JS (while still supporting the other case with a nice UX) is very ambitious and time-consuming.

ciscoheat commented 1 year ago

Btw @douglasward there is a function called splitPath that you may also find useful, when building the nested object from the posted string paths.

ciscoheat commented 1 year ago

And finally, there are some "test.ts" files in src, if you can make another one with tests for this feature, that'd be great. :)

ciscoheat commented 1 year ago

This could be useful for this issue: https://svelte.dev/repl/d8916d45012241dab5962c1323604fe9?version=4.2.0

Stadly commented 9 months ago

This is a great idea!

ciscoheat commented 9 months ago

When the source for v2 is ready for public scrutiny, it'll be much easier to add this feature.

iolyd commented 6 months ago

A challenge of working with nested data in plain forms is having a system to coerce the proper data types when parsing the formData entries. One approach is to have field-level preprocessing in your schema, like zod-form-data does it. But this means using primitive schemas that aren't your validator's natively provided ones and it also means you end up with composed schemas that are only geared towards formData, which also sometimes comes with losing the benefit and ease of use of nice utility packages like drizzle-zod since they output schemas using zod's primitives rather than zod-form-data's.

More interestingly, another approach is to give hints to the expected data type using a custom syntax in the field names. I recently came across a simple package (parse-nested-form-data) and found the idea to be quite elegant. This could probably easily be plugged into superValidate using a custom resolver wrapper.