go-app
go-app copied to clipboard
Component level action trigger global level handler
The actionDemo component is used for action handler test only.
The problem is that both comp1 and comp2 value are changed when the "Inc" button of comp1 is clicked.
package comp
import (
"fmt"
"github.com/maxence-charriere/go-app/v9/pkg/app"
)
type (
actionDemo struct {
app.Compo
}
actionCompo struct {
app.Compo
FName string
value int
}
)
func (this *actionDemo) Render() app.UI {
return app.Div().
Body(
&actionCompo{
FName: "comp1",
},
&actionCompo{
FName: "comp2",
},
)
}
func (this *actionCompo) OnMount(ctx app.Context) {
ctx.Handle("value", func(context app.Context, action app.Action) {
this.value += action.Value.(int)
})
}
func (this *actionCompo) Render() app.UI {
return app.Div().
Body(
app.Span().Body(
app.Text(this.FName),
app.Text(": "),
app.Text(fmt.Sprintf("%d", this.value)),
app.Button().
Text("Inc").
OnClick(func(ctx app.Context, e app.Event) {
ctx.NewActionWithValue("value", 1)
}),
app.Button().
Text("Des").
OnClick(func(ctx app.Context, e app.Event) {
ctx.NewActionWithValue("value", -1)
}),
))
}
Actions (and handlers) are global in go-app. In your example the action named "value" is received by both components. You need to vary the action name or use tags to determine if the component should handle the action.
The example updated using tag, only showing relevant changes:
func (this *actionCompo) OnMount(ctx app.Context) {
ctx.Handle("value", func(context app.Context, action app.Action) {
if action.Tags.Get("fname") != this.FName {
// not for me
return
}
this.value += action.Value.(int)
})
}
func (this *actionCompo) Render() app.UI {
return app.Div().
Body(
app.Span().Body(
app.Text(this.FName),
app.Text(": "),
app.Text(fmt.Sprintf("%d", this.value)),
app.Button().
Text("Inc").
OnClick(func(ctx app.Context, e app.Event) {
ctx.NewActionWithValue("value", 1,
app.T("fname", this.FName))
}),
app.Button().
Text("Des").
OnClick(func(ctx app.Context, e app.Event) {
ctx.NewActionWithValue("value", -1,
app.T("fname", this.FName))
}),
))
}
Is there a way to only let children components receive the action?
@gepengscu Not that I'm aware of.
@mlctrez Thank you for your advices! I solved my question that comunication between parent and child components using events.
// index.go
package event_demo
import "github.com/maxence-charriere/go-app/v9/pkg/app"
type (
Index struct {
app.Compo
}
)
func (this *Index) Render() app.UI {
return app.Div().
Body(
&parent{
FName: "p1",
FBody: app.FilterUIElems(
&child{FName: "c11"},
&child{FName: "c12"},
)},
&parent{
FName: "p2",
FBody: app.FilterUIElems(
&child{FName: "c21"},
&child{FName: "c22"},
),
},
&parent{
FName: "p3",
FBody: app.FilterUIElems(
&child{FName: "31"},
&child{FName: "32"},
),
},
)
}
// parent.go
package event_demo
import (
"fmt"
"github.com/maxence-charriere/go-app/v9/pkg/app"
)
type (
parent struct {
app.Compo
FBody []app.UI
FName string
}
)
func (this *parent) Render() app.UI {
return app.Div().
Body(
app.Range(this.FBody).Slice(func(i int) app.UI {
return this.FBody[i]
}),
).
On("mount", func(ctx app.Context, e app.Event) {
e.PreventDefault()
p := e.Get("detail")
app.Log(p.Get("name").String())
app.Log(p.Get("value").Float())
p.Get("fn").Invoke(this.FName)
app.Log(fmt.Sprintf("parent %s got mount event, %+v", this.FName, p))
})
}
// child.go
package event_demo
import (
"fmt"
"github.com/maxence-charriere/go-app/v9/pkg/app"
)
type (
child struct {
app.Compo
FName string
}
)
func (this *child) OnMount(ctx app.Context) {
evt := app.Window().Call("CreateEvent", "mount", map[string]any{
"name": this.FName,
"value": 3.14,
"fn": app.FuncOf(func(self app.Value, args []app.Value) any {
event := app.Event{Value: args[0]}
app.Log(this.FName, "callback is called by", event.String())
return nil
}),
})
ctx.JSSrc().Call("dispatchEvent", evt)
app.Log(fmt.Sprintf("child %s dispatchEvent", this.FName))
}
func (this *child) Render() app.UI {
return app.Div().
Body(
app.Text(this.FName),
).
On("parent", func(ctx app.Context, e app.Event) {
e.PreventDefault()
app.Log(fmt.Sprintf("%s got parent event", this.FName))
})
}
// app.js
function CreateEvent(name, value) {
return new CustomEvent(name, {bubbles: true, cancelable: true, detail: value})
}
I don't think that event is a good way. It will be better if data can be shared in context from parent to child like react Provider.
I struggle to understand what problem this code is solving. Can you give an example?
The problem is that a component like Form or checkbox.Group should have the oppertunity to pass data to its children or children'children...., without knowning who the child is and where the child locate. React solve the problem with Provider and Consumer the two components. I solve the problem with Consumer wrap the child (Input, Checkbox,Select...)and Provider wrap the parent (Form, Group,…). Consumer send a custom bubble event named "mount" and valued with key of the wrapped compo in OnMount. Provider listen on "mount" events and and dispatch action with value of parent data and key in "mount" event. Consumer filter actions with its key and get the data from parent by action. It seems too complicated. Is there any other better more methods?
Well, when I need to transfer data between components that are not directly connected, I just use global / package level variables. The framework we made has a shared state which is based on the "page" (which is more like a scene) and most of the components "inside" either are dumb or have access to that shared state.
I/We may not use go-app the same way as others, or maybe not even as intended. We do not have an "input" or "select" component that gets wrapped by another "form" or "label" component in current projects. We would have an "address-entry" component, that gets part of an "order form". Those then can use a component like a complex combo box with search and backend connection or such stuff. These get created by the parent and communicate through callbacks to update the parent's state. In the end, all the data is always in Go, we don't ever read data from the DOM outside the "internals" of a component, where it is sometimes needed. The DOM is just a "canvas" and in fact, I was already thinking about changing some stuff we render from using HTML elements to a simple canvas (we may be re-inventing flutter here lol).
There are mostly no interactions on the JavaScript level between components, either. I also kind of dislike using events because they are losing type information and convert data between Wasm and JS. Most of the time my credo is to "pretend that there is no java-script" and each external JS library used feels like a defeat to me.
One of our biggest app actually is just one component that has "scenes" and "states" that describe what "render" has to display. User input just updates the states, and the app renders the new state. This whole app will be part of another app later, that controls what the current "function" is (kind of like starting an application on your OS), but the controlling component has no clue about what this thing does. The controller may share something like a "notifications" or "status bar" or "log-panel" component. I had "events" for those before, but then I found it much more convenient just to have a global "status bar" or "logger" or "notifications" variable that gets transferred on component creation and is called using methods. Basically, I share state between components that way. Like the connection to the NATS-Server, which is just a global variable. Similar to how the "net/http" package keeps its internal connections globally but abstract this for the consumer. As there is also a guarantee that there is only one UI thread and code runs on that, you often do not even need to worry about concurrency.
I saw some people's approaches to create or abstract a JavaScript/CSS framework like Bootstrap as "go-app" but I don't see the use case for me. If I want to use a Vite/VueJS or React like framework, I would just use that and typescript. Go-App and Wasm to me is more like using wxWidgets and Python in the past.
It is indeed a better way. We did need a framework based on Go-App to solve this and other problems. It sounds like that you have not a checkbox.Group like component in the framework, and Checkbox component updates "states" in global "scenes", and register a update callback to the "states" with name as key. Is this right?
This would be probably just radio buttons for me. But to make them checkmarks, I would likely have the selected value in the component and map with all the possible keys and labels for those checkboxes. The render method would render those boxes and "select" the selected one. Every checkbox gets an event handler and if it is clicked, the component selected value gets updated and go-app update will be triggered. This will then update the DOM to match the next render call. This component would also have a "Class" field that is used as a class name for the renderer to "skin" the component if needed.
To make this clear: I would not read any data from the <input> tags ever here (or "send the form"). The selected box is the one which key is stored in the component struct as "selected string". There would also be no "group" name needed, as a group is everything in that one component. And the checkboxes would be not other components inside that component because I just would render them as <input type="checkbox">, but it could also just some div's that render something that looks like a checkbox. It does not need to be an input field at all.
The checkbox.Group here is just an example of set type components.
I think let set type component itself wrap its set values will be a better way.
In fact, UI interface has getParent() and getChildren() methods. If uppercase the getXXX functions, or implement Parent()UI and Children()[]UI , using a tree-walk in Go-App UI tree is an approach.
Another way is that creating context in Go side and passing context from parent to child in Go-App can also share data. Go side can determin whether the context should be passed from parent to child or not. In this way, parent data can be put into context object and shared with any child component.
I prefer the last one.
@oderwat Would you please give a simple example how to create the shared "states"? For example the "States".Name will show on Parent.Name, and also show on Child.Name. When Child update the Name(update "states".Name), I also want to Parent.Name also updated.
@cxjava I am not sure what you mean. I share a "state" by using a global variable (usually in a special package) or use a reference to a value or struct. If I need to get informed about a state change, I use a call back hook, the go-app state events or sometimes even a backend event bus loop. As all handler code and the render runs in the UI go routine, you generally do not need to fear concurrency problems when sharing data.
@oderwat Here is an example base on your mountpoint:
package main
import (
"log"
"net/http"
"strconv"
"github.com/maxence-charriere/go-app/v9/pkg/app"
"github.com/metatexx/go-app-pkgs/mountpoint"
)
var (
SharedName = &shared{}
// Shared = &isolate{}
)
type shared struct {
Name string
}
type tab struct {
app.Compo
Text string
Name string
}
func (c *tab) Render() app.UI {
return app.Div().Body(
app.Div().Text(c.Text),
app.Text("ChildName:"),
app.Input().Type("text").Value(c.Name).OnChange(func(ctx app.Context, e app.Event) {
SharedName.Name = ctx.JSSrc().Get("value").String()
app.Log(SharedName.Name)
}),
)
}
type isolate struct {
app.Compo
Tabs []app.UI
Active int
name string
mp *mountpoint.UI
}
var _ app.Mounter = (*isolate)(nil)
func (c *isolate) OnInit() {
SharedName = &shared{
Name: "will update from child",
}
c.name = SharedName.Name
}
func (c *isolate) OnMount(ctx app.Context) {
// c.name = SharedName.Name
c.Tabs = []app.UI{&tab{Text: "Content A", Name: c.name}, &tab{Text: "Content B", Name: c.name}, &tab{Text: "Content C", Name: c.name}}
c.mp = mountpoint.New(c.Tabs[0])
}
func (c *isolate) OnNav(ctx app.Context) {
url := ctx.Page().URL()
idx, err := strconv.Atoi(url.Fragment)
if err != nil {
idx = 0
}
c.Active = idx
c.mp.Switch(c.Tabs[c.Active])
}
func (c *isolate) Render() app.UI {
if c.Tabs == nil {
return app.Div().Text("Unmounted")
}
return app.Div().Body(
app.Div().Body(app.Text("ParentName:"), app.Text(c.name)), // app.Text(SharedName.Name) can work fine
app.A().Style("padding", "5px").Href("#0").Text("Tab 0"),
app.A().Style("padding", "5px").Href("#1").Text("Tab 1"),
app.A().Style("padding", "5px").Href("#2").Text("Tab 2"),
app.Div().Style("padding", "5px").Body(c.mp.UI()),
)
}
func main() {
app.Route("/", &isolate{})
app.RunWhenOnBrowser()
http.Handle("/", &app.Handler{
Name: "Isolate",
Description: "Isolated functionality test",
})
if err := http.ListenAndServe(":8000", nil); err != nil {
log.Fatal(err)
}
}
What I want is that: When I update the child name, I also want to parent name updated automatically.
I use the ctx.NewActionWithValue do it before.
Right now This example also works fine if bind the parent name to SharedName.Name.
But I am not sure this is the way you talk about "shared states" or your prefer. If not, can you help modify the above example base on your suggestion?
In my component framework, some components (like Form and FormItem, RadioGroup and Radio) are designed hierarchy. I use a hacked method GetParent to iterate over UI tree just in Go.
package utils
import (
"github.com/maxence-charriere/go-app/v9/pkg/app"
_ "unsafe"
)
//go:linkname GetParent github.com/maxence-charriere/go-app/v9/pkg/app.UI.getParent
func GetParent(ui app.UI) app.UI
func IterateParent(from app.UI, iterator func(app.UI) bool) {
for ui := from; ui != nil && iterator(ui); ui = GetParent(ui) {}
}
formItem demo
package form
import (
"github.com/maxence-charriere/go-app/v9/pkg/app"
)
type (
IForm interface {
app.Composer
}
formItem struct {
app.Compo
}
)
func (this *formItem) OnMount(ctx app.Context) {
IterateParent(ctx.Src(), func(ui app.UI) bool {
if f, ok := ui.(IForm); ok {
//got IForm
// do something
return false
}
return true
})
}
What puzzles me is that why make UI.getParent a private method? Does @maxence-charriere has any special purpose?