diff --git a/docs/docs/03-syntax-and-usage/10-template-composition.md b/docs/docs/03-syntax-and-usage/10-template-composition.md index e9d0f3b23..6ce08fdd7 100644 --- a/docs/docs/03-syntax-and-usage/10-template-composition.md +++ b/docs/docs/03-syntax-and-usage/10-template-composition.md @@ -236,6 +236,43 @@ func main() { ``` +## Joining Components + +Components can be aggregated into a single Component using `templ.Join`. + +```templ +package main + +templ hello() { + hello +} + +templ world() { + world +} + +templ helloWorld() { + @templ.Join(hello(), world()) +} +``` + +```go title="main.go" +package main + +import ( + "context" + "os" +) + +func main() { + helloWorld().Render(context.Background(), os.Stdout) +} +``` + +```html title="output" +helloworld +``` + ## Sharing and re-using components Since templ components are compiled into Go functions by the `go generate` command, templ components follow the rules of Go, and are shared in exactly the same way as Go code. diff --git a/join.go b/join.go new file mode 100644 index 000000000..a8093597a --- /dev/null +++ b/join.go @@ -0,0 +1,19 @@ +package templ + +import ( + "context" + "io" +) + +// Join returns a single `templ.Component` that will render provided components in order. +// If any of the components return an error the Join component will immediately return with the error. +func Join(components ...Component) Component { + return 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 + }) +} diff --git a/join_test.go b/join_test.go new file mode 100644 index 000000000..a0572f967 --- /dev/null +++ b/join_test.go @@ -0,0 +1,81 @@ +package templ_test + +import ( + "bytes" + "context" + "errors" + "io" + "testing" + + "github.com/a-h/templ" + "github.com/google/go-cmp/cmp" +) + +func TestJoin(t *testing.T) { + compErr := errors.New("component error") + + hello := templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { + if _, err := io.WriteString(w, "Hello"); err != nil { + t.Fatalf("failed to write string: %v", err) + } + return nil + }) + world := templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { + if _, err := io.WriteString(w, "World"); err != nil { + t.Fatalf("failed to write string: %v", err) + } + return nil + }) + err := templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { + return compErr + }) + + tests := []struct { + name string + input []templ.Component + expectedOutput string + expectedErr error + }{ + { + name: "a nil slice of components produces no output", + input: nil, + expectedOutput: "", + }, + { + name: "an empty list of components produces no output", + input: []templ.Component{}, + expectedOutput: "", + }, + { + name: "components are rendered in order", + input: []templ.Component{hello, world}, + expectedOutput: "HelloWorld", + }, + { + name: "components are rendered in order, and errors returned", + input: []templ.Component{hello, err}, + expectedOutput: "Hello", + expectedErr: compErr, + }, + { + name: "no further components are rendered after an error", + input: []templ.Component{err, hello}, + expectedOutput: "", + expectedErr: compErr, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := templ.Join(tt.input...) + b := new(bytes.Buffer) + err := got.Render(context.Background(), b) + if err != tt.expectedErr { + t.Fatalf("failed to render component: %v", err) + } + if diff := cmp.Diff(tt.expectedOutput, b.String()); diff != "" { + t.Error(diff) + } + }) + } +}