From 0cdf77f1e4b9280cbe84177ed0e52665d06307ee Mon Sep 17 00:00:00 2001 From: Manuel Odendahl Date: Wed, 26 Feb 2025 08:47:10 -0500 Subject: [PATCH] :ambulance: Reloading page templates works, overall --- cmd/ui-server/server.go | 57 +++- cmd/ui-server/templates.templ | 56 ++-- cmd/ui-server/templates_templ.go | 551 ++++++++++++++++--------------- examples/pages/cow/cow-quiz.yaml | 6 +- pkg/events/types.go | 7 +- pkg/events/watermill.go | 9 + pkg/server/sse/handler.go | 17 +- ttmp/2025-02-22/changelog.md | 12 +- 8 files changed, 401 insertions(+), 314 deletions(-) diff --git a/cmd/ui-server/server.go b/cmd/ui-server/server.go index 0393c3e..c6b613c 100644 --- a/cmd/ui-server/server.go +++ b/cmd/ui-server/server.go @@ -224,7 +224,7 @@ func (s *Server) loadPage(path string) error { s.mu.Unlock() // Publish page reload event - event := events.NewPageReloadEvent(relPath) + event := events.NewPageReloadEvent(relPath, def) if err := s.events.Publish(relPath, event); err != nil { log.Error().Err(err).Str("path", relPath).Msg("Failed to publish page reload event") } @@ -262,12 +262,6 @@ func (s *Server) handleFileRemove(path string) error { delete(s.routes, urlPath) s.mu.Unlock() - // Publish page reload event - event := events.NewPageReloadEvent(relPath) - if err := s.events.Publish(relPath, event); err != nil { - log.Error().Err(err).Str("path", relPath).Msg("Failed to publish page reload event") - } - log.Info().Str("path", relPath).Str("url", urlPath).Msg("Removed page") return nil } @@ -377,23 +371,52 @@ func (s *Server) registerComponentRenderers() { }) // Register page-template renderer - s.sseHandler.RegisterRenderer("page-template", func(pageID string, _ interface{}) (string, error) { - log.Debug().Str("pageID", pageID).Msg("Rendering full page template") + s.sseHandler.RegisterRenderer("page-content-template", func(pageID string, data interface{}) (string, error) { + log.Debug().Str("pageID", pageID).Msg("Rendering page content template") // Get the page definition - s.mu.RLock() - def, ok := s.pages[pageID] - s.mu.RUnlock() + var def UIDefinition + + // First check if we have data from the event + if data != nil { + // Try to use the data from the event + if eventData, ok := data.(map[string]interface{}); ok { + if pageData, ok := eventData["data"]; ok { + // Try to convert the data to UIDefinition + if pageDefMap, ok := pageData.(map[string]interface{}); ok { + if components, ok := pageDefMap["components"].([]interface{}); ok { + // Convert components to the expected format + compList := make([]map[string]interface{}, 0, len(components)) + for _, comp := range components { + if compMap, ok := comp.(map[string]interface{}); ok { + compList = append(compList, compMap) + } + } + def = UIDefinition{Components: compList} + } + } + } + } + } - if !ok { - return "", fmt.Errorf("page not found: %s", pageID) + // If we couldn't get the definition from the event data, use the stored one + if len(def.Components) == 0 { + s.mu.RLock() + storedDef, ok := s.pages[pageID] + s.mu.RUnlock() + + if !ok { + return "", fmt.Errorf("page not found: %s", pageID) + } + + def = storedDef } - // Render the page template + // Render just the page content template, not the full page with base var buf bytes.Buffer - err := pageTemplate(pageID, def).Render(context.Background(), &buf) + err := pageContentTemplate(pageID, def).Render(context.Background(), &buf) if err != nil { - return "", fmt.Errorf("failed to render page template: %w", err) + return "", fmt.Errorf("failed to render page content template: %w", err) } return buf.String(), nil diff --git a/cmd/ui-server/templates.templ b/cmd/ui-server/templates.templ index 06a1fe3..6b91b82 100644 --- a/cmd/ui-server/templates.templ +++ b/cmd/ui-server/templates.templ @@ -179,38 +179,42 @@ templ indexTemplate(pages map[string]UIDefinition) { templ pageTemplate(name string, def UIDefinition) { @base("UI Server - " + name) { -
-
-
-
-
Rendered UI
-
-
- for _, component := range def.Components { - for typ, props := range component { - @renderComponent(typ, props.(map[string]interface{})) - } + @pageContentTemplate(name, def) + } +} + +templ pageContentTemplate(name string, def UIDefinition) { +
+
+
+
+
Rendered UI
+
+
+ for _, component := range def.Components { + for typ, props := range component { + @renderComponent(typ, props.(map[string]interface{})) } -
+ }
-
-
-
-
YAML Source
-
-
-
{ yamlString(def) }
-
+
+
+
+
+
YAML Source
+
+
+
{ yamlString(def) }
- } +
} templ renderComponent(typ string, props map[string]interface{}) { diff --git a/cmd/ui-server/templates_templ.go b/cmd/ui-server/templates_templ.go index 44091cb..eea9c7c 100644 --- a/cmd/ui-server/templates_templ.go +++ b/cmd/ui-server/templates_templ.go @@ -35,7 +35,7 @@ func base(title string) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -48,7 +48,7 @@ func base(title string) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -56,7 +56,7 @@ func base(title string) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -97,12 +97,12 @@ func indexTemplate(pages map[string]UIDefinition) templ.Component { }() } ctx = templ.InitializeContext(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Available Pages

") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 4) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for name := range pages { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 6) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -124,12 +124,12 @@ func indexTemplate(pages map[string]UIDefinition) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 7) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 8) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -176,45 +176,7 @@ func pageTemplate(name string, def UIDefinition) templ.Component { }() } ctx = templ.InitializeContext(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Rendered UI
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, component := range def.Components { - for typ, props := range component { - templ_7745c5c3_Err = renderComponent(typ, props.(map[string]interface{})).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
YAML Source
")
-			if templ_7745c5c3_Err != nil {
-				return templ_7745c5c3_Err
-			}
-			var templ_7745c5c3_Var10 string
-			templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(yamlString(def))
-			if templ_7745c5c3_Err != nil {
-				return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 208, Col: 56}
-			}
-			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
-			if templ_7745c5c3_Err != nil {
-				return templ_7745c5c3_Err
-			}
-			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + templ_7745c5c3_Err = pageContentTemplate(name, def).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -228,6 +190,73 @@ func pageTemplate(name string, def UIDefinition) templ.Component { }) } +func pageContentTemplate(name string, def UIDefinition) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var9 := templ.GetChildren(ctx) + if templ_7745c5c3_Var9 == nil { + templ_7745c5c3_Var9 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 9) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs("/sse?page=" + name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 190, Col: 35} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 10) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, component := range def.Components { + for typ, props := range component { + templ_7745c5c3_Err = renderComponent(typ, props.(map[string]interface{})).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 11) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(yamlString(def)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 213, Col: 55} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 12) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + func renderComponent(typ string, props map[string]interface{}) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context @@ -244,529 +273,529 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var11 := templ.GetChildren(ctx) - if templ_7745c5c3_Var11 == nil { - templ_7745c5c3_Var11 = templ.NopComponent + templ_7745c5c3_Var12 := templ.GetChildren(ctx) + if templ_7745c5c3_Var12 == nil { + templ_7745c5c3_Var12 = templ.NopComponent } ctx = templ.ClearChildren(ctx) switch typ { case "button": if id, ok := props["id"].(string); ok { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 14) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var13 = []any{ + var templ_7745c5c3_Var14 = []any{ "btn", templ.KV("btn-primary", props["type"] == "primary"), templ.KV("btn-secondary", props["type"] == "secondary"), templ.KV("btn-danger", props["type"] == "danger"), templ.KV("btn-success", props["type"] == "success"), } - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var13...) + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 21) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } case "title": if id, ok := props["id"].(string); ok { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 24) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if content, ok := props["content"].(string); ok { - var templ_7745c5c3_Var20 string - templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(content) + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(content) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 248, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 252, Col: 16} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 25) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } case "text": if id, ok := props["id"].(string); ok { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 28) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if content, ok := props["content"].(string); ok { - var templ_7745c5c3_Var23 string - templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(content) + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(content) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 260, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 264, Col: 16} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 29) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } case "input": if id, ok := props["id"].(string); ok { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 40) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } case "textarea": if id, ok := props["id"].(string); ok { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 51) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } case "checkbox": if id, ok := props["id"].(string); ok { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 60) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if label, ok := props["label"].(string); ok { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 63) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 64) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -774,61 +803,61 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { case "list": if typ, ok := props["type"].(string); ok { if id, ok := props["id"].(string); ok { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 66) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if title, ok := props["title"].(string); ok { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 67) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var42 string - templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(title) + var templ_7745c5c3_Var43 string + templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 338, Col: 31} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 342, Col: 31} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 68) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if typ == "ul" { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 69) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if items, ok := props["items"].([]interface{}); ok { for _, item := range items { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
  • ") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 70) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } switch i := item.(type) { case string: - var templ_7745c5c3_Var43 string - templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(i) + var templ_7745c5c3_Var44 string + templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(i) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 347, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 351, Col: 16} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -840,35 +869,35 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { } } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
  • ") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 71) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 72) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else if typ == "ol" { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
    ") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 73) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if items, ok := props["items"].([]interface{}); ok { for _, item := range items { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
  1. ") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 74) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } switch i := item.(type) { case string: - var templ_7745c5c3_Var44 string - templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(i) + var templ_7745c5c3_Var45 string + templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(i) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 364, Col: 16} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `cmd/ui-server/templates.templ`, Line: 368, Col: 16} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -880,18 +909,18 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { } } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
  2. ") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 75) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 76) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 77) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -899,46 +928,46 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { } case "form": if id, ok := props["id"].(string); ok { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 81) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -954,7 +983,7 @@ func renderComponent(typ string, props map[string]interface{}) templ.Component { } } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 82) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/examples/pages/cow/cow-quiz.yaml b/examples/pages/cow/cow-quiz.yaml index 77cabe5..adde5f0 100644 --- a/examples/pages/cow/cow-quiz.yaml +++ b/examples/pages/cow/cow-quiz.yaml @@ -1,7 +1,7 @@ components: - title: id: quiz-title - content: "🎯 Test Your Cow Knowledge!" + content: "🎯 Test Your Cow Knowledge! yo foo bla" - text: id: quiz-intro @@ -65,3 +65,7 @@ components: id: reset-quiz text: "Start Over" type: secondary + + + + diff --git a/pkg/events/types.go b/pkg/events/types.go index 773d2df..bc1ae37 100644 --- a/pkg/events/types.go +++ b/pkg/events/types.go @@ -24,10 +24,11 @@ type EventManager interface { } // NewPageReloadEvent creates a new event for reloading a page -func NewPageReloadEvent(pageID string) UIEvent { +func NewPageReloadEvent(pageID string, pageDef interface{}) UIEvent { return UIEvent{ - Type: "page-reload", - PageID: pageID, + Type: "page-reload", + PageID: pageID, + Component: map[string]interface{}{"data": pageDef}, } } diff --git a/pkg/events/watermill.go b/pkg/events/watermill.go index e04f554..16543d9 100644 --- a/pkg/events/watermill.go +++ b/pkg/events/watermill.go @@ -36,6 +36,9 @@ func NewWatermillEventManager(logger *zerolog.Logger) (*WatermillEventManager, e // Subscribe implements EventManager.Subscribe func (m *WatermillEventManager) Subscribe(ctx context.Context, pageID string) (<-chan UIEvent, error) { topic := fmt.Sprintf("ui-updates.%s", pageID) + m.logger.Debug(). + Str("topic", topic). + Msg("Subscribing to topic") messages, err := m.subscriber.Subscribe(ctx, topic) if err != nil { return nil, fmt.Errorf("failed to subscribe to topic %s: %w", topic, err) @@ -57,9 +60,15 @@ func (m *WatermillEventManager) Subscribe(ctx context.Context, pageID string) (< case events <- event: msg.Ack() case <-ctx.Done(): + m.logger.Debug(). + Str("pageID", pageID). + Msg("Context done, closing") return ctx.Err() } } + m.logger.Debug(). + Str("pageID", pageID). + Msg("Messages channel closed") return nil }) diff --git a/pkg/server/sse/handler.go b/pkg/server/sse/handler.go index d9ea197..5c8a9e5 100644 --- a/pkg/server/sse/handler.go +++ b/pkg/server/sse/handler.go @@ -162,8 +162,14 @@ func (h *SSEHandler) handleConnection(ctx context.Context, conn *connection) { return } case <-ctx.Done(): + h.logger.Debug(). + Str("pageID", conn.pageID). + Msg("Context done, closing connection") return case <-conn.done: + h.logger.Debug(). + Str("pageID", conn.pageID). + Msg("Connection done, closing") return } } @@ -253,20 +259,21 @@ func (h *SSEHandler) handlePageReload(conn *connection, event events.UIEvent) er // Then, check if we have a page renderer to render the full page h.mu.RLock() - pageRenderer, ok := h.renderers["page-template"] + pageRenderer, ok := h.renderers["page-content-template"] h.mu.RUnlock() if !ok { h.logger.Debug(). - Str("eventType", "page-template"). + Str("eventType", "page-content-template"). Msg("No page renderer registered, skipping full page update") return nil } - // Render the full page template - html, err := pageRenderer(conn.pageID, nil) + // Render the page content template with the page ID + // Pass the event data which might contain updated page definition + html, err := pageRenderer(conn.pageID, event.Component) if err != nil { - return fmt.Errorf("failed to render page template: %w", err) + return fmt.Errorf("failed to render page content template: %w", err) } // Send the rendered page as a component-update event diff --git a/ttmp/2025-02-22/changelog.md b/ttmp/2025-02-22/changelog.md index 30d1624..ef56019 100644 --- a/ttmp/2025-02-22/changelog.md +++ b/ttmp/2025-02-22/changelog.md @@ -7,4 +7,14 @@ Updated the SSE implementation to properly support the htmx SSE extension API. T - Added support for different event types (component-update, page-reload, yaml-update) - Added ping events to keep connections alive - Registered component renderers in the server -- Enhanced page-reload events to also send a full page update for seamless refreshes \ No newline at end of file +- Enhanced page-reload events to also send a full page update for seamless refreshes +- Optimized page reload by extracting page content into a separate template to avoid re-rendering the base template + +# Enhanced Page Reload Events with Page Definitions + +Improved the page reload event system to include the full page definition in the event payload. + +- Modified `NewPageReloadEvent` to accept and include the page definition +- Updated server code to pass the current page definition when publishing reload events +- This ensures that the SSE handler always has access to the most up-to-date page data +- Improves reliability of page content updates during reload events \ No newline at end of file