Closed alexluong closed 4 weeks ago
Let me look into this. Its tecnically not supported yet but I did think it was working. @alexluong Thanks again :P
No worries!
I took a look and noticed that for slice, when you process each item, the item passed to the struct is not a pointer but a map instead. That's what I've found but don't know how to make it work just yet.
@alexluong Actually I was wrong. This was not implemented. I have just implemented it & all tests are passing. If you need it I can merge it now.
@Oudwins that would be great, but I can also pull your code locally and run my project against that branch if you prefer more time to polish.
Do you know if the zog/zhttp
package can support it as well?
@alexluong The zhttp package is literally 10 lines of code. It will support it yes.
Here is the PR, it would probably be fine to merge. But I am thinking some refactoring is in order to better support this use case: https://github.com/Oudwins/zog/pull/10
Do note that the zhttp package only has a provider for form data & query params currently. A provider for json is actually already done. I just haven't had the time to properly test it.
This would be the code if you want to use it:
func NewJsonDataProvider(r *http.Request) (p.DataProvider, error) {
var data map[string]any
decod := json.NewDecoder(r.Body)
err := decod.Decode(&data)
if err != nil {
return nil, err
}
return z.NewMapDataProvider(data), nil
}
@Oudwins thanks, I'll give it a try and let you know how it goes.
Also I'm testing this for nested structs within slices for FormData, which seems to be a bit tricky so that's why I'm a bit concerned with the zhttp
package. Will let you know how it goes. If you have an example HTML setup or something to test that would be great too.
@alexluong, Damm. Actually, I don't think it will work. Could you give me an example of what you are trying to do?
I think it won't work because there is no nested structure in form data
Yeah for sure. So imagine you build a form that can accept an array of objects. Say you want to invite multiple users at once.
The expected payload should be
type Payload struct {
Users []struct {
Name string
Email string
}
}
How would you approach this? I think this isn't an anti-pattern or anything, is it?
To be honest, I'm more of a SPA / REST person so this is usually pretty simple. I'm trying to embrace the web standard with FormData, so trying a simple Go x HTMX project and this is what I'm struggling with atm 😅
Why are you not doing this:
type Payload struct {
UserEmails: []string
}
I am struggling to imagine what you expect the form data to look like for this:
type Payload struct {
Users []struct {
Name string
Email string
}
}
If not using a struct doesn't work. We can think about if there is a clean way of implementing this. Otherwise you can take a look at the zhttp data provider and make your own.
It shouldn't be very difficult
@Oudwins that's because this is a simplified example. In the real use case, the struct may go a bit deeper, with the possibility of having more nested struct of its own. I try not to go too deep but I've come across this pattern a few times. Just wondering how other folks solve this problem.
Maybe I'm more used to JS / Rails having custom helpers to parse it. I should be able to figure something out tho, thanks!
@alexluong I haven't ever come across something like this for form data. With json yes of course all the time. If you show me an example of what the form data looks like before it is parsed by one of the js /rails libraries. I can look into it. Otherwise you just have to make a function that will convert the form data into something like this
{
Users: []Map[string]any
}
This is one good discussion from the JS side on how to solve this problem: https://github.com/remix-run/remix/discussions/1541
For Ruby, Rack server has a built-in helper to parse nested objects like this: https://github.com/rack/rack/blob/main/lib/rack/query_parser.rb
Hope that helps. I'm trying to figure out what Go folks do for now and probably write a custom data provider here.
I think the FormData looks something like this
{
"users[][name]": ["John", "Jane"],
"users[][email]": ["john@example.com", "jane@example.com"]
}
Okay, yea @alexluong, that makes sense. Its what I was thinking. Does remix support this out of the box?
I think this might make sense for a framework but I'm hesitant to add something that forces your form data to conform to a specific naming scheme in a library. I would need to think about this. If you want you can write a custom provider and we can go from there.
edit: ignore all this below ->
i think the syntax that would make most sense is something like
<!-- User 1 -->
<input name="users[].name" />
<input name="users[].email" />
<!-- User 2 -->
<input name="users[].name" />
<input name="users[].email" />
because go already parses form data into a map[string][]string
This is assuming that form data comes with a guaranteed order, if it doesn't that syntax wouldn't work
Actually. Reading more about it, this is something that they do support under the remix-validated-form
as per the docs:
<MyInput name="firstName" label="First Name" />
<MyInput name="lastName" label="Last Name" />
<MyInput name="address.street" label="Street" />
<MyInput name="address.city" label="City" />
<MyInput name="phones[0].type" label="Phone 1 Type" />
<MyInput name="phones[0].number" label="Phone 1 Number" />
<MyInput name="phones[1].type" label="Phone 2 Type" />
<MyInput name="phones[1].number" label="Phone 2 Number" />
I would be willing to add this to the zhttp package if you want to make a PR. Ignore my previous idea though, I think it would make sense to keep the same API.
Yup, I've used remix-validated-form
in the past for this use case.
Yeah, I can look more into it. I haven't messed around with struct tag and reflection much, so probably will need your help finalizing it but happy to spend some time on this.
@alexluong, I happy to help, but you actually don't need any struct tag reflection, that is handled inside zog automatically for you.
Just make a function that builds a map[string]any and pass that into the NewMapDataProvider() function.
So:
So for example, if you get something like this
map[string][]string{
"todo[0].name": []string{"alex"}
}
You want to make a slice of any & place this map at the 0s position:
map[string]any{
"name": "alex",
}
Does that make sense? We are creating a lot of garbage. But I don't think there is another reasonable way of getting the dx you want.
I have been thinking about this. I think there will probably be a way of supporting this use case in a much simpler way without having to create the data structure I was suggesting. But I need to think about the architecture quite a bit more, it will probably take same weeks to explore the idea
That's alright, in that case I'll write a helper to parse form data into map and use NewMapDataProvider
to make it work for now.
Sure, let me show you what we will do in the future though in case it helps inform the interface. Basically, Zog is already calculating the path of the current value for returning usable errors. Which means if we can give the data provider access to the current path it can use that to resolve the query.
Here is an example of the error paths generated for the code example from before:
@Oudwins gotcha, so you'll come up with some sort of logic to deduce the path for this case and the data provider will use that logic to construct the map accordingly?
Sooo I haven't quite got this to work just yet, but I'm gonna call it for right now. I'm not 100% sure if I'll continue exploring this, given you're going to come up with an alternative approach anyway. For the time being, I'll handle this from the client side and make a JSON request to the server and use z.NewMapDataProvider()
for it.
Leaving my WIP here in case anyone want to take it and run with it. I got some support from Mr.ChatGPT but I think there may be a fundamental error in the approach so I may have to rewrite quite a bit 😅. But anyway, this is the test file, I got deep struct to work, just need to support slices via TestParseSlices
.
@alexluong Yes something like that. The issue is that currently zog builds the path based on the input data. Meaning it might actually be impossible (or require a large rewrite) to use that path to index into the form data map. I'll need to think more about this. It won't be a priority for now but I would like to support it if possible.
Thanks for sharing your WIP. Probably the most complex part of this approach is handling slices so good place to put it down hahaha.
If you are not already I would suggest you use v0.6.2 since I merged some breaking changes for the error paths related to what we spoke about. Also, if you are using templ & tailwind you might find another package I maintain useful (tailwind-merge-go)
Other than that, I'll try to ping you here once I find a solution for this use case
@alexluong Not sure if you are still playing around with this. But if you are I have found this library: https://github.com/go-playground/form
You should be able to use it to parse into a map[string]any and use that as the data. Again we are parsing twice which is not ideal but a solution for this will come eventually just not yet
Hi, it seems this use case isn't supported yet. I ran into an error when trying this out. Sharing the recreation below.
On a similar note, does the
zog/zhttp
package support nested objects & arrays as well?