diff --git a/go.mod b/go.mod index c98d73a..28f2ab6 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,6 @@ require maragu.dev/gomponents v1.2.0 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 - github.com/fsnotify/fsnotify v1.9.0 - github.com/go-via/via-plugin-picocss v0.1.0 github.com/mattn/go-sqlite3 v1.14.32 github.com/starfederation/datastar-go v1.0.3 github.com/stretchr/testify v1.10.0 @@ -21,7 +19,6 @@ require ( github.com/kr/pretty v0.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - golang.org/x/sys v0.38.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3934bd1..fb49a09 100644 --- a/go.sum +++ b/go.sum @@ -8,10 +8,6 @@ github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUS github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-via/via-plugin-picocss v0.1.0 h1:ytVtBlfYBhidos5ub4a8liYqadz1AkeHhh7e7Paz620= -github.com/go-via/via-plugin-picocss v0.1.0/go.mod h1:5LEnLE7q8YfYY7jtH/TLPvfquB7Qt9WZ7TbKrskUW+0= github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f h1:jopqB+UTSdJGEJT8tEqYyE29zN91fi2827oLET8tl7k= github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= @@ -47,8 +43,6 @@ github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/h/datastar.go b/h/datastar.go index d50de92..55db744 100644 --- a/h/datastar.go +++ b/h/datastar.go @@ -1,9 +1,13 @@ package h -import "fmt" - -type OnClickOpts string - -func OnClick(actionid string, opt ...OnClickOpts) H { - return Data("on:click", fmt.Sprintf("@get('/_action/%s')", actionid)) +func DataInit(expression string) H { + return Data("init", expression) +} + +func DataEffect(expression string) H { + return Data("effect", expression) +} + +func DataIgnoreMorph() H { + return Attr("data-ignore-morph") } diff --git a/internal/examples/chatroom/ADR.md b/internal/examples/chatroom/ADR.md index e122636..f87e9d1 100644 --- a/internal/examples/chatroom/ADR.md +++ b/internal/examples/chatroom/ADR.md @@ -1,10 +1,10 @@ -## ADR +# ADR - Support Multiple Rooms -Not single chat room toy problem. + Not single chat room toy problem. - Rooms are generic -They know nothing of their data. Just store it. Reusable for different usecases. + They know nothing of their data. Just store it. Reusable for different usecases. - Server controls push frequency -Debounce to every 400ms, if dirty. + Debounce to every 400ms, if dirty. diff --git a/internal/examples/livereload/main.go b/internal/examples/livereload/main.go index 87bbd4b..c7b5203 100644 --- a/internal/examples/livereload/main.go +++ b/internal/examples/livereload/main.go @@ -2,7 +2,7 @@ package main import ( "github.com/go-via/via" - "github.com/go-via/via-plugin-picocss/picocss" + // "github.com/go-via/via-plugin-picocss/picocss" "github.com/go-via/via/h" ) @@ -15,7 +15,9 @@ func main() { DocumentTitle: "Live Reload Demo", DevMode: true, LogLvl: via.LogLevelDebug, - Plugins: []via.Plugin{picocss.Default}, + Plugins: []via.Plugin{ + // picocss.Default + }, }) v.Page("/", func(c *via.Context) { diff --git a/internal/examples/pathparams/main.go b/internal/examples/pathparams/main.go index bbaf983..c7359dd 100644 --- a/internal/examples/pathparams/main.go +++ b/internal/examples/pathparams/main.go @@ -4,7 +4,7 @@ import ( "strconv" "github.com/go-via/via" - "github.com/go-via/via-plugin-picocss/picocss" + // "github.com/go-via/via-plugin-picocss/picocss" . "github.com/go-via/via/h" ) @@ -12,7 +12,7 @@ func main() { v := via.New() v.Config(via.Options{ - Plugins: []via.Plugin{picocss.Default}, + // Plugins: []via.Plugin{picocss.Default}, }) v.Page("/counters/{counter_id}/{start_at_step}", func(c *via.Context) { diff --git a/internal/examples/pathparams/pathparams b/internal/examples/pathparams/pathparams new file mode 100755 index 0000000..4bc69f2 Binary files /dev/null and b/internal/examples/pathparams/pathparams differ diff --git a/internal/examples/picocss/main.go b/internal/examples/picocss/main.go index 68a1bb9..f0e2ad2 100644 --- a/internal/examples/picocss/main.go +++ b/internal/examples/picocss/main.go @@ -2,7 +2,7 @@ package main import ( "github.com/go-via/via" - "github.com/go-via/via-plugin-picocss/picocss" + // "github.com/go-via/via-plugin-picocss/picocss" "github.com/go-via/via/h" ) @@ -15,9 +15,9 @@ func main() { DocumentTitle: "Via Counter", // Plugin is placed here. Use picocss.WithOptions(pococss.Options) to add the plugin // with a different color theme or to enable a classes for a wide range of colors. - Plugins: []via.Plugin{ - picocss.Default, - }, + // Plugins: []via.Plugin{ + // picocss.Default, + // }, }) v.Page("/", func(c *via.Context) { diff --git a/internal/examples/plugins/main.go b/internal/examples/plugins/main.go index 5831116..7a490af 100644 --- a/internal/examples/plugins/main.go +++ b/internal/examples/plugins/main.go @@ -37,7 +37,7 @@ func main() { } func PicoCSSPlugin(v *via.V) { - v.HandleFunc("GET /_plugins/picocss/assets/style.css", func(w http.ResponseWriter, r *http.Request) { + v.HTTPServeMux().HandleFunc("GET /_plugins/picocss/assets/style.css", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/css") _, _ = w.Write(picoCSSFile) }) diff --git a/internal/examples/realtimechart/main.go b/internal/examples/realtimechart/main.go index 7d28259..91dbb5b 100644 --- a/internal/examples/realtimechart/main.go +++ b/internal/examples/realtimechart/main.go @@ -6,7 +6,7 @@ import ( "time" "github.com/go-via/via" - "github.com/go-via/via-plugin-picocss/picocss" + // "github.com/go-via/via-plugin-picocss/picocss" "github.com/go-via/via/h" ) @@ -17,7 +17,7 @@ func main() { LogLvl: via.LogLevelDebug, DevMode: true, Plugins: []via.Plugin{ - picocss.Default, + // picocss.Default, }, }) @@ -26,8 +26,42 @@ func main() { ) v.Page("/", func(c *via.Context) { - chartComp := c.Component(chartCompFn) + isLive := true + + isLiveSig := c.Signal("on") + + refreshRate := c.Signal("24") + + computedTickDuration := func() time.Duration { + return 1000 / time.Duration(refreshRate.Int()) * time.Millisecond + } + + updateData := c.OnInterval(computedTickDuration(), func() { + ts := time.Now().UnixMilli() + val := rand.ExpFloat64() * 10 + + c.ExecScript(fmt.Sprintf(` + if (myChart) { + myChart.appendData({seriesIndex: 0, data: [[%d, %f]]}); + myChart.setOption({},{notMerge:false,lazyUpdate:true}); + }; + `, ts, val)) + }) + updateData.Start() + + updateRefreshRate := c.Action(func() { + updateData.UpdateInterval(computedTickDuration()) + }) + + toggleIsLive := c.Action(func() { + isLive = isLiveSig.Bool() + if isLive { + updateData.Start() + } else { + updateData.Stop() + } + }) c.View(func() h.H { return h.Div(h.Style("overflow-x:hidden"), h.Section(h.Class("container"), @@ -40,144 +74,100 @@ func main() { ), ), ), - chartComp(), + h.Div( + h.Div(h.ID("chart"), h.DataIgnoreMorph(), h.Style("width:100%;height:400px;"), + h.Script(h.Raw(` + var prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); + var myChart = echarts.init(document.getElementById('chart'), prefersDark.matches ? 'dark' : 'light'); + var option = { + backgroundColor: prefersDark.matches ? 'transparent' : '#ffffff', + animationDurationUpdate: 0, // affects updates/redraws + tooltip: { + trigger: 'axis', + position: function (pt) { + return [pt[0], '10%']; + }, + syncStrategy: 'closestSampledPoint', + backgroundColor: prefersDark.matches ? '#13171fc0' : '#eeeeeec0', + extraCssText: 'backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);' + }, + title: { + left: 'center', + text: '📈 Real-Time Chart Example' + }, + xAxis: { + type: 'time', + boundaryGap: false, + axisLabel: { + hideOverlap: true + } + }, + yAxis: { + type: 'value', + boundaryGap: [0, '100%'], + min: 0, + max: 100 + }, + dataZoom: [ + { + type: 'inside', + start: 1, + end: 100 + }, + { + start: 0, + end: 100 + } + ], + series: [ + { + name: 'Fake Data', + type: 'line', + symbol: 'none', + sampling: 'max', + itemStyle: { + color: '#e8ae01' + }, + lineStyle: { color: '#e8ae01'}, + areaStyle: { + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { + offset: 0, + color: '#fecc63' + }, + { + offset: 1, + color: '#c79400' + } + ]) + }, + large: true, + data: [] + } + ] + }; + option && myChart.setOption(option); + `)), + ), + h.Section( + h.Article( + h.H5(h.Text("Controls")), + h.Hr(), + h.Div(h.Class("grid"), + h.FieldSet( + h.Legend(h.Text("Live Data")), + h.Input(h.Type("checkbox"), h.Role("switch"), isLiveSig.Bind(), toggleIsLive.OnChange()), + ), + h.Label(h.Text("Refresh Rate (Hz) ― "), refreshRate.Text(), + h.Input(h.Type("range"), h.Attr("min", "1"), h.Attr("max", "200"), refreshRate.Bind(), updateRefreshRate.OnChange()), + ), + ), + ), + ), + ), ) }) }) v.Start() } - -func chartCompFn(c *via.Context) { - isLive := true - - isLiveSig := c.Signal("on") - - refreshRate := c.Signal("24") - - computedTickDuration := func() time.Duration { - return 1000 / time.Duration(refreshRate.Int()) * time.Millisecond - } - - updateData := c.OnInterval(computedTickDuration(), func() { - ts := time.Now().UnixMilli() - val := rand.ExpFloat64() * 10 - - c.ExecScript(fmt.Sprintf(` - if (myChart) { - myChart.appendData({seriesIndex: 0, data: [[%d, %f]]}); - myChart.setOption({ - },{ - notMerge:false, - lazyUpdate:true - }); - }; - `, ts, val)) - }) - updateData.Start() - - updateRefreshRate := c.Action(func() { - updateData.UpdateInterval(computedTickDuration()) - }) - - toggleIsLive := c.Action(func() { - isLive = isLiveSig.Bool() - if isLive { - updateData.Start() - } else { - updateData.Stop() - } - }) - - c.View(func() h.H { - return h.Div(h.Div(h.ID("chart"), h.Style("width:100%;height:400px;"), - h.Script(h.Raw(` - var prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); - var myChart = echarts.init(document.getElementById('chart'), prefersDark.matches ? 'dark' : 'light'); - var option = { - backgroundColor: prefersDark.matches ? 'transparent' : '#ffffff', - animationDurationUpdate: 0, // affects updates/redraws - tooltip: { - trigger: 'axis', - position: function (pt) { - return [pt[0], '10%']; - }, - syncStrategy: 'closestSampledPoint', - backgroundColor: prefersDark.matches ? '#13171fc0' : '#eeeeeec0', - extraCssText: 'backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px);' - }, - title: { - left: 'center', - text: '📈 Real-Time Chart Example' - }, - xAxis: { - type: 'time', - boundaryGap: false, - axisLabel: { - hideOverlap: true - } - }, - yAxis: { - type: 'value', - boundaryGap: [0, '100%'], - min: 0, - max: 100 - }, - dataZoom: [ - { - type: 'inside', - start: 1, - end: 100 - }, - { - start: 0, - end: 100 - } - ], - series: [ - { - name: 'Fake Data', - type: 'line', - symbol: 'none', - sampling: 'max', - itemStyle: { - color: '#e8ae01' - }, - lineStyle: { color: '#e8ae01'}, - areaStyle: { - color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ - { - offset: 0, - color: '#fecc63' - }, - { - offset: 1, - color: '#c79400' - } - ]) - }, - large: true, - data: [] - } - ] - }; - option && myChart.setOption(option); - `))), - h.Section( - h.Article( - h.H5(h.Text("Controls")), - h.Hr(), - h.Div(h.Class("grid"), - h.FieldSet( - h.Legend(h.Text("Live Data")), - h.Input(h.Type("checkbox"), h.Role("switch"), isLiveSig.Bind(), toggleIsLive.OnChange()), - ), - h.Label(h.Text("Refresh Rate (Hz) ― "), refreshRate.Text(), - h.Input(h.Type("range"), h.Attr("min", "1"), h.Attr("max", "200"), refreshRate.Bind(), updateRefreshRate.OnChange()), - ), - ), - ), - ), - ) - }) -} diff --git a/via.go b/via.go index 94fb510..727cb81 100644 --- a/via.go +++ b/via.go @@ -1,5 +1,6 @@ -// Package via provides a reactive web framework for Go. -// It lets you build live, type-safe web interfaces without JavaScript. +// Package via provides a reactive, real-time engine for creating Go web +// applications. It lets you build live, type-safe web interfaces without +// JavaScript. // // Via unifies routing, state, and UI reactivity through a simple mental model: // Go on the server — HTML in the browser — updated in real time via Datastar. @@ -231,21 +232,18 @@ func (v *V) getCtx(id string) (*Context, error) { return nil, fmt.Errorf("ctx '%s' not found", id) } -// HandleFunc registers the HTTP handler function for a given pattern. The handler function panics if -// in conflict with another registered handler with the same pattern. -func (v *V) HandleFunc(pattern string, f http.HandlerFunc) { - v.mux.HandleFunc(pattern, f) -} - // Start starts the Via HTTP server on the given address. func (v *V) Start() { v.logInfo(nil, "via started at [%s]", v.cfg.ServerAddress) log.Fatalf("[fatal] %v", http.ListenAndServe(v.cfg.ServerAddress, v.mux)) } -// Handler returns the underlying http.Handler for use with custom servers or testing. -// This enables integration with test frameworks like gost-dom/browser for SSE/Datastar testing. -func (v *V) Handler() http.Handler { +// HTTPServeMux returns the underlying HTTP request multiplexer to enable user extentions, middleware and +// plugins. It also enables integration with test frameworks like gost-dom/browser for SSE/Datastar testing. +// +// IMPORTANT. The returned *http.ServeMux can only be modified during initialization, before calling via.Start(). +// Concurrent handler registration is not safe. +func (v *V) HTTPServeMux() *http.ServeMux { return v.mux } @@ -423,14 +421,12 @@ func New() *V { if sse.Context().Err() == nil { v.logErr(c, "PatchElements failed: %v", err) } - continue } case patchTypeSignals: if err := sse.PatchSignals([]byte(patch.content)); err != nil { if sse.Context().Err() == nil { v.logErr(c, "PatchSignals failed: %v", err) } - continue } case patchTypeScript: if err := sse.ExecuteScript(patch.content, datastar.WithExecuteScriptAutoRemove(true)); err != nil {