From 9f9e4eb5681d93f035cd56a9280cb4a8d0787570 Mon Sep 17 00:00:00 2001 From: Joao Goncalves Date: Sun, 9 Nov 2025 03:17:03 -0100 Subject: [PATCH] refactor: move document head includes out of via configuration into their own append funcs; update examples --- configuration.go | 11 ---- internal/examples/livereload/livereload.go | 73 +++++++++++----------- internal/examples/livereload/main.go | 8 +-- internal/examples/picocss/main.go | 7 +-- internal/examples/plugins/main.go | 10 +-- internal/examples/realtimechart/main.go | 11 ++-- via.go | 42 ++++++++----- 7 files changed, 72 insertions(+), 90 deletions(-) diff --git a/configuration.go b/configuration.go index 852faf7..24b48cd 100644 --- a/configuration.go +++ b/configuration.go @@ -1,7 +1,5 @@ package via -import "github.com/go-via/via/h" - type LogLevel int const ( @@ -22,15 +20,6 @@ type Options struct { // The title of the HTML document. DocumentTitle string - // Elements to include in the head of the base HTML document. - // Useful for including css stylesheets and JS scripts. - DocumentHeadIncludes []h.H - - // Elements to include in the bottom of the body of the base - // HTML document.Useful for including JS scripts or a footer. - DocumentBodyIncludes []h.H - // Plugins to extend the capabilities of the `Via` application. - // Check `https://github.com/go-via/plugins` for a list of available plugins. Plugins []Plugin } diff --git a/internal/examples/livereload/livereload.go b/internal/examples/livereload/livereload.go index 7d79803..885f982 100644 --- a/internal/examples/livereload/livereload.go +++ b/internal/examples/livereload/livereload.go @@ -16,42 +16,39 @@ func LiveReloadPlugin(v *via.V) { <-r.Context().Done() }) -} - -func liveReloadScript() h.H { - return h.Script(h.Raw(` -if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { - const evtSource = new EventSource('/dev/reload'); - let overlay = null; - let showTimer = null; - - evtSource.onerror = () => { - evtSource.close(); - - showTimer = setTimeout(() => { - if (!overlay) { - overlay = document.createElement('div'); - overlay.style.cssText = 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(200, 200, 200, 0.95); padding: 20px 40px; border-radius: 8px; color: #333; font-size: 24px; z-index: 999999; font-family: -apple-system, sans-serif;'; - overlay.textContent = '🔌 Reconnecting...'; - document.body.appendChild(overlay); - } - }, 1000); - - (async function poll() { - for (let i = 0; i < 100; i++) { - try { - const res = await fetch('/', { method: 'HEAD', signal: AbortSignal.timeout(1000) }); - if (res.ok) { - clearTimeout(showTimer); - if (overlay) overlay.remove(); - location.reload(); - return; - } - } catch (e) {} - await new Promise(r => setTimeout(r, 50)); - } - })(); - }; -} -`)) + v.AppendToFoot(h.Script(h.Raw(` + if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { + const evtSource = new EventSource('/dev/reload'); + let overlay = null; + let showTimer = null; + + evtSource.onerror = () => { + evtSource.close(); + + showTimer = setTimeout(() => { + if (!overlay) { + overlay = document.createElement('div'); + overlay.style.cssText = 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(200, 200, 200, 0.95); padding: 20px 40px; border-radius: 8px; color: #333; font-size: 24px; z-index: 999999; font-family: -apple-system, sans-serif;'; + overlay.textContent = '🔌 Reconnecting...'; + document.body.appendChild(overlay); + } + }, 1000); + + (async function poll() { + for (let i = 0; i < 100; i++) { + try { + const res = await fetch('/', { method: 'HEAD', signal: AbortSignal.timeout(1000) }); + if (res.ok) { + clearTimeout(showTimer); + if (overlay) overlay.remove(); + location.reload(); + return; + } + } catch (e) {} + await new Promise(r => setTimeout(r, 50)); + } + })(); + }; + } +`))) } diff --git a/internal/examples/livereload/main.go b/internal/examples/livereload/main.go index 91f5365..9624091 100644 --- a/internal/examples/livereload/main.go +++ b/internal/examples/livereload/main.go @@ -10,13 +10,9 @@ type Counter struct{ Count int } func main() { v := via.New() - LiveReloadPlugin(v) - v.Config(via.Options{ DocumentTitle: "Live Reload", - DocumentHeadIncludes: []h.H{ - liveReloadScript(), - }, + Plugins: []via.Plugin{LiveReloadPlugin}, }) v.Page("/", func(c *via.Context) { @@ -29,7 +25,7 @@ func main() { }) c.View(func() h.H { - return h.Div(h.Class("container"), + return h.Div( h.H1(h.Text("Live Reload")), h.P(h.Textf("Count: %d", data.Count)), h.P(h.Span(h.Text("Step: ")), h.Span(step.Text())), diff --git a/internal/examples/picocss/main.go b/internal/examples/picocss/main.go index 3c536a3..28341cb 100644 --- a/internal/examples/picocss/main.go +++ b/internal/examples/picocss/main.go @@ -8,12 +8,7 @@ import ( func main() { v := via.New() - v.Config(via.Options{ - DocumentTitle: "Via", - DocumentHeadIncludes: []h.H{ - h.Link(h.Rel("stylesheet"), h.Href("https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css")), - }, - }) + v.AppendToHead(h.Link(h.Rel("stylesheet"), h.Href("https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"))) v.Page("/", func(c *via.Context) { c.View(func() h.H { diff --git a/internal/examples/plugins/main.go b/internal/examples/plugins/main.go index 80d64a8..05149ec 100644 --- a/internal/examples/plugins/main.go +++ b/internal/examples/plugins/main.go @@ -8,8 +8,8 @@ import ( "github.com/go-via/via/h" ) -// In this example we create a Via application with a plugin that adds PicoCSS. The plugin -// is handed to Via in the app Configuration. +// Example of a Via application with a plugin that adds PicoCSS. The plugin +// is handed to Via in the Configuration. //go:embed pico.yellow.min.css var picoCSSFile []byte @@ -41,9 +41,5 @@ func PicoCSSPlugin(v *via.V) { w.Header().Set("Content-Type", "text/css") _, _ = w.Write(picoCSSFile) }) - v.Config(via.Options{ - DocumentHeadIncludes: []h.H{ - h.Link(h.Rel("stylesheet"), h.Href("/_plugins/picocss/assets/style.css")), - }, - }) + v.AppendToHead(h.Link(h.Rel("stylesheet"), h.Href("/_plugins/picocss/assets/style.css"))) } diff --git a/internal/examples/realtimechart/main.go b/internal/examples/realtimechart/main.go index 9c1478b..8ef31d8 100644 --- a/internal/examples/realtimechart/main.go +++ b/internal/examples/realtimechart/main.go @@ -13,13 +13,10 @@ import ( func main() { v := via.New() - v.Config(via.Options{ - DocumentTitle: "Via", - DocumentHeadIncludes: []h.H{ - h.Link(h.Rel("stylesheet"), h.Href("https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css")), - h.Script(h.Src("https://unpkg.com/echarts@6.0.0/dist/echarts.min.js")), - }, - }) + v.AppendToHead( + h.Link(h.Rel("stylesheet"), h.Href("https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css")), + h.Script(h.Src("https://unpkg.com/echarts@6.0.0/dist/echarts.min.js")), + ) v.Page("/", func(c *via.Context) { chartComp := c.Component(chartCompFn) diff --git a/via.go b/via.go index 2ea53c6..20b2f6c 100644 --- a/via.go +++ b/via.go @@ -29,7 +29,8 @@ type V struct { mux *http.ServeMux contextRegistry map[string]*Context contextRegistryMutex sync.RWMutex - baseLayout func(h.HTML5Props) h.H + documentHeadIncludes []h.H + documentFootIncludes []h.H } func (v *V) logErr(c *Context, format string, a ...any) { @@ -78,12 +79,6 @@ func (v *V) Config(cfg Options) { if cfg.DocumentTitle != "" { v.cfg.DocumentTitle = cfg.DocumentTitle } - if cfg.DocumentHeadIncludes != nil { - v.cfg.DocumentHeadIncludes = cfg.DocumentHeadIncludes - } - if cfg.DocumentBodyIncludes != nil { - v.cfg.DocumentBodyIncludes = cfg.DocumentBodyIncludes - } if cfg.Plugins != nil { for _, plugin := range cfg.Plugins { if plugin != nil { @@ -93,6 +88,27 @@ func (v *V) Config(cfg Options) { } } +// AppendToHead appends the given h.H nodes to the head of the base HTML document. +// Useful for including css stylesheets and JS scripts. +func (v *V) AppendToHead(elements ...h.H) { + for _, el := range elements { + if el != nil { + v.documentHeadIncludes = append(v.documentHeadIncludes, el) + } + } +} + +// AppendToFoot appends the given h.H nodes to the end of the base HTML document body. +// Useful for including JS scripts. +func (v *V) AppendToFoot(elements ...h.H) { + for _, el := range elements { + if el != nil { + v.documentFootIncludes = append(v.documentFootIncludes, el) + } + } + +} + // Page registers a route and its associated page handler. // The handler receives a *Context to define UI, signals, and actions. // @@ -113,13 +129,11 @@ func (v *V) Page(route string, composeContext func(c *Context)) { v.logDebug(c, "GET %s", route) composeContext(c) v.registerCtx(c.id, c) - headElements := v.cfg.DocumentHeadIncludes + headElements := v.documentHeadIncludes headElements = append(headElements, h.Meta(h.Data("signals", fmt.Sprintf("{'via-ctx':'%s'}", id)))) headElements = append(headElements, h.Meta(h.Data("init", "@get('/_sse')"))) bottomBodyElements := []h.H{c.view()} - for _, el := range v.cfg.DocumentBodyIncludes { - bottomBodyElements = append(bottomBodyElements, el) - } + bottomBodyElements = append(bottomBodyElements, v.documentFootIncludes...) view := h.HTML5(h.HTML5Props{ Title: v.cfg.DocumentTitle, Head: headElements, @@ -172,10 +186,8 @@ func New() *V { mux: mux, contextRegistry: make(map[string]*Context), cfg: Options{ - LogLvl: LogLevelDebug, - DocumentTitle: "Via Application", - DocumentHeadIncludes: make([]h.H, 0), - DocumentBodyIncludes: make([]h.H, 0), + LogLvl: LogLevelDebug, + DocumentTitle: "Via Application", }, }