diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index f5c0e4a..fb54383 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -26,3 +26,8 @@ jobs: - name: Test run: go test -v ./... + + - name: Lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest diff --git a/response.go b/response.go index 28de987..bbddaaa 100644 --- a/response.go +++ b/response.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "html/template" "net/http" ) @@ -76,6 +77,61 @@ func (r Response) Write(w http.ResponseWriter) error { return nil } +// RenderHTML renders an HTML document fragment along with the defined HTMX headers. +func (r Response) RenderHTML(w http.ResponseWriter, html template.HTML) (int, error) { + err := r.Write(w) + if err != nil { + return 0, err + } + + return w.Write([]byte(html)) +} + +// RenderTempl renders a Templ component along with the defined HTMX headers. +func (r Response) RenderTempl(ctx context.Context, w http.ResponseWriter, c templComponent) error { + err := r.Write(w) + if err != nil { + return err + } + + err = c.Render(ctx, w) + if err != nil { + return err + } + + return nil +} + +// MustWrite applies the defined HTMX headers to a given response writer, otherwise it panics. +// +// Under the hood this uses [Response.Write]. +func (r Response) MustWrite(w http.ResponseWriter) { + err := r.Write(w) + if err != nil { + panic(err) + } +} + +// MustRenderHTML renders an HTML document fragment along with the defined HTMX headers, otherwise it panics. +// +// Under the hood this uses [Response.RenderHTML]. +func (r Response) MustRenderHTML(w http.ResponseWriter, html template.HTML) { + _, err := r.RenderHTML(w, html) + if err != nil { + panic(err) + } +} + +// MustRenderTempl renders a Templ component along with the defined HTMX headers, otherwise it panics. +// +// Under the hood this uses [Response.RenderTempl]. +func (r Response) MustRenderTempl(ctx context.Context, w http.ResponseWriter, c templComponent) { + err := r.RenderTempl(ctx, w, c) + if err != nil { + panic(err) + } +} + // Headers returns a copied map of the headers. Any modifications to the // returned headers will not affect the headers in this struct. func (r Response) Headers() (map[string]string, error) { @@ -111,18 +167,3 @@ func (r Response) Headers() (map[string]string, error) { return m, nil } - -// RenderTempl renders a Templ component along with the defined HTMX headers. -func (r Response) RenderTempl(ctx context.Context, w http.ResponseWriter, c templComponent) error { - err := r.Write(w) - if err != nil { - return err - } - - err = c.Render(ctx, w) - if err != nil { - return err - } - - return nil -} diff --git a/response_test.go b/response_test.go new file mode 100644 index 0000000..ff75929 --- /dev/null +++ b/response_test.go @@ -0,0 +1,102 @@ +package htmx + +import ( + "html/template" + "net/http" + "testing" +) + +func TestWrite(t *testing.T) { + w := newMockResponseWriter() + + err := NewResponse(). + StatusCode(StatusStopPolling). + Location("/profiles"). + Redirect("/pull"). + PushURL("/push"). + Refresh(true). + ReplaceURL("/water"). + Retarget("#world"). + Reselect("#hello"). + AddTrigger(Trigger("myEvent")). + Reswap(SwapInnerHTML.ShowOn("#swappy", Top)). + Write(w) + if err != nil { + t.Errorf("an error occurred writing a response: %v", err) + } + + if w.statusCode != StatusStopPolling { + t.Errorf("wrong error code. want=%v, got=%v", StatusStopPolling, w.statusCode) + } + + expectedHeaders := map[string]string{ + HeaderTrigger: "myEvent", + HeaderLocation: "/profiles", + HeaderRedirect: "/pull", + HeaderPushURL: "/push", + HeaderRefresh: "true", + HeaderReplaceUrl: "/water", + HeaderRetarget: "#world", + HeaderReselect: "#hello", + HeaderReswap: "innerHTML show:#swappy:top", + } + + for k, v := range expectedHeaders { + got := w.header.Get(k) + if got != v { + t.Errorf("wrong value for header %q. got=%q, want=%q", k, got, v) + } + } +} + +func TestRenderHTML(t *testing.T) { + text := `hello world!` + + w := newMockResponseWriter() + + _, err := NewResponse().Location("/conversation/message").RenderHTML(w, template.HTML(text)) + if err != nil { + t.Errorf("an error occurred writing HTML: %v", err) + } + + if got, want := w.Header().Get(HeaderLocation), "/conversation/message"; got != want { + t.Errorf("wrong value for header %q. got=%q, want=%q", HeaderLocation, got, want) + } + + if string(w.body) != text { + t.Errorf("wrong response body. got=%q, want=%q", string(w.body), text) + } +} + +func TestMustRenderHTML(t *testing.T) { + text := `hello world!` + + w := newMockResponseWriter() + + NewResponse().MustRenderHTML(w, template.HTML(text)) +} + +type mockResponseWriter struct { + body []byte + statusCode int + header http.Header +} + +func newMockResponseWriter() *mockResponseWriter { + return &mockResponseWriter{ + header: http.Header{}, + } +} + +func (mrw *mockResponseWriter) Header() http.Header { + return mrw.header +} + +func (mrw *mockResponseWriter) Write(b []byte) (int, error) { + mrw.body = append(mrw.body, b...) + return 0, nil +} + +func (mrw *mockResponseWriter) WriteHeader(statusCode int) { + mrw.statusCode = statusCode +}