diff --git a/actiontrigger.go b/actiontrigger.go index 2c9e1b2..56dfd09 100644 --- a/actiontrigger.go +++ b/actiontrigger.go @@ -12,7 +12,13 @@ type actionTrigger struct { } // OnClick returns a via.h DOM node that triggers on click. It can be added -// to other nodes in a view. +// to element nodes in a view. func (a *actionTrigger) OnClick() h.H { return h.Data("on:click", fmt.Sprintf("@get('/_action/%s')", a.id)) } + +// OnChange returns a via.h DOM node that triggers on input change. It can be added +// to element nodes in a view. +func (a *actionTrigger) OnChange() h.H { + return h.Data("on:change__debounce.200ms", fmt.Sprintf("@get('/_action/%s')", a.id)) +} diff --git a/h/attributes.go b/h/attributes.go index eed7b84..171a361 100644 --- a/h/attributes.go +++ b/h/attributes.go @@ -34,6 +34,10 @@ func Class(v string) H { return gh.Class(v) } +func Role(v string) H { + return gh.Role(v) +} + // Data attributes automatically have their name prefixed with "data-". func Data(name, v string) H { return gh.Data(name, v) diff --git a/h/h.go b/h/h.go index 4ce471b..e521b2a 100644 --- a/h/h.go +++ b/h/h.go @@ -11,6 +11,7 @@ package h import ( "io" + g "maragu.dev/gomponents" gc "maragu.dev/gomponents/components" ) @@ -50,6 +51,13 @@ func Attr(name string, value ...string) H { return g.Attr(name, value...) } +func If(condition bool, n H) H { + if condition { + return n + } + return nil +} + // HTML5Props defines properties for HTML5 pages. Title is set always set, Description // and Language elements only if the strings are non-empty. type HTML5Props struct { diff --git a/h/util.go b/h/util.go index 61b5803..8d5567a 100644 --- a/h/util.go +++ b/h/util.go @@ -7,6 +7,10 @@ import ( func retype(nodes []H) []g.Node { list := make([]g.Node, len(nodes)) for i, node := range nodes { + if node == nil { + list[i] = nil + continue + } list[i] = node.(g.Node) } return list diff --git a/internal/examples/realtimechart/main.go b/internal/examples/realtimechart/main.go index 41df6c4..f76dd9e 100644 --- a/internal/examples/realtimechart/main.go +++ b/internal/examples/realtimechart/main.go @@ -38,33 +38,47 @@ func chartCompFn(c *via.Context) { data := make([]float64, 1000) labels := make([]string, 1000) + isLive := false + refreshRate := c.Signal(1) + tkr := time.NewTicker(1000 * time.Millisecond) + + updateRefreshRate := c.Action(func() { + tkr.Reset(1000 / time.Duration(refreshRate.Int()) * time.Millisecond) + }) + + toggleIsLive := c.Action(func() { + isLive = !isLive + }) + 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) + data = append(data[1:], rand.Float64()*10) labelsTxt, _ := json.Marshal(labels) dataTxt, _ := json.Marshal(data) - c.ExecScript(fmt.Sprintf(` + if isLive { + c.ExecScript(fmt.Sprintf(` if (myChart) myChart.setOption({ xAxis: [{data: %s}], series:[{data: %s}] - }); + },{notMerge:false,lazyUpdate:true}); `, labelsTxt, dataTxt)) + } } }() c.View(func() h.H { - return h.Div(h.ID("chart"), h.Style("width:100%;height:400px;"), - h.Script(h.Raw(` + return h.Div( + 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 + animationDurationUpdate: 0, // affects updates/redraws tooltip: { trigger: 'axis', position: function (pt) { @@ -75,15 +89,6 @@ func chartCompFn(c *via.Context) { left: 'center', text: 'Large Area Chart' }, - toolbox: { - feature: { - dataZoom: { - yAxisIndex: 'none' - }, - restore: {}, - saveAsImage: {} - } - }, xAxis: { type: 'category', boundaryGap: false, @@ -101,7 +106,7 @@ func chartCompFn(c *via.Context) { }, { start: 0, - end: 100 + end: 100 } ], series: [ @@ -131,6 +136,22 @@ func chartCompFn(c *via.Context) { }; 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"), toggleIsLive.OnChange()), + ), + h.Label(h.ID("refresh-rate-input"), h.Span(h.Text("Refresh Rate (")), h.Span(refreshRate.Text()), h.Span(h.Text(" Hz)")), + h.Input(h.Type("range"), h.Attr("min", "1"), h.Attr("max", "200"), refreshRate.Bind(), updateRefreshRate.OnChange()), + ), + ), + ), + ), + ), ) }) } diff --git a/signal.go b/signal.go index 541370e..edb38c8 100644 --- a/signal.go +++ b/signal.go @@ -49,7 +49,7 @@ func (s *signal) Bind() h.H { // // Example: // -// h.Div(h.Text("x: "), mysignal.Text()) +// h.Div(mysignal.Text()) func (s *signal) Text() h.H { return h.Data("text", "$"+s.id) } diff --git a/via.go b/via.go index 7fe1b44..9e2eb63 100644 --- a/via.go +++ b/via.go @@ -106,7 +106,7 @@ func (v *via) Page(route string, composeContext func(c *Context)) { headElements := v.cfg.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{h.Div(h.ID(c.id), c.view())} + bottomBodyElements := []h.H{c.view()} for _, el := range v.cfg.DocumentBodyIncludes { bottomBodyElements = append(bottomBodyElements, el) }