diff --git a/cmd/wsh/cmd/wshcmd-html.go b/cmd/wsh/cmd/wshcmd-html.go
index e28ac73ae0..a16bc821d1 100644
--- a/cmd/wsh/cmd/wshcmd-html.go
+++ b/cmd/wsh/cmd/wshcmd-html.go
@@ -6,7 +6,6 @@ package cmd
import (
"context"
"log"
- "time"
"github.com/spf13/cobra"
"github.com/wavetermdev/waveterm/pkg/vdom"
@@ -18,7 +17,7 @@ import (
)
var htmlCmdNewBlock bool
-var GlobalVDomClient *vdomclient.Client
+var HtmlVDomClient *vdomclient.Client = vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true})
func init() {
htmlCmd.Flags().BoolVarP(&htmlCmdNewBlock, "newblock", "n", false, "create a new block")
@@ -32,172 +31,198 @@ var htmlCmd = &cobra.Command{
RunE: htmlRun,
}
-func StyleTag(ctx context.Context, props map[string]any) any {
- return vdom.Bind(`
-
- `, nil)
+// Prop Types
+type BgItemProps struct {
+ Bg string `json:"bg"`
+ Label string `json:"label"`
+ OnClick func() `json:"onClick"`
}
-type BgItemProps struct {
- Bg string
- Label string
+type BgListProps struct {
+ Items []BgItem `json:"items"`
+}
+
+type BgItem struct {
+ Bg string `json:"bg"`
+ Label string `json:"label"`
}
-func BgItemTag(ctx context.Context, props BgItemProps) any {
- clickFn := func() {
- log.Printf("bg item clicked %q\n", props.Bg)
- blockInfo, err := wshclient.BlockInfoCommand(GlobalVDomClient.RpcClient, GlobalVDomClient.RpcContext.BlockId, nil)
- if err != nil {
- log.Printf("error getting block info: %v\n", err)
- return
+// Components
+var Style = vdomclient.DefineComponent[struct{}](HtmlVDomClient, "Style",
+ func(ctx context.Context, _ struct{}) any {
+ return vdom.E("style", nil, `
+ .root {
+ padding: 10px;
+ }
+
+ .background {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ }
+
+ .background-inner {
+ max-width: 300px;
+ }
+
+ .bg-item {
+ cursor: pointer;
+ padding: 8px 12px;
+ border-radius: 4px;
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ justify-content: flex-start;
+ }
+
+ .bg-item:hover {
+ background-color: var(--button-grey-hover-bg);
+ }
+
+ .bg-preview {
+ width: 20px;
+ height: 20px;
+ margin-right: 10px;
+ border-radius: 50%;
+ border: 1px solid #777;
+ }
+
+ .bg-label {
+ display: block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ `)
+ },
+)
+
+var BgItemTag = vdomclient.DefineComponent[BgItemProps](HtmlVDomClient, "BgItem",
+ func(ctx context.Context, props BgItemProps) any {
+ return vdom.E("div",
+ vdom.P("className", "bg-item"),
+ vdom.P("onClick", props.OnClick),
+ vdom.E("div",
+ vdom.P("className", "bg-preview"),
+ vdom.PStyle("background", props.Bg),
+ ),
+ vdom.E("div",
+ vdom.P("className", "bg-label"),
+ props.Label,
+ ),
+ )
+ },
+)
+
+var BgList = vdomclient.DefineComponent[BgListProps](HtmlVDomClient, "BgList",
+ func(ctx context.Context, props BgListProps) any {
+ setBackground := func(bg string) func() {
+ return func() {
+ blockInfo, err := wshclient.BlockInfoCommand(HtmlVDomClient.RpcClient, HtmlVDomClient.RpcContext.BlockId, nil)
+ if err != nil {
+ log.Printf("error getting block info: %v\n", err)
+ return
+ }
+ err = wshclient.SetMetaCommand(HtmlVDomClient.RpcClient, wshrpc.CommandSetMetaData{
+ ORef: waveobj.ORef{OType: "tab", OID: blockInfo.TabId},
+ Meta: map[string]any{"bg": bg},
+ }, nil)
+ if err != nil {
+ log.Printf("error setting meta: %v\n", err)
+ }
+ }
}
- log.Printf("block info: tabid=%q\n", blockInfo.TabId)
- err = wshclient.SetMetaCommand(GlobalVDomClient.RpcClient, wshrpc.CommandSetMetaData{
- ORef: waveobj.ORef{OType: "tab", OID: blockInfo.TabId},
- Meta: map[string]any{"bg": props.Bg},
- }, nil)
- if err != nil {
- log.Printf("error setting meta: %v\n", err)
+
+ items := make([]*vdom.VDomElem, 0, len(props.Items))
+ for _, item := range props.Items {
+ items = append(items, BgItemTag(BgItemProps{
+ Bg: item.Bg,
+ Label: item.Label,
+ OnClick: setBackground(item.Bg),
+ }))
}
- // wshclient.SetMetaCommand(GlobalVDomClient.RpcClient)
- }
- params := map[string]any{
- "bg": props.Bg,
- "label": props.Label,
- "clickHandler": clickFn,
- }
- return vdom.Bind(`
-
`, params)
-}
-func AllBgItemsTag(ctx context.Context, props map[string]any) any {
- items := []map[string]any{
- {"bg": nil, "label": "default"},
- {"bg": "#ff0000", "label": "red"},
- {"bg": "#00ff00", "label": "green"},
- {"bg": "#0000ff", "label": "blue"},
- }
- bgElems := make([]*vdom.VDomElem, 0)
- for _, item := range items {
- elem := vdom.E("BgItemTag", item)
- bgElems = append(bgElems, elem)
- }
- return vdom.Bind(`
-
- `, map[string]any{"bgElems": bgElems})
-}
+ return vdom.E("div",
+ vdom.P("className", "background"),
+ vdom.E("div",
+ vdom.P("className", "background-inner"),
+ items,
+ ),
+ )
+ },
+)
-func MakeVDom() *vdom.VDomElem {
- vdomStr := `
-
-
-
Set Background
-
-
-
-
-
-

-
-
- `
- elem := vdom.Bind(vdomStr, nil)
- return elem
-}
+var App = vdomclient.DefineComponent[struct{}](HtmlVDomClient, "App",
+ func(ctx context.Context, _ struct{}) any {
+ inputText, setInputText := vdom.UseState(ctx, "start")
-func GlobalEventHandler(client *vdomclient.Client, event vdom.VDomEvent) {
- if event.EventType == "clickinc" {
- client.SetAtomVal("num", client.GetAtomVal("num").(int)+1)
- return
- }
-}
+ bgItems := []BgItem{
+ {Bg: "", Label: "default"},
+ {Bg: "#ff0000", Label: "red"},
+ {Bg: "#00ff00", Label: "green"},
+ {Bg: "#0000ff", Label: "blue"},
+ }
+
+ return vdom.E("div",
+ vdom.P("className", "root"),
+ Style(struct{}{}),
+ vdom.E("h1", nil, "Set Background"),
+ vdom.E("div", nil,
+ vdom.E("wave:markdown",
+ vdom.P("text", "*quick vdom application to set background colors*"),
+ ),
+ ),
+ vdom.E("div", nil,
+ BgList(BgListProps{Items: bgItems}),
+ ),
+ vdom.E("div", nil,
+ vdom.E("img",
+ vdom.P("style", "width: 100%; height: 100%; max-width: 300px; max-height: 300px; object-fit: contain;"),
+ vdom.P("src", "vdom:///test.png"),
+ ),
+ ),
+ vdom.E("div", nil,
+ vdom.E("input",
+ vdom.P("type", "text"),
+ vdom.P("value", inputText),
+ vdom.P("onChange", func(e vdom.VDomEvent) {
+ setInputText(e.TargetValue)
+ }),
+ ),
+ vdom.E("div", nil,
+ "text ", inputText,
+ ),
+ ),
+ )
+ },
+)
func htmlRun(cmd *cobra.Command, args []string) error {
WriteStderr("running wsh html %q\n", RpcContext.BlockId)
-
- client, err := vdomclient.MakeClient(&vdom.VDomBackendOpts{CloseOnCtrlC: true})
+ client := HtmlVDomClient
+ err := client.Connect()
if err != nil {
return err
}
- GlobalVDomClient = client
- client.SetGlobalEventHandler(GlobalEventHandler)
- log.Printf("created client: %v\n", client)
- client.RegisterComponent("StyleTag", StyleTag)
- client.RegisterComponent("BgItemTag", BgItemTag)
- client.RegisterComponent("AllBgItemsTag", AllBgItemsTag)
+
+ // Set up the root component
+ client.SetRootElem(App(struct{}{}))
+
+ // Set up file handler
client.RegisterFileHandler("/test.png", "~/Downloads/IMG_1939.png")
- client.SetRootElem(MakeVDom())
+
+ // Create the VDOM context
err = client.CreateVDomContext(&vdom.VDomTarget{NewBlock: htmlCmdNewBlock})
if err != nil {
return err
}
- log.Printf("created context\n")
+
+ // Handle shutdown
go func() {
<-client.DoneCh
wshutil.DoShutdown("vdom closed by FE", 0, true)
}()
- log.Printf("created vdom context\n")
- go func() {
- time.Sleep(5 * time.Second)
- log.Printf("updating text\n")
- client.SetAtomVal("text", "updated text")
- err := client.SendAsyncInitiation()
- if err != nil {
- log.Printf("error sending async initiation: %v\n", err)
- }
- }()
+
<-client.DoneCh
return nil
}
diff --git a/frontend/app/view/vdom/vdom-model.tsx b/frontend/app/view/vdom/vdom-model.tsx
index 44ab2e0f66..f7bdf144f8 100644
--- a/frontend/app/view/vdom/vdom-model.tsx
+++ b/frontend/app/view/vdom/vdom-model.tsx
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { BlockNodeModel } from "@/app/block/blocktypes";
-import { getBlockMetaKeyAtom, globalStore, WOS } from "@/app/store/global";
+import { getBlockMetaKeyAtom, globalStore, PLATFORM, WOS } from "@/app/store/global";
import { makeORef } from "@/app/store/wos";
import { waveEventSubscribe } from "@/app/store/wps";
import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
@@ -43,24 +43,45 @@ function makeVDomIdMap(vdom: VDomElem, idMap: Map) {
}
}
-function convertEvent(e: React.SyntheticEvent, fromProp: string): any {
- if (e == null) {
- return null;
- }
- if (fromProp == "onClick") {
- return { type: "click" };
- }
- if (fromProp == "onKeyDown") {
- const waveKeyEvent = adaptFromReactOrNativeKeyEvent(e as React.KeyboardEvent);
- return waveKeyEvent;
+function annotateEvent(event: VDomEvent, propName: string, reactEvent: React.SyntheticEvent) {
+ if (reactEvent == null) {
+ return;
}
- if (fromProp == "onFocus") {
- return { type: "focus" };
+ if (propName == "onChange") {
+ const changeEvent = reactEvent as React.ChangeEvent;
+ event.targetvalue = changeEvent.target?.value;
+ event.targetchecked = changeEvent.target?.checked;
+ }
+ if (propName == "onClick" || propName == "onMouseDown") {
+ const mouseEvent = reactEvent as React.MouseEvent;
+ event.mousedata = {
+ button: mouseEvent.button,
+ buttons: mouseEvent.buttons,
+ alt: mouseEvent.altKey,
+ control: mouseEvent.ctrlKey,
+ shift: mouseEvent.shiftKey,
+ meta: mouseEvent.metaKey,
+ clientx: mouseEvent.clientX,
+ clienty: mouseEvent.clientY,
+ pagex: mouseEvent.pageX,
+ pagey: mouseEvent.pageY,
+ screenx: mouseEvent.screenX,
+ screeny: mouseEvent.screenY,
+ movementx: mouseEvent.movementX,
+ movementy: mouseEvent.movementY,
+ };
+ if (PLATFORM == "darwin") {
+ event.mousedata.cmd = event.mousedata.meta;
+ event.mousedata.option = event.mousedata.alt;
+ } else {
+ event.mousedata.cmd = event.mousedata.alt;
+ event.mousedata.option = event.mousedata.meta;
+ }
}
- if (fromProp == "onBlur") {
- return { type: "blur" };
+ if (propName == "onKeyDown") {
+ const waveKeyEvent = adaptFromReactOrNativeKeyEvent(reactEvent as React.KeyboardEvent);
+ event.keydata = waveKeyEvent;
}
- return { type: "unknown" };
}
class VDomWshClient extends WshClient {
@@ -200,7 +221,7 @@ export class VDomModel {
this.batchedEvents.push({
waveid: null,
eventtype: "onKeyDown",
- eventdata: e,
+ keydata: e,
});
this.queueUpdate();
return true;
@@ -540,26 +561,22 @@ export class VDomModel {
}
}
}
+ if (update.haswork) {
+ this.queueUpdate(true);
+ }
}
- callVDomFunc(fnDecl: VDomFunc, e: any, compId: string, propName: string) {
- const eventData = convertEvent(e, propName);
+ callVDomFunc(fnDecl: VDomFunc, e: React.SyntheticEvent, compId: string, propName: string) {
+ const vdomEvent: VDomEvent = {
+ waveid: compId,
+ eventtype: propName,
+ };
if (fnDecl.globalevent) {
- const waveEvent: VDomEvent = {
- waveid: null,
- eventtype: fnDecl.globalevent,
- eventdata: eventData,
- };
- this.batchedEvents.push(waveEvent);
- } else {
- const vdomEvent: VDomEvent = {
- waveid: compId,
- eventtype: propName,
- eventdata: eventData,
- };
- this.batchedEvents.push(vdomEvent);
+ vdomEvent.globaleventtype = fnDecl.globalevent;
}
- this.queueUpdate();
+ annotateEvent(vdomEvent, propName, e);
+ this.batchedEvents.push(vdomEvent);
+ this.queueUpdate(true);
}
createFeUpdate(): VDomFrontendUpdate {
diff --git a/frontend/app/view/vdom/vdom-utils.tsx b/frontend/app/view/vdom/vdom-utils.tsx
index c4bc7f2e9e..0f049c35bc 100644
--- a/frontend/app/view/vdom/vdom-utils.tsx
+++ b/frontend/app/view/vdom/vdom-utils.tsx
@@ -136,3 +136,49 @@ export function validateAndWrapReactStyle(model: VDomModel, style: Record {
+ if (!transferElem.waveid) {
+ return; // Skip elements without waveid
+ }
+ const elem: VDomElem = {
+ waveid: transferElem.tag !== "#text" ? transferElem.waveid : undefined,
+ tag: transferElem.tag,
+ props: transferElem.props,
+ text: transferElem.text,
+ children: [], // Placeholder to be populated later
+ };
+ elemMap[transferElem.waveid] = elem;
+
+ // Collect root elements
+ if (transferElem.root) {
+ roots.push(elem);
+ }
+ });
+
+ // Now populate children for each element
+ transferElems.forEach((transferElem) => {
+ if (!transferElem.waveid || !transferElem.children) return;
+
+ const currentElem = elemMap[transferElem.waveid];
+ currentElem.children = transferElem.children
+ .map((childId) => elemMap[childId])
+ .filter((child) => child !== undefined); // Filter out any undefined children
+ });
+
+ return roots;
+}
diff --git a/frontend/app/view/vdom/vdom.tsx b/frontend/app/view/vdom/vdom.tsx
index 89fd6f14d5..786ba13d56 100644
--- a/frontend/app/view/vdom/vdom.tsx
+++ b/frontend/app/view/vdom/vdom.tsx
@@ -145,8 +145,9 @@ const SvgUrlIdAttributes = {
function convertVDomFunc(model: VDomModel, fnDecl: VDomFunc, compId: string, propName: string): (e: any) => void {
return (e: any) => {
if ((propName == "onKeyDown" || propName == "onKeyDownCapture") && fnDecl["#keys"]) {
+ dlog("key event", fnDecl, e);
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
- for (let keyDesc of fnDecl.keys || []) {
+ for (let keyDesc of fnDecl["#keys"] || []) {
if (checkKeyPressed(waveEvent, keyDesc)) {
e.preventDefault();
e.stopPropagation();
diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts
index 42d4dc78d3..f3fb1279b8 100644
--- a/frontend/types/custom.d.ts
+++ b/frontend/types/custom.d.ts
@@ -134,48 +134,6 @@ declare global {
keyType: string;
};
- interface WaveKeyboardEvent {
- type: "keydown" | "keyup" | "keypress" | "unknown";
- /**
- * Equivalent to KeyboardEvent.key.
- */
- key: string;
- /**
- * Equivalent to KeyboardEvent.code.
- */
- code: string;
- /**
- * Equivalent to KeyboardEvent.shiftKey.
- */
- shift: boolean;
- /**
- * Equivalent to KeyboardEvent.controlKey.
- */
- control: boolean;
- /**
- * Equivalent to KeyboardEvent.altKey.
- */
- alt: boolean;
- /**
- * Equivalent to KeyboardEvent.metaKey.
- */
- meta: boolean;
- /**
- * cmd is special, on mac it is meta, on windows it is alt
- */
- cmd: boolean;
- /**
- * option is special, on mac it is alt, on windows it is meta
- */
- option: boolean;
-
- repeat: boolean;
- /**
- * Equivalent to KeyboardEvent.location.
- */
- location: number;
- }
-
type SubjectWithRef = rxjs.Subject & { refCount: number; release: () => void };
type HeaderElem =
diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts
index 8c0f80e932..44bfcc9ba0 100644
--- a/frontend/types/gotypes.d.ts
+++ b/frontend/types/gotypes.d.ts
@@ -650,6 +650,7 @@ declare global {
ts: number;
blockid: string;
opts?: VDomBackendOpts;
+ haswork?: boolean;
renderupdates?: VDomRenderUpdate[];
statesync?: VDomStateSync[];
refoperations?: VDomRefOperation[];
@@ -684,7 +685,13 @@ declare global {
type VDomEvent = {
waveid: string;
eventtype: string;
- eventdata: any;
+ globaleventtype?: string;
+ targetvalue?: string;
+ targetchecked?: boolean;
+ targetname?: string;
+ targetid?: string;
+ keydata?: WaveKeyboardEvent;
+ mousedata?: WavePointerData;
};
// vdom.VDomFrontendUpdate
@@ -708,7 +715,7 @@ declare global {
stoppropagation?: boolean;
preventdefault?: boolean;
globalevent?: string;
- keys?: string[];
+ #keys?: string[];
};
// vdom.VDomMessage
@@ -855,6 +862,21 @@ declare global {
datadir: string;
};
+ // vdom.WaveKeyboardEvent
+ type WaveKeyboardEvent = {
+ type: "keydown"|"keyup"|"keypress"|"unknown";
+ key: string;
+ code: string;
+ repeat?: boolean;
+ location?: number;
+ shift?: boolean;
+ control?: boolean;
+ alt?: boolean;
+ meta?: boolean;
+ cmd?: boolean;
+ option?: boolean;
+ };
+
// wshrpc.WaveNotificationOptions
type WaveNotificationOptions = {
title?: string;
@@ -878,6 +900,26 @@ declare global {
obj?: WaveObj;
};
+ // vdom.WavePointerData
+ type WavePointerData = {
+ button: number;
+ buttons: number;
+ clientx?: number;
+ clienty?: number;
+ pagex?: number;
+ pagey?: number;
+ screenx?: number;
+ screeny?: number;
+ movementx?: number;
+ movementy?: number;
+ shift?: boolean;
+ control?: boolean;
+ alt?: boolean;
+ meta?: boolean;
+ cmd?: boolean;
+ option?: boolean;
+ };
+
// waveobj.Window
type WaveWindow = WaveObj & {
workspaceid: string;
diff --git a/pkg/util/utilfn/marshal.go b/pkg/util/utilfn/marshal.go
new file mode 100644
index 0000000000..fca0dd3736
--- /dev/null
+++ b/pkg/util/utilfn/marshal.go
@@ -0,0 +1,145 @@
+// Copyright 2024, Command Line Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+package utilfn
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "strings"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+func ReUnmarshal(out any, in any) error {
+ barr, err := json.Marshal(in)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(barr, out)
+}
+
+// does a mapstructure using "json" tags
+func DoMapStructure(out any, input any) error {
+ dconfig := &mapstructure.DecoderConfig{
+ Result: out,
+ TagName: "json",
+ }
+ decoder, err := mapstructure.NewDecoder(dconfig)
+ if err != nil {
+ return err
+ }
+ return decoder.Decode(input)
+}
+
+func MapToStruct(in map[string]any, out any) error {
+ // Check that out is a pointer
+ outValue := reflect.ValueOf(out)
+ if outValue.Kind() != reflect.Ptr {
+ return fmt.Errorf("out parameter must be a pointer, got %v", outValue.Kind())
+ }
+
+ // Get the struct it points to
+ elem := outValue.Elem()
+ if elem.Kind() != reflect.Struct {
+ return fmt.Errorf("out parameter must be a pointer to struct, got pointer to %v", elem.Kind())
+ }
+
+ // Get type information
+ typ := elem.Type()
+
+ // For each field in the struct
+ for i := 0; i < typ.NumField(); i++ {
+ field := typ.Field(i)
+
+ // Skip unexported fields
+ if !field.IsExported() {
+ continue
+ }
+
+ name := getJSONName(field)
+ if value, ok := in[name]; ok {
+ if err := setValue(elem.Field(i), value); err != nil {
+ return fmt.Errorf("error setting field %s: %w", name, err)
+ }
+ }
+ }
+
+ return nil
+}
+
+func StructToMap(in any) (map[string]any, error) {
+ // Get value and handle pointer
+ val := reflect.ValueOf(in)
+ if val.Kind() == reflect.Ptr {
+ val = val.Elem()
+ }
+
+ // Check that we have a struct
+ if val.Kind() != reflect.Struct {
+ return nil, fmt.Errorf("input must be a struct or pointer to struct, got %v", val.Kind())
+ }
+
+ // Get type information
+ typ := val.Type()
+ out := make(map[string]any)
+
+ // For each field in the struct
+ for i := 0; i < typ.NumField(); i++ {
+ field := typ.Field(i)
+
+ // Skip unexported fields
+ if !field.IsExported() {
+ continue
+ }
+
+ name := getJSONName(field)
+ out[name] = val.Field(i).Interface()
+ }
+
+ return out, nil
+}
+
+// getJSONName returns the field name to use for JSON mapping
+func getJSONName(field reflect.StructField) string {
+ tag := field.Tag.Get("json")
+ if tag == "" || tag == "-" {
+ return field.Name
+ }
+ return strings.Split(tag, ",")[0]
+}
+
+// setValue attempts to set a reflect.Value with a given interface{} value
+func setValue(field reflect.Value, value any) error {
+ if value == nil {
+ return nil
+ }
+
+ valueRef := reflect.ValueOf(value)
+
+ // Direct assignment if types are exactly equal
+ if valueRef.Type() == field.Type() {
+ field.Set(valueRef)
+ return nil
+ }
+
+ // Check if types are assignable
+ if valueRef.Type().AssignableTo(field.Type()) {
+ field.Set(valueRef)
+ return nil
+ }
+
+ // If field is pointer and value isn't already a pointer, try address
+ if field.Kind() == reflect.Ptr && valueRef.Kind() != reflect.Ptr {
+ return setValue(field, valueRef.Addr().Interface())
+ }
+
+ // Try conversion if types are convertible
+ if valueRef.Type().ConvertibleTo(field.Type()) {
+ field.Set(valueRef.Convert(field.Type()))
+ return nil
+ }
+
+ return fmt.Errorf("cannot set value of type %v to field of type %v", valueRef.Type(), field.Type())
+}
diff --git a/pkg/util/utilfn/utilfn.go b/pkg/util/utilfn/utilfn.go
index 914dc4e97e..2e3acdfeb8 100644
--- a/pkg/util/utilfn/utilfn.go
+++ b/pkg/util/utilfn/utilfn.go
@@ -29,8 +29,6 @@ import (
"syscall"
"text/template"
"unicode/utf8"
-
- "github.com/mitchellh/mapstructure"
)
var HexDigits = []byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}
@@ -751,27 +749,6 @@ func IndentString(indent string, str string) string {
return rtn.String()
}
-func ReUnmarshal(out any, in any) error {
- barr, err := json.Marshal(in)
- if err != nil {
- return err
- }
- return json.Unmarshal(barr, out)
-}
-
-// does a mapstructure using "json" tags
-func DoMapStructure(out any, input any) error {
- dconfig := &mapstructure.DecoderConfig{
- Result: out,
- TagName: "json",
- }
- decoder, err := mapstructure.NewDecoder(dconfig)
- if err != nil {
- return err
- }
- return decoder.Decode(input)
-}
-
func SliceIdx[T comparable](arr []T, elem T) int {
for idx, e := range arr {
if e == elem {
diff --git a/pkg/vdom/vdom.go b/pkg/vdom/vdom.go
index d7c66177a5..a3cc04a7f9 100644
--- a/pkg/vdom/vdom.go
+++ b/pkg/vdom/vdom.go
@@ -7,10 +7,13 @@ import (
"context"
"encoding/json"
"fmt"
+ "log"
"reflect"
"strconv"
"strings"
"unicode"
+
+ "github.com/wavetermdev/waveterm/pkg/util/utilfn"
)
// ReactNode types = nil | string | Elem
@@ -25,6 +28,17 @@ type Hook struct {
Deps []any
}
+type Component[P any] func(props P) *VDomElem
+
+type styleAttrWrapper struct {
+ StyleAttr string
+ Val any
+}
+
+type styleAttrMapWrapper struct {
+ StyleAttrMap map[string]any
+}
+
func (e *VDomElem) Key() string {
keyVal, ok := e.Props[KeyPropKey]
if !ok {
@@ -54,6 +68,20 @@ func mergeProps(props *map[string]any, newProps map[string]any) {
}
}
+func mergeStyleAttr(props *map[string]any, styleAttr styleAttrWrapper) {
+ if *props == nil {
+ *props = make(map[string]any)
+ }
+ if (*props)["style"] == nil {
+ (*props)["style"] = make(map[string]any)
+ }
+ styleMap, ok := (*props)["style"].(map[string]any)
+ if !ok {
+ return
+ }
+ styleMap[styleAttr.StyleAttr] = styleAttr.Val
+}
+
func E(tag string, parts ...any) *VDomElem {
rtn := &VDomElem{Tag: tag}
for _, part := range parts {
@@ -65,13 +93,49 @@ func E(tag string, parts ...any) *VDomElem {
mergeProps(&rtn.Props, props)
continue
}
+ if styleAttr, ok := part.(styleAttrWrapper); ok {
+ mergeStyleAttr(&rtn.Props, styleAttr)
+ continue
+ }
+ if styleAttrMap, ok := part.(styleAttrMapWrapper); ok {
+ for k, v := range styleAttrMap.StyleAttrMap {
+ mergeStyleAttr(&rtn.Props, styleAttrWrapper{StyleAttr: k, Val: v})
+ }
+ continue
+ }
elems := partToElems(part)
rtn.Children = append(rtn.Children, elems...)
}
return rtn
}
-func P(propName string, propVal any) map[string]any {
+func Props(props any) map[string]any {
+ m, err := utilfn.StructToMap(props)
+ if err != nil {
+ return nil
+ }
+ return m
+}
+
+func PStyle(styleAttr string, propVal any) any {
+ return styleAttrWrapper{StyleAttr: styleAttr, Val: propVal}
+}
+
+func P(propName string, propVal any) any {
+ if propVal == nil {
+ return map[string]any{propName: nil}
+ }
+ if propName == "style" {
+ strVal, ok := propVal.(string)
+ if ok {
+ styleMap, err := styleAttrStrToStyleMap(strVal, nil)
+ if err == nil {
+ return styleAttrMapWrapper{StyleAttrMap: styleMap}
+ }
+ log.Printf("Error parsing style attribute: %v\n", err)
+ return nil
+ }
+ }
return map[string]any{propName: propVal}
}
diff --git a/pkg/vdom/vdom_comp.go b/pkg/vdom/vdom_comp.go
index 118375b369..12392ab983 100644
--- a/pkg/vdom/vdom_comp.go
+++ b/pkg/vdom/vdom_comp.go
@@ -12,7 +12,7 @@ type ChildKey struct {
Key string
}
-type Component struct {
+type ComponentImpl struct {
WaveId string
Tag string
Key string
@@ -26,13 +26,13 @@ type Component struct {
Text string
// base component -- vdom, wave elem, or #fragment
- Children []*Component
+ Children []*ComponentImpl
// component -> component
- Comp *Component
+ Comp *ComponentImpl
}
-func (c *Component) compMatch(tag string, key string) bool {
+func (c *ComponentImpl) compMatch(tag string, key string) bool {
if c == nil {
return false
}
diff --git a/pkg/vdom/vdom_html.go b/pkg/vdom/vdom_html.go
index 67721efc2d..dc94b7c8c9 100644
--- a/pkg/vdom/vdom_html.go
+++ b/pkg/vdom/vdom_html.go
@@ -273,17 +273,25 @@ func convertStyleToReactStyles(styleMap map[string]string, params map[string]any
return rtn
}
+func styleAttrStrToStyleMap(styleText string, params map[string]any) (map[string]any, error) {
+ parser := cssparser.MakeParser(styleText)
+ m, err := parser.Parse()
+ if err != nil {
+ return nil, err
+ }
+ return convertStyleToReactStyles(m, params), nil
+}
+
func fixStyleAttribute(elem *VDomElem, params map[string]any, elemPath []string) error {
styleText, ok := elem.Props["style"].(string)
if !ok {
return nil
}
- parser := cssparser.MakeParser(styleText)
- m, err := parser.Parse()
+ styleMap, err := styleAttrStrToStyleMap(styleText, params)
if err != nil {
return fmt.Errorf("%v (at %s)", err, makePathStr(elemPath))
}
- elem.Props["style"] = convertStyleToReactStyles(m, params)
+ elem.Props["style"] = styleMap
return nil
}
diff --git a/pkg/vdom/vdom_root.go b/pkg/vdom/vdom_root.go
index 3f26bc660c..b59f5dd37e 100644
--- a/pkg/vdom/vdom_root.go
+++ b/pkg/vdom/vdom_root.go
@@ -6,7 +6,6 @@ package vdom
import (
"context"
"fmt"
- "log"
"reflect"
"github.com/google/uuid"
@@ -19,7 +18,7 @@ var vdomContextKey = vdomContextKeyType{}
type VDomContextVal struct {
Root *RootElem
- Comp *Component
+ Comp *ComponentImpl
HookIdx int
}
@@ -31,9 +30,9 @@ type Atom struct {
type RootElem struct {
OuterCtx context.Context
- Root *Component
+ Root *ComponentImpl
CFuncs map[string]any
- CompMap map[string]*Component // component waveid -> component
+ CompMap map[string]*ComponentImpl // component waveid -> component
EffectWorkQueue []*EffectWorkElem
NeedsRenderMap map[string]bool
Atoms map[string]*Atom
@@ -64,7 +63,7 @@ func MakeRoot() *RootElem {
return &RootElem{
Root: nil,
CFuncs: make(map[string]any),
- CompMap: make(map[string]*Component),
+ CompMap: make(map[string]*ComponentImpl),
Atoms: make(map[string]*Atom),
}
}
@@ -151,11 +150,10 @@ func (r *RootElem) RegisterComponent(name string, cfunc any) error {
}
func (r *RootElem) Render(elem *VDomElem) {
- log.Printf("Render %s\n", elem.Tag)
r.render(elem, &r.Root)
}
-func (vdf *VDomFunc) CallFn() {
+func (vdf *VDomFunc) CallFn(event VDomEvent) {
if vdf.Fn == nil {
return
}
@@ -163,10 +161,18 @@ func (vdf *VDomFunc) CallFn() {
if rval.Kind() != reflect.Func {
return
}
- rval.Call(nil)
+ rtype := rval.Type()
+ if rtype.NumIn() == 0 {
+ rval.Call(nil)
+ }
+ if rtype.NumIn() == 1 {
+ if rtype.In(0) == reflect.TypeOf((*VDomEvent)(nil)).Elem() {
+ rval.Call([]reflect.Value{reflect.ValueOf(event)})
+ }
+ }
}
-func callVDomFn(fnVal any, data any) {
+func callVDomFn(fnVal any, data VDomEvent) {
if fnVal == nil {
return
}
@@ -192,13 +198,13 @@ func callVDomFn(fnVal any, data any) {
}
}
-func (r *RootElem) Event(id string, propName string, data any) {
+func (r *RootElem) Event(id string, propName string, event VDomEvent) {
comp := r.CompMap[id]
if comp == nil || comp.Elem == nil {
return
}
fnVal := comp.Elem.Props[propName]
- callVDomFn(fnVal, data)
+ callVDomFn(fnVal, event)
}
// this will be called by the frontend to say the DOM has been mounted
@@ -235,7 +241,7 @@ func (r *RootElem) RunWork() {
}
}
-func (r *RootElem) render(elem *VDomElem, comp **Component) {
+func (r *RootElem) render(elem *VDomElem, comp **ComponentImpl) {
if elem == nil || elem.Tag == "" {
r.unmount(comp)
return
@@ -264,7 +270,7 @@ func (r *RootElem) render(elem *VDomElem, comp **Component) {
r.renderComponent(cfunc, elem, comp)
}
-func (r *RootElem) unmount(comp **Component) {
+func (r *RootElem) unmount(comp **ComponentImpl) {
if *comp == nil {
return
}
@@ -287,21 +293,21 @@ func (r *RootElem) unmount(comp **Component) {
*comp = nil
}
-func (r *RootElem) createComp(tag string, key string, comp **Component) {
- *comp = &Component{WaveId: uuid.New().String(), Tag: tag, Key: key}
+func (r *RootElem) createComp(tag string, key string, comp **ComponentImpl) {
+ *comp = &ComponentImpl{WaveId: uuid.New().String(), Tag: tag, Key: key}
r.CompMap[(*comp).WaveId] = *comp
}
-func (r *RootElem) renderText(text string, comp **Component) {
+func (r *RootElem) renderText(text string, comp **ComponentImpl) {
if (*comp).Text != text {
(*comp).Text = text
}
}
-func (r *RootElem) renderChildren(elems []VDomElem, curChildren []*Component) []*Component {
- newChildren := make([]*Component, len(elems))
- curCM := make(map[ChildKey]*Component)
- usedMap := make(map[*Component]bool)
+func (r *RootElem) renderChildren(elems []VDomElem, curChildren []*ComponentImpl) []*ComponentImpl {
+ newChildren := make([]*ComponentImpl, len(elems))
+ curCM := make(map[ChildKey]*ComponentImpl)
+ usedMap := make(map[*ComponentImpl]bool)
for idx, child := range curChildren {
if child.Key != "" {
curCM[ChildKey{Tag: child.Tag, Idx: 0, Key: child.Key}] = child
@@ -311,7 +317,7 @@ func (r *RootElem) renderChildren(elems []VDomElem, curChildren []*Component) []
}
for idx, elem := range elems {
elemKey := elem.Key()
- var curChild *Component
+ var curChild *ComponentImpl
if elemKey != "" {
curChild = curCM[ChildKey{Tag: elem.Tag, Idx: 0, Key: elemKey}]
} else {
@@ -329,14 +335,14 @@ func (r *RootElem) renderChildren(elems []VDomElem, curChildren []*Component) []
return newChildren
}
-func (r *RootElem) renderSimple(elem *VDomElem, comp **Component) {
+func (r *RootElem) renderSimple(elem *VDomElem, comp **ComponentImpl) {
if (*comp).Comp != nil {
r.unmount(&(*comp).Comp)
}
(*comp).Children = r.renderChildren(elem.Children, (*comp).Children)
}
-func (r *RootElem) makeRenderContext(comp *Component) context.Context {
+func (r *RootElem) makeRenderContext(comp *ComponentImpl) context.Context {
var ctx context.Context
if r.OuterCtx != nil {
ctx = r.OuterCtx
@@ -359,7 +365,15 @@ func callCFunc(cfunc any, ctx context.Context, props map[string]any) any {
rval := reflect.ValueOf(cfunc)
arg2Type := rval.Type().In(1)
arg2Val := reflect.New(arg2Type)
- utilfn.ReUnmarshal(arg2Val.Interface(), props)
+ // if arg2 is a map, just pass props
+ if arg2Type.Kind() == reflect.Map {
+ arg2Val.Elem().Set(reflect.ValueOf(props))
+ } else {
+ err := utilfn.MapToStruct(props, arg2Val.Interface())
+ if err != nil {
+ fmt.Printf("error unmarshalling props: %v\n", err)
+ }
+ }
rtnVal := rval.Call([]reflect.Value{reflect.ValueOf(ctx), arg2Val.Elem()})
if len(rtnVal) == 0 {
return nil
@@ -367,7 +381,7 @@ func callCFunc(cfunc any, ctx context.Context, props map[string]any) any {
return rtnVal[0].Interface()
}
-func (r *RootElem) renderComponent(cfunc any, elem *VDomElem, comp **Component) {
+func (r *RootElem) renderComponent(cfunc any, elem *VDomElem, comp **ComponentImpl) {
if (*comp).Children != nil {
for _, child := range (*comp).Children {
r.unmount(&child)
@@ -414,7 +428,7 @@ func convertPropsToVDom(props map[string]any) map[string]any {
return vdomProps
}
-func convertBaseToVDom(c *Component) *VDomElem {
+func convertBaseToVDom(c *ComponentImpl) *VDomElem {
elem := &VDomElem{WaveId: c.WaveId, Tag: c.Tag}
if c.Elem != nil {
elem.Props = convertPropsToVDom(c.Elem.Props)
@@ -428,7 +442,7 @@ func convertBaseToVDom(c *Component) *VDomElem {
return elem
}
-func convertToVDom(c *Component) *VDomElem {
+func convertToVDom(c *ComponentImpl) *VDomElem {
if c == nil {
return nil
}
@@ -442,7 +456,7 @@ func convertToVDom(c *Component) *VDomElem {
}
}
-func (r *RootElem) makeVDom(comp *Component) *VDomElem {
+func (r *RootElem) makeVDom(comp *ComponentImpl) *VDomElem {
vdomElem := convertToVDom(comp)
return vdomElem
}
@@ -450,3 +464,53 @@ func (r *RootElem) makeVDom(comp *Component) *VDomElem {
func (r *RootElem) MakeVDom() *VDomElem {
return r.makeVDom(r.Root)
}
+
+func ConvertElemsToTransferElems(elems []VDomElem) []VDomTransferElem {
+ var transferElems []VDomTransferElem
+ textCounter := 0 // Counter for generating unique IDs for #text nodes
+
+ // Helper function to recursively process each VDomElem in preorder
+ var processElem func(elem VDomElem, isRoot bool) string
+ processElem = func(elem VDomElem, isRoot bool) string {
+ // Handle #text nodes by generating a unique placeholder ID
+ if elem.Tag == "#text" {
+ textId := fmt.Sprintf("text-%d", textCounter)
+ textCounter++
+ transferElems = append(transferElems, VDomTransferElem{
+ Root: isRoot,
+ WaveId: textId,
+ Tag: elem.Tag,
+ Text: elem.Text,
+ Props: nil,
+ Children: nil,
+ })
+ return textId
+ }
+
+ // Convert children to WaveId references, handling potential #text nodes
+ childrenIds := make([]string, len(elem.Children))
+ for i, child := range elem.Children {
+ childrenIds[i] = processElem(child, false) // Children are not roots
+ }
+
+ // Create the VDomTransferElem for the current element
+ transferElem := VDomTransferElem{
+ Root: isRoot,
+ WaveId: elem.WaveId,
+ Tag: elem.Tag,
+ Props: elem.Props,
+ Children: childrenIds,
+ Text: elem.Text,
+ }
+ transferElems = append(transferElems, transferElem)
+
+ return elem.WaveId
+ }
+
+ // Start processing each top-level element, marking them as roots
+ for _, elem := range elems {
+ processElem(elem, true)
+ }
+
+ return transferElems
+}
diff --git a/pkg/vdom/vdom_test.go b/pkg/vdom/vdom_test.go
index 42e8214d8e..db56a4415a 100644
--- a/pkg/vdom/vdom_test.go
+++ b/pkg/vdom/vdom_test.go
@@ -90,7 +90,7 @@ func Test1(t *testing.T) {
printVDom(root)
root.RunWork()
printVDom(root)
- root.Event(testContext.ButtonId, "onClick", nil)
+ root.Event(testContext.ButtonId, "onClick", VDomEvent{EventType: "onClick"})
root.RunWork()
printVDom(root)
}
diff --git a/pkg/vdom/vdom_types.go b/pkg/vdom/vdom_types.go
index 1c09d28175..e3b3c00e9f 100644
--- a/pkg/vdom/vdom_types.go
+++ b/pkg/vdom/vdom_types.go
@@ -31,6 +31,16 @@ type VDomElem struct {
Text string `json:"text,omitempty"`
}
+// the over the wire format for a vdom element
+type VDomTransferElem struct {
+ Root bool `json:"root,omitempty"`
+ WaveId string `json:"waveid,omitempty"` // required, except for #text nodes
+ Tag string `json:"tag"`
+ Props map[string]any `json:"props,omitempty"`
+ Children []string `json:"children,omitempty"`
+ Text string `json:"text,omitempty"`
+}
+
//// protocol messages
type VDomCreateContext struct {
@@ -74,6 +84,7 @@ type VDomBackendUpdate struct {
Ts int64 `json:"ts"`
BlockId string `json:"blockid"`
Opts *VDomBackendOpts `json:"opts,omitempty"`
+ HasWork bool `json:"haswork,omitempty"`
RenderUpdates []VDomRenderUpdate `json:"renderupdates,omitempty"`
StateSync []VDomStateSync `json:"statesync,omitempty"`
RefOperations []VDomRefOperation `json:"refoperations,omitempty"`
@@ -95,7 +106,7 @@ type VDomFunc struct {
StopPropagation bool `json:"stoppropagation,omitempty"`
PreventDefault bool `json:"preventdefault,omitempty"`
GlobalEvent string `json:"globalevent,omitempty"`
- Keys []string `json:"keys,omitempty"` // special for keyDown events a list of keys to "capture"
+ Keys []string `json:"#keys,omitempty"` // special for keyDown events a list of keys to "capture"
}
// used in props
@@ -128,9 +139,15 @@ type VDomRefPosition struct {
///// subbordinate protocol types
type VDomEvent struct {
- WaveId string `json:"waveid"` // empty for global events
- EventType string `json:"eventtype"`
- EventData any `json:"eventdata"`
+ WaveId string `json:"waveid"`
+ EventType string `json:"eventtype"` // usually the prop name (e.g. onClick, onKeyDown)
+ GlobalEventType string `json:"globaleventtype,omitempty"`
+ TargetValue string `json:"targetvalue,omitempty"`
+ TargetChecked bool `json:"targetchecked,omitempty"`
+ TargetName string `json:"targetname,omitempty"`
+ TargetId string `json:"targetid,omitempty"`
+ KeyData *WaveKeyboardEvent `json:"keydata,omitempty"`
+ MouseData *WavePointerData `json:"mousedata,omitempty"`
}
type VDomRenderContext struct {
@@ -199,3 +216,41 @@ type VDomKeyboardEvent struct {
Repeat bool `json:"repeat,omitempty"`
Location int `json:"location,omitempty"`
}
+
+type WaveKeyboardEvent struct {
+ Type string `json:"type" tstype:"\"keydown\"|\"keyup\"|\"keypress\"|\"unknown\""`
+ Key string `json:"key"` // KeyboardEvent.key
+ Code string `json:"code"` // KeyboardEvent.code
+ Repeat bool `json:"repeat,omitempty"`
+ Location int `json:"location,omitempty"` // KeyboardEvent.location
+
+ // modifiers
+ Shift bool `json:"shift,omitempty"`
+ Control bool `json:"control,omitempty"`
+ Alt bool `json:"alt,omitempty"`
+ Meta bool `json:"meta,omitempty"`
+ Cmd bool `json:"cmd,omitempty"` // special (on mac it is meta, on windows/linux it is alt)
+ Option bool `json:"option,omitempty"` // special (on mac it is alt, on windows/linux it is meta)
+}
+
+type WavePointerData struct {
+ Button int `json:"button"`
+ Buttons int `json:"buttons"`
+
+ ClientX int `json:"clientx,omitempty"`
+ ClientY int `json:"clienty,omitempty"`
+ PageX int `json:"pagex,omitempty"`
+ PageY int `json:"pagey,omitempty"`
+ ScreenX int `json:"screenx,omitempty"`
+ ScreenY int `json:"screeny,omitempty"`
+ MovementX int `json:"movementx,omitempty"`
+ MovementY int `json:"movementy,omitempty"`
+
+ // Modifiers
+ Shift bool `json:"shift,omitempty"`
+ Control bool `json:"control,omitempty"`
+ Alt bool `json:"alt,omitempty"`
+ Meta bool `json:"meta,omitempty"`
+ Cmd bool `json:"cmd,omitempty"` // special (on mac it is meta, on windows/linux it is alt)
+ Option bool `json:"option,omitempty"` // special (on mac it is alt, on windows/linux it is meta)
+}
diff --git a/pkg/vdom/vdomclient/vdomclient.go b/pkg/vdom/vdomclient/vdomclient.go
index b21bfdbed8..97972aa433 100644
--- a/pkg/vdom/vdomclient/vdomclient.go
+++ b/pkg/vdom/vdomclient/vdomclient.go
@@ -4,12 +4,14 @@
package vdomclient
import (
+ "context"
"fmt"
"log"
"net/http"
"os"
"sync"
"time"
+ "unicode"
"github.com/google/uuid"
"github.com/gorilla/mux"
@@ -64,7 +66,7 @@ func (c *Client) SetOverrideUrlHandler(handler http.Handler) {
c.OverrideUrlHandler = handler
}
-func MakeClient(opts *vdom.VDomBackendOpts) (*Client, error) {
+func MakeClient(opts *vdom.VDomBackendOpts) *Client {
client := &Client{
Lock: &sync.Mutex{},
Root: vdom.MakeRoot(),
@@ -74,34 +76,38 @@ func MakeClient(opts *vdom.VDomBackendOpts) (*Client, error) {
if opts != nil {
client.Opts = *opts
}
+ return client
+}
+
+func (client *Client) Connect() error {
jwtToken := os.Getenv(wshutil.WaveJwtTokenVarName)
if jwtToken == "" {
- return nil, fmt.Errorf("no %s env var set", wshutil.WaveJwtTokenVarName)
+ return fmt.Errorf("no %s env var set", wshutil.WaveJwtTokenVarName)
}
rpcCtx, err := wshutil.ExtractUnverifiedRpcContext(jwtToken)
if err != nil {
- return nil, fmt.Errorf("error extracting rpc context from %s: %v", wshutil.WaveJwtTokenVarName, err)
+ return fmt.Errorf("error extracting rpc context from %s: %v", wshutil.WaveJwtTokenVarName, err)
}
client.RpcContext = rpcCtx
if client.RpcContext == nil || client.RpcContext.BlockId == "" {
- return nil, fmt.Errorf("no block id in rpc context")
+ return fmt.Errorf("no block id in rpc context")
}
client.ServerImpl = &VDomServerImpl{BlockId: client.RpcContext.BlockId, Client: client}
sockName, err := wshutil.ExtractUnverifiedSocketName(jwtToken)
if err != nil {
- return nil, fmt.Errorf("error extracting socket name from %s: %v", wshutil.WaveJwtTokenVarName, err)
+ return fmt.Errorf("error extracting socket name from %s: %v", wshutil.WaveJwtTokenVarName, err)
}
rpcClient, err := wshutil.SetupDomainSocketRpcClient(sockName, client.ServerImpl)
if err != nil {
- return nil, fmt.Errorf("error setting up domain socket rpc client: %v", err)
+ return fmt.Errorf("error setting up domain socket rpc client: %v", err)
}
client.RpcClient = rpcClient
authRtn, err := wshclient.AuthenticateCommand(client.RpcClient, jwtToken, &wshrpc.RpcOpts{NoResponse: true})
if err != nil {
- return nil, fmt.Errorf("error authenticating rpc connection: %v", err)
+ return fmt.Errorf("error authenticating rpc connection: %v", err)
}
client.RouteId = authRtn.RouteId
- return client, nil
+ return nil
}
func (c *Client) SetRootElem(elem *vdom.VDomElem) {
@@ -170,6 +176,19 @@ func makeNullVDom() *vdom.VDomElem {
return &vdom.VDomElem{WaveId: uuid.New().String(), Tag: vdom.WaveNullTag}
}
+func DefineComponent[P any](client *Client, name string, renderFn func(ctx context.Context, props P) any) vdom.Component[P] {
+ if name == "" {
+ panic("Component name cannot be empty")
+ }
+ if !unicode.IsUpper(rune(name[0])) {
+ panic("Component name must start with an uppercase letter")
+ }
+ client.RegisterComponent(name, renderFn)
+ return func(props P) *vdom.VDomElem {
+ return vdom.E(name, vdom.Props(props))
+ }
+}
+
func (c *Client) RegisterComponent(name string, cfunc any) error {
return c.Root.RegisterComponent(name, cfunc)
}
@@ -185,6 +204,7 @@ func (c *Client) fullRender() (*vdom.VDomBackendUpdate, error) {
Type: "backendupdate",
Ts: time.Now().UnixMilli(),
BlockId: c.RpcContext.BlockId,
+ HasWork: len(c.Root.EffectWorkQueue) > 0,
Opts: &c.Opts,
RenderUpdates: []vdom.VDomRenderUpdate{
{UpdateType: "root", VDom: *renderedVDom},
diff --git a/pkg/vdom/vdomclient/vdomserverimpl.go b/pkg/vdom/vdomclient/vdomserverimpl.go
index 2d8e352b8d..69dc34c381 100644
--- a/pkg/vdom/vdomclient/vdomserverimpl.go
+++ b/pkg/vdom/vdomclient/vdomserverimpl.go
@@ -36,15 +36,15 @@ func (impl *VDomServerImpl) VDomRenderCommand(ctx context.Context, feUpdate vdom
}
// run events
for _, event := range feUpdate.Events {
- if event.WaveId == "" {
+ if event.GlobalEventType != "" {
if impl.Client.GlobalEventHandler != nil {
impl.Client.GlobalEventHandler(impl.Client, event)
}
} else {
- impl.Client.Root.Event(event.WaveId, event.EventType, event.EventData)
+ impl.Client.Root.Event(event.WaveId, event.EventType, event)
}
}
- if feUpdate.Resync {
+ if feUpdate.Resync || true {
return impl.Client.fullRender()
}
return impl.Client.incrementalRender()