a-h / templ

A language for writing HTML user interfaces in Go.
https://templ.guide/
MIT License
7.2k stars 238 forks source link

Deeply nested structs / maps are inaccessible and unusable - Ex data.Form.FieldErrors.NewEmail undefined (type validator.FieldErrors has no field or method NewEmail) #832

Open LordPraslea opened 3 days ago

LordPraslea commented 3 days ago

I have web app which started out as html/template. Now it's hybrid html/template and templ. Often, I need to add new functionality / touch old html/templates which I then transform to Templ.

I encounterd a great deal of headaches/errors and it seems it's way more complicated to do some things than it was with html/template. I am curious to other ways of handling some issues I found.

Right now I get

I get a bunch of errors:

...
ui/components/auth/some_component_templ.go:188:85: data.Form.FieldErrors.NewEmail undefined (type validator.FieldErrors has no field or method NewEmail)
....

Even if I go 1 layer deep let's say form.FieldErrors.NewEmail I get the same type of errors...

Here's a basic code

type Validator struct {
    FieldErrors    map[string]string
    NonFieldErrors []string
}
type someEmailForm struct {
    Email               string `form:"email"`
    validator.Validator `form:"-"`
}

templ SomeComponent (data *components.TemplateData ) {
<div class="columns is-centered">
    <div class="form box column is-half-tablet my-4 p-4">
    for _, err := range data.Form.NonFieldErrors {
      <div class="error is-size-4 is-danger">{err}</div>
        }
        <form action="/user/reset-password" method="POST" novalidate class="signup">

      if data.Form.FieldErrors.Email != "" {
        <label class="error help is-danger">{ data.Form.FieldErrors.Email }</label>
      }

      // ...  other code
    </form>
  </div>
</div>
}

func (app *application) someEmailHandler(w http.ResponseWriter, r *http.Request) {
    data := app.newTemplateData(r)
    data.Form = someEmailForm{}
    app.render(w, r, http.StatusOK, auth.SomeComponent(data), data)
}

app.newTemplateData just returns a TEmplateData struct which looks like this:

type TemplateData struct {
    Data            map[string]interface{}
    ListData        []interface{}

    Form        any // <- THIS IS IMPORTANT
    Collection  *models.Collection
    Collections []*models.Collection
    Products    []*models.Product
    AccessLinks []*models.AccessLink
    Title       string
    User        *models.User
    Lesson      *models.Lesson
    Lessons     []*models.Lesson
  // .. many other structs
}

Now, I want to keep things simple and not have to refactor my whole application this is why I kept the hybrid approach But with each new feature, it seems the development time is increased by 300% when using Templ.

Is there a way to allow nested data structs and/or map[string]string types?

Thanks

a-h commented 3 days ago

I was unable to reproduce a failure to parse deeply nested expressions used within for statement.

Can you make a complete, minimal reproduction of the issue as a tiny project and upload it to Github please?

LordPraslea commented 18 hours ago

Hi

I'll do my best, but this means taking apart the whole complex project and putting it back together which might take a lot of time, the problem is already explained and detailed above.

I'll get back

joerdav commented 18 hours ago

There are a couple things that don't seem right with this code. In terms of Go typing not templ.

  1. Form is any type, yet you access it as if it is a concrete type data.Form.(someEmailForm).FieldErrors.Email. I would expect you would need to cast the Form field: data.Form.FieldErrors.Email
  2. You seem to be accessing Email on FieldErrors, but FieldErrors is a map, not a struct, so I would expect it to be indexed like data.Form.(someEmailForm).FieldErrors["Email"].

I hope this helps.

LordPraslea commented 18 hours ago

There are a couple things that don't seem right with this code. In terms of Go typing not templ.

1. `Form` is `any` type, yet you access it as if it is a concrete type `data.Form.(someEmailForm).FieldErrors.Email`. I would expect you would need to cast the `Form` field: `data.Form.FieldErrors.Email`

2. You seem to be accessing `Email` on `FieldErrors`, but `FieldErrors` is a map, not a struct, so I would expect it to be indexed like `data.Form.(someEmailForm).FieldErrors["Email"]`.

I hope this helps.

Hi @joerdav , over the weekend I figured out that I needed to cast the data and indeed to access it as Map, not struct. However, I hoped top keep some of the consistency between html/template and templ could have allowed most of the code to stay the same without it becoming overly complex, long, and ugly.

Why? I have over 100 places where I'd need to update each separate form and it would mean a complete refactoring/reimplementation. Sometimes, even the validations would need become extremely long because of that

I guess if I don't find an alternative i'll have to do.

On the other hand, would it be possible to define a local variable in a templ to help with the boilerplate? I've tried something but can't seem to get to it

form := data.Form.(UserResetPasswordForm)

instead of having to do

  <label class="label">Email:</label>
      if data.Form.(UserResetPasswordForm).FieldErrors["Email"] != "" {
        <label class="error help is-danger">{ data.Form.(UserResetPasswordForm).FieldErrors.["Email"] }</label>
      }
... 10 times for each field

Thanks for the input:)

LordPraslea commented 18 hours ago

Ok so I experimented with https://templ.guide/syntax-and-usage/raw-go And added the environment variable and it works. Hope the API won't change to soon.