diff --git a/controller/controller.go b/controller/controller.go index 7215c77..6048375 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -36,10 +36,13 @@ func NewController(c *services.Container) Controller { // RenderPage renders a Page as an HTTP response func (c *Controller) RenderPage(ctx echo.Context, page Page) error { + var buf *bytes.Buffer + var err error + // Page name is required if page.Name == "" { ctx.Logger().Error("page render failed due to missing name") - return echo.NewHTTPError(http.StatusInternalServerError, "Internal server error") + return echo.NewHTTPError(http.StatusInternalServerError) } // Use the app name in configuration if a value was not set @@ -47,38 +50,65 @@ func (c *Controller) RenderPage(ctx echo.Context, page Page) error { page.AppName = c.Container.Config.App.Name } - // Parse and execute the templates for the Page - // As mentioned in the documentation for the Page struct, the templates used for the page will be: - // 1. The layout/base template specified in Page.Layout - // 2. The content template specified in Page.Name - // 3. All templates within the components directory - // Also included is the function map provided by the funcmap package - buf, err := c.Container.TemplateRenderer.ParseAndExecute( - "controller", - page.Name, - page.Layout, - []string{ - fmt.Sprintf("layouts/%s", page.Layout), - fmt.Sprintf("pages/%s", page.Name), - }, - []string{"components"}, - page, - ) + // Check if this is an HTMX request + if page.HTMX.Request.Enabled && !page.HTMX.Request.Boosted { + // Disable caching + page.Cache.Enabled = false + + // Parse and execute + buf, err = c.Container.TemplateRenderer.ParseAndExecute( + "page:htmx", + page.Name, + "htmx", + []string{ + "htmx", + fmt.Sprintf("pages/%s", page.Name), + }, + []string{"components"}, + page, + ) + } else { + // Parse and execute the templates for the Page + // As mentioned in the documentation for the Page struct, the templates used for the page will be: + // 1. The layout/base template specified in Page.Layout + // 2. The content template specified in Page.Name + // 3. All templates within the components directory + // Also included is the function map provided by the funcmap package + buf, err = c.Container.TemplateRenderer.ParseAndExecute( + "page", + page.Name, + page.Layout, + []string{ + fmt.Sprintf("layouts/%s", page.Layout), + fmt.Sprintf("pages/%s", page.Name), + }, + []string{"components"}, + page, + ) + } if err != nil { ctx.Logger().Errorf("failed to parse and execute templates: %v", err) - return echo.NewHTTPError(http.StatusInternalServerError, "Internal server error") + return echo.NewHTTPError(http.StatusInternalServerError) } - // Cache this page, if caching was enabled - c.cachePage(ctx, page, buf) + // Set the status code + ctx.Response().Status = page.StatusCode // Set any headers for k, v := range page.Headers { ctx.Response().Header().Set(k, v) } - return ctx.HTMLBlob(page.StatusCode, buf.Bytes()) + // Apply the HTMX response, if one + if page.HTMX.Response != nil { + page.HTMX.Response.Apply(ctx) + } + + // Cache this page, if caching was enabled + c.cachePage(ctx, page, buf) + + return ctx.HTMLBlob(ctx.Response().Status, buf.Bytes()) } // cachePage caches the HTML for a given Page if the Page has caching enabled @@ -92,6 +122,12 @@ func (c *Controller) cachePage(ctx echo.Context, page Page, html *bytes.Buffer) page.Cache.Expiration = c.Container.Config.Cache.Expiration.Page } + // Extract the headers + headers := make(map[string]string) + for k, v := range ctx.Response().Header() { + headers[k] = v[0] + } + // The request URL is used as the cache key so the middleware can serve the // cached page on matching requests key := ctx.Request().URL.String() @@ -102,8 +138,8 @@ func (c *Controller) cachePage(ctx echo.Context, page Page, html *bytes.Buffer) cp := middleware.CachedPage{ URL: key, HTML: html.Bytes(), - Headers: page.Headers, - StatusCode: page.StatusCode, + Headers: headers, + StatusCode: ctx.Response().Status, } err := marshaler.New(c.Container.Cache).Set(ctx.Request().Context(), key, cp, opts) diff --git a/controller/form.go b/controller/form.go index 014ba1b..e236001 100644 --- a/controller/form.go +++ b/controller/form.go @@ -67,6 +67,10 @@ func (f FormSubmission) GetFieldStatusClass(fieldName string) string { return "" } +func (f FormSubmission) IsDone() bool { + return f.IsSubmitted && !f.HasErrors() +} + func (f *FormSubmission) setErrorMessages(form interface{}, err error) { // Only this is supported right now ves, ok := err.(validator.ValidationErrors) diff --git a/controller/page.go b/controller/page.go index 3e0783b..2877b4c 100644 --- a/controller/page.go +++ b/controller/page.go @@ -6,6 +6,7 @@ import ( "time" "goweb/context" + "goweb/htmx" "goweb/msg" echomw "github.com/labstack/echo/v4/middleware" @@ -94,6 +95,11 @@ type Page struct { // This will only be populated if the request ID middleware is in effect for the given request. RequestID string + HTMX struct { + Request htmx.Request + Response *htmx.Response + } + // Cache stores values for caching the response of this page Cache struct { // Enabled dictates if the response of this page should be cached. @@ -133,6 +139,8 @@ func NewPage(ctx echo.Context) Page { p.IsAuth = true } + p.HTMX.Request = htmx.GetRequest(ctx) + return p } diff --git a/routes/contact.go b/routes/contact.go index fcaf5ed..8817db7 100644 --- a/routes/contact.go +++ b/routes/contact.go @@ -1,11 +1,8 @@ package routes import ( - "net/http" - "goweb/context" "goweb/controller" - "goweb/msg" "github.com/labstack/echo/v4" ) @@ -43,6 +40,8 @@ func (c *Contact) Post(ctx echo.Context) error { // return c.Get(ctx) //} + // TODO: Error handling w/ HTMX support + // Parse the form values var form ContactForm if err := ctx.Bind(&form); err != nil { @@ -55,17 +54,11 @@ func (c *Contact) Post(ctx echo.Context) error { ctx.Set(context.FormKey, form) - if form.Submission.HasErrors() { - return c.Get(ctx) + if !form.Submission.HasErrors() { + if err := c.Container.Mail.Send(ctx, form.Email, "Hello!"); err != nil { + ctx.Logger().Error(err) + } } - htmx := controller.GetHTMXRequest(ctx) - - if htmx.Enabled { - return ctx.String(http.StatusOK, "HELLO!") - } else { - msg.Success(ctx, "Thank you for contacting us!") - msg.Info(ctx, "We will respond to you shortly.") - return c.Redirect(ctx, "home") - } + return c.Get(ctx) } diff --git a/services/mail.go b/services/mail.go index d498bb2..368044d 100644 --- a/services/mail.go +++ b/services/mail.go @@ -31,7 +31,7 @@ func NewMailClient(cfg *config.Config, templates *TemplateRenderer) (*MailClient // Send sends an email to a given email address with a given body func (c *MailClient) Send(ctx echo.Context, to, body string) error { if c.skipSend() { - ctx.Logger().Debugf("skipping email sent to: %s") + ctx.Logger().Debugf("skipping email sent to: %s", to) } // TODO: Finish based on your mail sender of choice @@ -43,7 +43,7 @@ func (c *MailClient) Send(ctx echo.Context, to, body string) error { // The funcmap will be automatically added to the template and the data will be passed in. func (c *MailClient) SendTemplate(ctx echo.Context, to, template string, data interface{}) error { if c.skipSend() { - ctx.Logger().Debugf("skipping template email sent to: %s") + ctx.Logger().Debugf("skipping template email sent to: %s", to) } // Parse and execute template diff --git a/templates/htmx.gohtml b/templates/htmx.gohtml new file mode 100644 index 0000000..9846ca5 --- /dev/null +++ b/templates/htmx.gohtml @@ -0,0 +1 @@ +{{template "content" .}} \ No newline at end of file diff --git a/templates/pages/contact.gohtml b/templates/pages/contact.gohtml index 1514808..8e80036 100644 --- a/templates/pages/contact.gohtml +++ b/templates/pages/contact.gohtml @@ -1,27 +1,51 @@ {{define "content"}} -