a-h / templ

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

Proposal: Allow spreading List of `templ.Components`, similar to `{ children ... }` #788

Open antran22 opened 3 weeks ago

antran22 commented 3 weeks ago

Idea: We want to pass in a list of components as props and spread it inside a templ, similar to how we spread { children ...}

Reason: Sometimes children is not enough when we need to define multiple children rendering point, especially on complex wrapper components. At this point, we will define something similar to a render props from React.

Example:

templ Modal(actions []templ.Component) {
   <div>
       <div class="main-content">
             {...children}
       </div>
       <div class="actions">
             <!-- This is the point where we want to spread the props -->
             {...actions}
       <div>
   <div>
}

templ ActionButton1() {
    <button>Submit</button>
}

templ ActionButton2() {
    <button>Cancel</button>
}

templ Main() {
   @Modal([]templ.Component{
          ActionButton1(),
          ActionButton2(),
   }) {
        <div>Content</div>
   }
}

This currently doesn't work. Current workaround is to iterate through actions directly.

templ Modal(actions []templ.Component) {
   <div>
       <div class="main-content">
             {...children}
       </div>
       <div class="actions">
             for _, action := range actions {
                   @action
             }
       <div>
   <div>
}

This proposal will improve the succinctness of the template.

Suggested implementation:

We can define a CombinedComponents type that implement templ.Component and represent a slice of templ.Component. Rendering will proceed by iterating over each children component.

Then at those spread point we instead render a wrapped CombinedComponents.

a-h commented 3 weeks ago

This strikes me as a map function in Go, so you could implement it as a function like this:

func Chain(components ...templ.Component) templ.Component {
  return templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
    for _, c := range components {
      if err = c.Render(ctx, w); err != nil {
        return err
      }
    }
    return nil
  })
}

And use it in templ like:

templ Modal(actions []templ.Component) {
   <div>
       <div class="main-content">
             {...children}
       </div>
       <div class="actions">
             @mypackage.Chain(actions...)
       <div>
   <div>
}

As an aside, if was designing it today, I wouldn't have added { ...children } as a special case, but would have created a @templ.Children() function instead - to get the children from the context, and rendered them out.

I was initially enthusiastic about adding something new to templ's namespace, but I'm not sure it's a good thing to hide the fact it's doing a loop just to save two lines of code.

I suspect something would pop up in the Go stdlib if it was something Go-ish. What do other folks think?