sveltekit-superforms icon indicating copy to clipboard operation
sveltekit-superforms copied to clipboard

Send nested data in a plain form

Open douglasward opened this issue 1 year ago • 15 comments

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?

douglasward avatar Jun 12 '23 14:06 douglasward

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' }]
}

ciscoheat avatar Jun 12 '23 14:06 ciscoheat

...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[])

OllieJT avatar Jun 15 '23 23:06 OllieJT

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

ciscoheat avatar Jun 16 '23 05:06 ciscoheat

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.

douglasward avatar Jun 17 '23 19:06 douglasward

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!

ciscoheat avatar Jun 17 '23 22:06 ciscoheat

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.

douglasward avatar Jun 18 '23 18:06 douglasward

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.

ciscoheat avatar Jun 20 '23 10:06 ciscoheat

+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"

denlukia avatar Jun 21 '23 06:06 denlukia

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 avatar Jun 21 '23 07:06 ciscoheat

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 avatar Jun 21 '23 10:06 ciscoheat

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 avatar Jun 21 '23 10:06 ciscoheat

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

ciscoheat avatar Sep 12 '23 17:09 ciscoheat

This is a great idea!

Stadly avatar Dec 20 '23 12:12 Stadly

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

ciscoheat avatar Dec 20 '23 12:12 ciscoheat

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.

emmbm avatar Mar 16 '24 15:03 emmbm