feat: add real-time chart example
This commit is contained in:
30
configuration.go
Normal file
30
configuration.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package via
|
||||||
|
|
||||||
|
import "github.com/go-via/via/h"
|
||||||
|
|
||||||
|
type LogLevel int
|
||||||
|
|
||||||
|
const (
|
||||||
|
LogLevelError LogLevel = iota
|
||||||
|
LogLevelWarn
|
||||||
|
LogLevelInfo
|
||||||
|
LogLevelDebug
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config defines configuration options for the via application
|
||||||
|
type Configuration struct {
|
||||||
|
// Level of the logs to write to stdout.
|
||||||
|
// Options: Error, Warn, Info, Debug.
|
||||||
|
LogLvl LogLevel
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
68
context.go
68
context.go
@@ -46,38 +46,17 @@ func (c *Context) View(f func() h.H) {
|
|||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// counterComponent := func(c *via.Context) {
|
// counterCompFn := func(c *via.Context) {
|
||||||
// count := 0
|
// (...)
|
||||||
// step := c.Signal(1)
|
// }
|
||||||
//
|
|
||||||
// increment := c.Action(func() {
|
|
||||||
// count += step.Int()
|
|
||||||
// c.Sync()
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// c.View(func() h.H {
|
|
||||||
// return h.Div(
|
|
||||||
// h.P(h.Textf("Count: %d", count)),
|
|
||||||
// h.P(h.Span(h.Text("Step: ")), h.Span(step.Text())),
|
|
||||||
// h.Label(
|
|
||||||
// h.Text("Update Step: "),
|
|
||||||
// h.Input(h.Type("number"), step.Bind()),
|
|
||||||
// ),
|
|
||||||
// h.Button(h.Text("Increment"), increment.OnClick()),
|
|
||||||
// )
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
//
|
//
|
||||||
// v.Page("/", func(c *via.Context) {
|
// v.Page("/", func(c *via.Context) {
|
||||||
// counter1 := c.Component(counterComponent)
|
// counterComp := c.Component(counterCompFn)
|
||||||
// counter2 := c.Component(counterComponent)
|
|
||||||
//
|
//
|
||||||
// c.View(func() h.H {
|
// c.View(func() h.H {
|
||||||
// return h.Div(
|
// return h.Div(
|
||||||
// h.H1(h.Text("Counter 1")),
|
// h.H1(h.Text("Counter")),
|
||||||
// counter1(),
|
// counterComp(),
|
||||||
// h.H1(h.Text("Counter 2")),
|
|
||||||
// counter2(),
|
|
||||||
// )
|
// )
|
||||||
// })
|
// })
|
||||||
// })
|
// })
|
||||||
@@ -145,20 +124,24 @@ func (c *Context) Signals() map[string]*signal {
|
|||||||
return c.signals
|
return c.signals
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signal creates a reactive signal and initializes it with a value.
|
// Signal creates a reactive signal and initializes it with the given value.
|
||||||
// Use Bind() to link the value of input elements to the signal and Text() to
|
// Use Bind() to link the value of input elements to the signal and Text() to
|
||||||
// display the signal value and watch the UI update live as the input changes.
|
// display the signal value and watch the UI update live as the input changes.
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// h.Div(
|
// mysignal := c.Signal("world")
|
||||||
|
//
|
||||||
|
// c.View(func() h.H {
|
||||||
|
// return h.Div(
|
||||||
// h.P(h.Span(h.Text("Hello, ")), h.Span(mysignal.Text())),
|
// h.P(h.Span(h.Text("Hello, ")), h.Span(mysignal.Text())),
|
||||||
// h.Input(h.Value("World"), mysignal.Bind()),
|
// h.Input(mysignal.Bind()),
|
||||||
// )
|
// )
|
||||||
|
// })
|
||||||
//
|
//
|
||||||
// Signals are 'alive' only in the browser, but Via always injects their values into
|
// Signals are 'alive' only in the browser, but Via always injects their values into
|
||||||
// the Context before each action call.
|
// the Context before each action call.
|
||||||
// If any signal value is updated by the server the update is automatically sent to the
|
// If any signal value is updated by the server, the update is automatically sent to the
|
||||||
// browser when using Sync() or SyncSignsls().
|
// browser when using Sync() or SyncSignsls().
|
||||||
func (c *Context) Signal(v any) *signal {
|
func (c *Context) Signal(v any) *signal {
|
||||||
sigID := genRandID()
|
sigID := genRandID()
|
||||||
@@ -214,7 +197,8 @@ func (c *Context) Sync() {
|
|||||||
sse = c.sse
|
sse = c.sse
|
||||||
}
|
}
|
||||||
if sse == nil {
|
if sse == nil {
|
||||||
c.app.logErr(c, "sync view failed: inactive SSE stream")
|
c.app.logWarn(c, "view out of sync: no sse stream")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
elemsPatch := bytes.NewBuffer(make([]byte, 0))
|
elemsPatch := bytes.NewBuffer(make([]byte, 0))
|
||||||
if err := c.view().Render(elemsPatch); err != nil {
|
if err := c.view().Render(elemsPatch); err != nil {
|
||||||
@@ -261,7 +245,8 @@ func (c *Context) SyncElements(elem h.H) {
|
|||||||
sse = c.sse
|
sse = c.sse
|
||||||
}
|
}
|
||||||
if sse == nil {
|
if sse == nil {
|
||||||
c.app.logErr(c, "sync element failed: no sse connection")
|
c.app.logWarn(c, "elements out of sync: no sse stream")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if c.view == nil {
|
if c.view == nil {
|
||||||
c.app.logErr(c, "sync element failed: viewfn is nil")
|
c.app.logErr(c, "sync element failed: viewfn is nil")
|
||||||
@@ -286,7 +271,8 @@ func (c *Context) SyncSignals() {
|
|||||||
sse = c.sse
|
sse = c.sse
|
||||||
}
|
}
|
||||||
if sse == nil {
|
if sse == nil {
|
||||||
c.app.logErr(c, "sync signals failed: sse connection not found")
|
c.app.logWarn(c, "signals out of sync: no sse stream")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
updatedSigs := make(map[string]any)
|
updatedSigs := make(map[string]any)
|
||||||
for id, sig := range c.signals {
|
for id, sig := range c.signals {
|
||||||
@@ -302,6 +288,20 @@ func (c *Context) SyncSignals() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Context) ExecScript(s string) {
|
||||||
|
var sse *datastar.ServerSentEventGenerator
|
||||||
|
if c.isComponent() {
|
||||||
|
sse = c.parentPageCtx.sse
|
||||||
|
} else {
|
||||||
|
sse = c.sse
|
||||||
|
}
|
||||||
|
if sse == nil {
|
||||||
|
c.app.logWarn(c, "script out of sync: no sse stream")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = sse.ExecuteScript(s)
|
||||||
|
}
|
||||||
|
|
||||||
func newContext(id string, a *via) *Context {
|
func newContext(id string, a *via) *Context {
|
||||||
if a == nil {
|
if a == nil {
|
||||||
log.Fatalf("create context failed: app pointer is nil")
|
log.Fatalf("create context failed: app pointer is nil")
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ func Placeholder(v string) H {
|
|||||||
return gh.Placeholder(v)
|
return gh.Placeholder(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Rel(v string) H {
|
||||||
|
return gh.Rel(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Class(v string) H {
|
||||||
|
return gh.Class(v)
|
||||||
|
}
|
||||||
|
|
||||||
// Data attributes automatically have their name prefixed with "data-".
|
// Data attributes automatically have their name prefixed with "data-".
|
||||||
func Data(name, v string) H {
|
func Data(name, v string) H {
|
||||||
return gh.Data(name, v)
|
return gh.Data(name, v)
|
||||||
|
|||||||
@@ -392,6 +392,10 @@ func Var(children ...H) H {
|
|||||||
return gh.Var(retype(children)...)
|
return gh.Var(retype(children)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func StyleEl(children ...H) H {
|
||||||
|
return gh.StyleEl(retype(children)...)
|
||||||
|
}
|
||||||
|
|
||||||
func Video(children ...H) H {
|
func Video(children ...H) H {
|
||||||
return gh.Video(retype(children)...)
|
return gh.Video(retype(children)...)
|
||||||
}
|
}
|
||||||
|
|||||||
6
h/h.go
6
h/h.go
@@ -35,6 +35,12 @@ func Raw(s string) H {
|
|||||||
return g.Raw(s)
|
return g.Raw(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rawf creates a text DOM [Node] that just Renders the interpolated and
|
||||||
|
// unescaped string format.
|
||||||
|
func Rawf(format string, a ...any) H {
|
||||||
|
return g.Rawf(format, a...)
|
||||||
|
}
|
||||||
|
|
||||||
// Attr creates an attribute DOM [Node] with a name and optional value.
|
// Attr creates an attribute DOM [Node] with a name and optional value.
|
||||||
// If only a name is passed, it's a name-only (boolean) attribute (like "required").
|
// If only a name is passed, it's a name-only (boolean) attribute (like "required").
|
||||||
// If a name and value are passed, it's a name-value attribute (like `class="header"`).
|
// If a name and value are passed, it's a name-value attribute (like `class="header"`).
|
||||||
|
|||||||
35
internal/examples/picocss/main.go
Normal file
35
internal/examples/picocss/main.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-via/via"
|
||||||
|
"github.com/go-via/via/h"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
v := via.New()
|
||||||
|
|
||||||
|
v.Config(via.Configuration{
|
||||||
|
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.Page("/", func(c *via.Context) {
|
||||||
|
c.View(func() h.H {
|
||||||
|
return h.Div(
|
||||||
|
h.H1(h.Text("Hello PicoCSS!")),
|
||||||
|
h.H2(h.Text("Hello PicoCSS!")),
|
||||||
|
h.H3(h.Text("Hello PicoCSS!")),
|
||||||
|
h.H4(h.Text("Hello PicoCSS!")),
|
||||||
|
h.H5(h.Text("Hello PicoCSS!")),
|
||||||
|
h.H6(h.Text("Hello PicoCSS!")),
|
||||||
|
h.Div(h.Class("grid"),
|
||||||
|
h.Button(h.Text("Primary")),
|
||||||
|
h.Button(h.Class("secondary"), h.Text("Secondary")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
v.Start(":3000")
|
||||||
|
}
|
||||||
136
internal/examples/realtimechart/main.go
Normal file
136
internal/examples/realtimechart/main.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-via/via"
|
||||||
|
"github.com/go-via/via/h"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
v := via.New()
|
||||||
|
|
||||||
|
v.Config(via.Configuration{
|
||||||
|
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.Page("/", func(c *via.Context) {
|
||||||
|
chartComp := c.Component(chartCompFn)
|
||||||
|
|
||||||
|
c.View(func() h.H {
|
||||||
|
return h.Div(h.Class("container"),
|
||||||
|
chartComp(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
v.Start(":3000")
|
||||||
|
}
|
||||||
|
|
||||||
|
func chartCompFn(c *via.Context) {
|
||||||
|
data := make([]float64, 1000)
|
||||||
|
labels := make([]string, 1000)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
tkr := time.NewTicker(60 * time.Millisecond)
|
||||||
|
defer tkr.Stop()
|
||||||
|
for range tkr.C {
|
||||||
|
labels = append(labels[1:], time.Now().Format("15:04:05.000"))
|
||||||
|
data = append(data[1:], rand.Float64()*1000)
|
||||||
|
labelsTxt, _ := json.Marshal(labels)
|
||||||
|
dataTxt, _ := json.Marshal(data)
|
||||||
|
|
||||||
|
c.ExecScript(fmt.Sprintf(`
|
||||||
|
if (myChart)
|
||||||
|
myChart.setOption({
|
||||||
|
xAxis: [{data: %s}],
|
||||||
|
series:[{data: %s}]
|
||||||
|
});
|
||||||
|
`, labelsTxt, dataTxt))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
c.View(func() h.H {
|
||||||
|
return h.Div(h.ID("chart"), h.Style("width:100%;height:400px;"),
|
||||||
|
h.Script(h.Raw(`
|
||||||
|
const 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: 60, // affects updates/redraws
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
position: function (pt) {
|
||||||
|
return [pt[0], '10%'];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
left: 'center',
|
||||||
|
text: 'Large Area Chart'
|
||||||
|
},
|
||||||
|
toolbox: {
|
||||||
|
feature: {
|
||||||
|
dataZoom: {
|
||||||
|
yAxisIndex: 'none'
|
||||||
|
},
|
||||||
|
restore: {},
|
||||||
|
saveAsImage: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
data: []
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
boundaryGap: [0, '100%']
|
||||||
|
},
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
type: 'inside',
|
||||||
|
start: 80,
|
||||||
|
end: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
start: 0,
|
||||||
|
end: 100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'Fake Data',
|
||||||
|
type: 'line',
|
||||||
|
symbol: 'none',
|
||||||
|
sampling: 'lttb',
|
||||||
|
itemStyle: {
|
||||||
|
color: '#1488CC'
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
color: '#1488CC'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: '#2B32B2'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
data: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
option && myChart.setOption(option);
|
||||||
|
`)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
74
via.go
74
via.go
@@ -12,6 +12,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/go-via/via/h"
|
"github.com/go-via/via/h"
|
||||||
@@ -21,23 +22,10 @@ import (
|
|||||||
//go:embed datastar.js
|
//go:embed datastar.js
|
||||||
var datastarJS []byte
|
var datastarJS []byte
|
||||||
|
|
||||||
type config struct {
|
|
||||||
logLvl LogLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogLevel int
|
|
||||||
|
|
||||||
const (
|
|
||||||
LogLevelError LogLevel = iota
|
|
||||||
LogLevelWarn
|
|
||||||
LogLevelInfo
|
|
||||||
LogLevelDebug
|
|
||||||
)
|
|
||||||
|
|
||||||
// via is the root application.
|
// via is the root application.
|
||||||
// It manages page routing, user sessions, and SSE connections for live updates.
|
// It manages page routing, user sessions, and SSE connections for live updates.
|
||||||
type via struct {
|
type via struct {
|
||||||
cfg config
|
cfg Configuration
|
||||||
mux *http.ServeMux
|
mux *http.ServeMux
|
||||||
contextRegistry map[string]*Context
|
contextRegistry map[string]*Context
|
||||||
contextRegistryMutex sync.RWMutex
|
contextRegistryMutex sync.RWMutex
|
||||||
@@ -57,7 +45,7 @@ func (v *via) logWarn(c *Context, format string, a ...any) {
|
|||||||
if c != nil && c.id != "" {
|
if c != nil && c.id != "" {
|
||||||
cRef = fmt.Sprintf("via-ctx=%q ", c.id)
|
cRef = fmt.Sprintf("via-ctx=%q ", c.id)
|
||||||
}
|
}
|
||||||
if v.cfg.logLvl <= LogLevelWarn {
|
if v.cfg.LogLvl <= LogLevelWarn {
|
||||||
log.Printf("[warn] %smsg=%q", cRef, fmt.Sprintf(format, a...))
|
log.Printf("[warn] %smsg=%q", cRef, fmt.Sprintf(format, a...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,7 +55,7 @@ func (v *via) logInfo(c *Context, format string, a ...any) {
|
|||||||
if c != nil && c.id != "" {
|
if c != nil && c.id != "" {
|
||||||
cRef = fmt.Sprintf("via-ctx=%q ", c.id)
|
cRef = fmt.Sprintf("via-ctx=%q ", c.id)
|
||||||
}
|
}
|
||||||
if v.cfg.logLvl >= LogLevelInfo {
|
if v.cfg.LogLvl <= LogLevelInfo {
|
||||||
log.Printf("[info] %smsg=%q", cRef, fmt.Sprintf(format, a...))
|
log.Printf("[info] %smsg=%q", cRef, fmt.Sprintf(format, a...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,11 +65,24 @@ func (v *via) logDebug(c *Context, format string, a ...any) {
|
|||||||
if c != nil && c.id != "" {
|
if c != nil && c.id != "" {
|
||||||
cRef = fmt.Sprintf("via-ctx=%q ", c.id)
|
cRef = fmt.Sprintf("via-ctx=%q ", c.id)
|
||||||
}
|
}
|
||||||
if v.cfg.logLvl == LogLevelDebug {
|
if v.cfg.LogLvl == LogLevelDebug {
|
||||||
log.Printf("[debug] %smsg=%q", cRef, fmt.Sprintf(format, a...))
|
log.Printf("[debug] %smsg=%q", cRef, fmt.Sprintf(format, a...))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Config overrides the default configuration with the given configuration options.
|
||||||
|
func (v *via) Config(cfg Configuration) {
|
||||||
|
if cfg.LogLvl != v.cfg.LogLvl {
|
||||||
|
v.cfg.LogLvl = cfg.LogLvl
|
||||||
|
}
|
||||||
|
if cfg.DocumentHeadIncludes != nil {
|
||||||
|
v.cfg.DocumentHeadIncludes = cfg.DocumentHeadIncludes
|
||||||
|
}
|
||||||
|
if cfg.DocumentBodyIncludes != nil {
|
||||||
|
v.cfg.DocumentBodyIncludes = cfg.DocumentBodyIncludes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Page registers a route and its associated page handler.
|
// Page registers a route and its associated page handler.
|
||||||
// The handler receives a *Context to define UI, signals, and actions.
|
// The handler receives a *Context to define UI, signals, and actions.
|
||||||
//
|
//
|
||||||
@@ -94,17 +95,25 @@ func (v *via) logDebug(c *Context, format string, a ...any) {
|
|||||||
// })
|
// })
|
||||||
func (v *via) Page(route string, composeContext func(c *Context)) {
|
func (v *via) Page(route string, composeContext func(c *Context)) {
|
||||||
v.mux.HandleFunc("GET "+route, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
v.mux.HandleFunc("GET "+route, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.Contains(r.URL.Path, "favicon") {
|
||||||
|
return
|
||||||
|
}
|
||||||
id := fmt.Sprintf("%s_/%s", route, genRandID())
|
id := fmt.Sprintf("%s_/%s", route, genRandID())
|
||||||
c := newContext(id, v)
|
c := newContext(id, v)
|
||||||
v.logDebug(c, "GET %s", route)
|
v.logDebug(c, "GET %s", route)
|
||||||
composeContext(c)
|
composeContext(c)
|
||||||
v.registerCtx(c.id, c)
|
v.registerCtx(c.id, c)
|
||||||
view := v.baseLayout(h.HTML5Props{
|
headElements := v.cfg.DocumentHeadIncludes
|
||||||
Head: []h.H{
|
headElements = append(headElements, h.Meta(h.Data("signals", fmt.Sprintf("{'via-ctx':'%s'}", id))))
|
||||||
h.Meta(h.Data("signals", fmt.Sprintf("{'via-ctx':'%s'}", id))),
|
headElements = append(headElements, h.Meta(h.Data("init", "@get('/_sse')")))
|
||||||
h.Meta(h.Data("init", "@get('/_sse')")),
|
bottomBodyElements := []h.H{h.Div(h.ID(c.id), c.view())}
|
||||||
},
|
for _, el := range v.cfg.DocumentBodyIncludes {
|
||||||
Body: []h.H{h.Div(h.ID(c.id))},
|
bottomBodyElements = append(bottomBodyElements, el)
|
||||||
|
}
|
||||||
|
view := h.HTML5(h.HTML5Props{
|
||||||
|
Title: v.cfg.DocumentTitle,
|
||||||
|
Head: headElements,
|
||||||
|
Body: bottomBodyElements,
|
||||||
})
|
})
|
||||||
_ = view.Render(w)
|
_ = view.Render(w)
|
||||||
}))
|
}))
|
||||||
@@ -114,6 +123,7 @@ func (v *via) registerCtx(id string, c *Context) {
|
|||||||
v.contextRegistryMutex.Lock()
|
v.contextRegistryMutex.Lock()
|
||||||
defer v.contextRegistryMutex.Unlock()
|
defer v.contextRegistryMutex.Unlock()
|
||||||
v.contextRegistry[id] = c
|
v.contextRegistry[id] = c
|
||||||
|
v.logDebug(c, "new context added to registry")
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (a *App) unregisterCtx(id string) {
|
// func (a *App) unregisterCtx(id string) {
|
||||||
@@ -131,6 +141,12 @@ func (v *via) getCtx(id string) (*Context, error) {
|
|||||||
return nil, fmt.Errorf("ctx '%s' not found", id)
|
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 *via) HandleFunc(pattern string, f http.HandlerFunc) {
|
||||||
|
v.mux.HandleFunc(pattern, f)
|
||||||
|
}
|
||||||
|
|
||||||
// Start starts the Via HTTP server on the given address.
|
// Start starts the Via HTTP server on the given address.
|
||||||
func (v *via) Start(addr string) {
|
func (v *via) Start(addr string) {
|
||||||
v.logInfo(nil, "via started")
|
v.logInfo(nil, "via started")
|
||||||
@@ -143,12 +159,14 @@ func New() *via {
|
|||||||
app := &via{
|
app := &via{
|
||||||
mux: mux,
|
mux: mux,
|
||||||
contextRegistry: make(map[string]*Context),
|
contextRegistry: make(map[string]*Context),
|
||||||
cfg: config{logLvl: LogLevelDebug},
|
cfg: Configuration{
|
||||||
baseLayout: h.HTML5,
|
LogLvl: LogLevelDebug,
|
||||||
|
DocumentTitle: "Via Application",
|
||||||
|
DocumentHeadIncludes: make([]h.H, 0),
|
||||||
|
DocumentBodyIncludes: make([]h.H, 0),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
app.mux.HandleFunc("GET /favicon.ico", func(w http.ResponseWriter, r *http.Request) {})
|
|
||||||
|
|
||||||
app.mux.HandleFunc("GET /_datastar.js", func(w http.ResponseWriter, r *http.Request) {
|
app.mux.HandleFunc("GET /_datastar.js", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/javascript")
|
w.Header().Set("Content-Type", "application/javascript")
|
||||||
_, _ = w.Write(datastarJS)
|
_, _ = w.Write(datastarJS)
|
||||||
@@ -165,7 +183,7 @@ func New() *via {
|
|||||||
}
|
}
|
||||||
c.sse = datastar.NewSSE(w, r)
|
c.sse = datastar.NewSSE(w, r)
|
||||||
app.logDebug(c, "SSE connection established")
|
app.logDebug(c, "SSE connection established")
|
||||||
c.Sync()
|
c.SyncSignals()
|
||||||
<-c.sse.Context().Done()
|
<-c.sse.Context().Done()
|
||||||
c.sse = nil
|
c.sse = nil
|
||||||
app.logDebug(c, "SSE connection closed")
|
app.logDebug(c, "SSE connection closed")
|
||||||
|
|||||||
Reference in New Issue
Block a user